108 |
111 | {
112 | displayColorPicker ?
113 |
114 |
115 |
120 |
:
121 | null
122 | }
123 |
;
124 | }
125 | };
126 |
127 | const ColorPicker = withStyles(styles)(Def);
128 | export default ColorPicker;
--------------------------------------------------------------------------------
/src/drawing/SVGDrawable.js:
--------------------------------------------------------------------------------
1 | import assign from 'object-assign';
2 |
3 | const constructors = [];
4 |
5 | function SVGDrawable(parent, drawable) {
6 | const children = new Map();
7 | this.element = null;
8 |
9 | function removeChild(childPath) {
10 | const svgDrawable = children.get(childPath);
11 | if (svgDrawable) {
12 | children.delete(childPath);
13 | }
14 | }
15 |
16 | this.update = () => {
17 | // update matrix - copy from drawable
18 | if (parent && drawable && !drawable.visible && this.element) {
19 | parent.removeChild(this.element);
20 | this.element = null;
21 | }
22 | if (drawable && drawable.visible && this.element) {
23 | this.element.id = drawable.id;
24 |
25 | const dataset = this.element.dataset;
26 | Object.keys(dataset).forEach(k => delete dataset[k]);
27 | if (drawable.data && typeof drawable.data === 'object') {
28 | // Object.keys(drawable.data).forEach(k => delete dataset[k]);
29 | assign(dataset, drawable.data);
30 | }
31 |
32 | // if (drawable.visible) {
33 | // this.element.removeAttribute('visibility');
34 | // } else {
35 | // this.element.setAttribute('visibility', 'collapse');
36 | // }
37 |
38 | if (drawable.opacity < 1) {
39 | this.element.setAttribute('opacity', drawable.opacity);
40 | } else {
41 | this.element.removeAttribute('opacity');
42 | }
43 |
44 | const {
45 | a,
46 | b,
47 | c,
48 | d,
49 | tx,
50 | ty
51 | } = drawable.matrix;
52 | if (a === 1 && b === 0 && c === 0 && d === 1 && tx === 0 && ty === 0) {
53 | // identity. don't need any transform
54 | this.element.removeAttribute('transform');
55 | } else {
56 | this.element.setAttribute('transform', `matrix(${[a, b, c, d, tx, ty].join(', ')})`);
57 | }
58 | }
59 | if (drawable && drawable.visible) {
60 | // update children
61 | drawable.children.forEach(childDrawable => {
62 | let svgDrawable = children.get(childDrawable);
63 | if (!svgDrawable) {
64 | let ChildConstructor = SVGDrawable;
65 | for (let i = 0; i < constructors.length; i++) {
66 | const [D, S] = constructors[i];
67 | if (childDrawable instanceof D) {
68 | ChildConstructor = S;
69 | break;
70 | }
71 | }
72 |
73 | svgDrawable = new ChildConstructor(this.element, childDrawable);
74 | children.set(childDrawable, svgDrawable);
75 | }
76 | svgDrawable.update();
77 | });
78 | }
79 | };
80 |
81 | const callDestroy = () => this.destroy();
82 | this.destroy = () => {
83 | if (parent && this.element && this.element.parentElement === parent) {
84 | parent.removeChild(this.element);
85 | }
86 |
87 | if (drawable) {
88 | drawable.off('destroy', callDestroy);
89 | drawable.off('remove', removeChild);
90 | }
91 |
92 | children.clear();
93 | };
94 |
95 | if (drawable) {
96 | drawable.on('destroy', callDestroy);
97 | drawable.on('remove', removeChild);
98 | }
99 | }
100 |
101 | SVGDrawable.register = (drawable, svgDrawable) => constructors.push([drawable, svgDrawable]);
102 |
103 | export default SVGDrawable;
--------------------------------------------------------------------------------
/src/drawing/SVGText.js:
--------------------------------------------------------------------------------
1 | import SVGDrawable from './SVGDrawable';
2 | import Text from './Text';
3 |
4 | import createSVGElement from './createSVGElement';
5 |
6 | const stylesheets = new WeakMap();
7 | const elementSizes = new WeakMap();
8 |
9 | function fontStyleRule(size) {
10 | return `.font-${size} {
11 | font-family: Arial;
12 | font-size: ${size}px;
13 | text-anchor: middle;
14 | dominant-baseline: middle;
15 | }`;
16 | }
17 |
18 | function setFontStyle(svg, element, size) {
19 | let sheetSizes = stylesheets.get(svg);
20 | if (elementSizes.has(element)) {
21 | // clear out old style if it exists
22 | const oldSize = elementSizes.get(element);
23 | if (oldSize === size) {
24 | return;
25 | }
26 |
27 | element.classList.remove('font-' + oldSize);
28 | if (oldSize && sheetSizes && sheetSizes.has(oldSize)) {
29 | const sheetInfo = sheetSizes.get(oldSize);
30 | const { elements, styleElement } = sheetInfo;
31 | if (elements && elements.has(element)) {
32 | elements.delete(element);
33 | if (!elements.size && styleElement.parentNode) {
34 | styleElement.parentNode.removeChild(styleElement);
35 | sheetSizes.delete(oldSize);
36 | }
37 | }
38 | }
39 | }
40 | if (!size) {
41 | return;
42 | }
43 |
44 | elementSizes.set(element, size);
45 |
46 | if (!sheetSizes) {
47 | sheetSizes = new Map();
48 | stylesheets.set(svg, sheetSizes);
49 | }
50 | let sheetInfo = sheetSizes.get(size);
51 | if (!sheetInfo) {
52 | const styleElement = createSVGElement('style', svg.ownerDocument);
53 | svg.insertBefore(styleElement, svg.firstChild);
54 | const rule = fontStyleRule(size);
55 | styleElement.appendChild(svg.ownerDocument.createTextNode(rule));
56 | sheetInfo = {
57 | styleElement,
58 | elements: new Set()
59 | };
60 | sheetSizes.set(size, sheetInfo);
61 | }
62 | sheetInfo.elements.add(element);
63 | element.classList.add('font-' + size);
64 | }
65 |
66 | function SVGText(parent, drawable) {
67 | SVGDrawable.call(this, parent, drawable);
68 |
69 | let textElement = null;
70 |
71 | const svg = parent.ownerSVGElement;
72 |
73 | const superUpdate = this.update;
74 | this.update = () => {
75 | const text = drawable && drawable.text || '';
76 | if (!this.element) {
77 | textElement = this.element = createSVGElement('text');
78 | textElement.appendChild(document.createTextNode(text));
79 |
80 | if (parent) {
81 | parent.appendChild(this.element);
82 | }
83 | } else {
84 | textElement.firstChild.nodeValue = text;
85 | }
86 |
87 | setFontStyle(svg, textElement, drawable.visible ? drawable.size : undefined);
88 |
89 | if (drawable && text) {
90 | const hex = ('00000' + drawable.color.toString(16)).slice(-6);
91 | textElement.setAttribute('fill', '#' + hex);
92 | }
93 |
94 | superUpdate.call(this);
95 | };
96 |
97 | const superDestroy = this.destroy;
98 | this.destroy = () => {
99 | if (textElement) {
100 | setFontStyle(svg, textElement, undefined);
101 | }
102 | superDestroy.call(this);
103 | };
104 | }
105 |
106 | SVGText.prototype = Object.create(SVGDrawable.prototype);
107 | SVGDrawable.register(Text, SVGText);
108 |
109 | export default SVGText;
--------------------------------------------------------------------------------
/src/components/Slider.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Slider from 'rc-slider';
4 | import 'rc-slider/assets/index.css';
5 |
6 | /*
7 | Material UI stuff
8 | */
9 | import withStyles from '@material-ui/core/styles/withStyles';
10 | import PropTypes from 'prop-types';
11 | import Tooltip from '@material-ui/core/Tooltip';
12 |
13 | const styles = theme => ({
14 | container: {
15 | padding: `0 ${theme.spacing.unit * 2}px`
16 | }
17 | });
18 |
19 | const Handle = Slider.Handle;
20 |
21 | function createSliderWithTooltip(Component) {
22 | return class HandleWrapper extends React.Component {
23 | static propTypes = {
24 | classes: PropTypes.object.isRequired,
25 | theme: PropTypes.object.isRequired,
26 | marks: PropTypes.object,
27 | tipFormatter: PropTypes.func,
28 | handleStyle: PropTypes.arrayOf(PropTypes.object),
29 | tipProps: PropTypes.object
30 | }
31 |
32 | static defaultProps = {
33 | tipFormatter: value => value,
34 | handleStyle: [{}],
35 | tipProps: {}
36 | }
37 |
38 | constructor(props) {
39 | super(props);
40 | this.state = { visibles: {} };
41 | }
42 |
43 | handleTooltipVisibleChange = (index, visible) => {
44 | this.setState((prevState) => {
45 | return {
46 | visibles: {
47 | ...prevState.visibles,
48 | [index]: visible
49 | }
50 | };
51 | });
52 | }
53 | handleWithTooltip = ({ value, dragging, index, disabled, ...restProps }) => {
54 | const {
55 | tipFormatter,
56 | tipProps,
57 | theme
58 | } = this.props;
59 |
60 | const {
61 | title = tipFormatter(value),
62 | placement = 'top',
63 | ...restTooltipProps
64 | } = tipProps;
65 |
66 | // todo: replace prefixCls with className?
67 | // todo: override tooltipOpen to remove animation
68 |
69 | const handleStyle = {
70 | borderColor: theme.palette.primary.main
71 | };
72 |
73 | return (
74 |
81 | this.handleTooltipVisibleChange(index, true)}
86 | onMouseLeave={() => this.handleTooltipVisibleChange(index, false)}
87 | />
88 |
89 | );
90 | }
91 | render() {
92 | const { classes, theme } = this.props;
93 | const trackStyle = {
94 | backgroundColor: theme.palette.primary.main
95 | };
96 |
97 | const style = {};
98 | if (this.props.marks) {
99 | style.marginBottom = 32;
100 | }
101 |
102 | const props = {
103 | style,
104 | trackStyle,
105 | ...this.props
106 | };
107 | return
108 |
112 |
;
113 | }
114 | };
115 | }
116 |
117 | const Def = withStyles(styles, { withTheme: true })(createSliderWithTooltip(Slider));
118 | export default Def;
119 |
120 | // const Range = withStyles(styles)(createSliderWithTooltip(Slider.Range));
121 | // export Range
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | education, socio-economic status, nationality, personal appearance, race,
10 | religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at support@datavized.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 |
--------------------------------------------------------------------------------
/src/drawing/PixiLinePath.js:
--------------------------------------------------------------------------------
1 | import PixiDrawable from './PixiDrawable';
2 | import LinePath from './LinePath';
3 |
4 | import { Graphics, GraphicsRenderer } from '@pixi/graphics';
5 | import { Renderer } from '@pixi/core';
6 | import { Container } from '@pixi/display';
7 |
8 | Renderer.registerPlugin('graphics', GraphicsRenderer);
9 |
10 | function PixiLinePath(parent, drawable) {
11 | PixiDrawable.call(this, parent, drawable);
12 |
13 | const points = drawable && drawable.points;
14 |
15 | const superUpdate = this.update;
16 | this.update = () => {
17 | const drawable = this.drawable;
18 |
19 | // create mesh or container if haven't done it yet
20 | if (!this.pixiObject) {
21 | if (points && points.length > 1) {
22 | const graphics = new Graphics();
23 | graphics.lineColor = 0;
24 | graphics.lineAlpha = 1;
25 | graphics.lineWidth = drawable.lineWidth;
26 |
27 | const firstPoint = points[0];
28 | graphics.moveTo(firstPoint[0], firstPoint[1]);
29 | for (let i = 1; i < points.length; i++) {
30 | const point = points[i];
31 | graphics.lineTo(point[0], point[1]);
32 | }
33 |
34 | this.pixiObject = graphics;
35 | } else {
36 | this.pixiObject = new Container();
37 | }
38 | if (parent) {
39 | parent.addChild(this.pixiObject);
40 | }
41 | }
42 |
43 | const pixiObject = this.pixiObject;
44 | if (drawable && pixiObject.graphicsData &&
45 | (pixiObject.lineColor !== drawable.color || pixiObject.lineAlpha !== drawable.opacity)) {
46 |
47 | pixiObject.lineColor = drawable.color;
48 | pixiObject.graphicsData.forEach(data => {
49 | data.lineColor = drawable.color;
50 | data.lineAlpha = drawable.opacity;
51 | });
52 | pixiObject.dirty++;
53 |
54 | /*
55 | PIXI Doesn't provide a way to change the colors of lines
56 | once they are converted to geometry, so we need to reach
57 | under the hood to do it. This is a bit slow and clunky,
58 | but it's much faster than it would be to regenerate the
59 | entire geometry from scratch every time
60 | */
61 |
62 | // eslint-disable-next-line no-underscore-dangle
63 | const webglData = pixiObject._webGL;
64 | if (webglData) {
65 | const color = drawable.color;
66 | const alpha = drawable.opacity;
67 |
68 | /* eslint-disable no-bitwise */
69 | const r = (color >> 16 & 255) / 255; // red
70 | const g = (color >> 8 & 255) / 255; // green
71 | const b = (color & 255) / 255; // blue
72 | /* eslint-enable no-bitwise */
73 |
74 | Object.keys(webglData).forEach(i => {
75 | webglData[i].data.forEach(webglGraphicsData => {
76 | for (let i = 0, n = webglGraphicsData.points.length; i < n; i += 6) {
77 | const ri = i + 2;
78 | const gi = i + 3;
79 | const bi = i + 4;
80 | const ai = i + 5;
81 | webglGraphicsData.points[ri] = r;
82 | webglGraphicsData.glPoints[ri] = r;
83 |
84 | webglGraphicsData.points[gi] = g;
85 | webglGraphicsData.glPoints[gi] = g;
86 |
87 | webglGraphicsData.points[bi] = b;
88 | webglGraphicsData.glPoints[bi] = b;
89 |
90 | webglGraphicsData.points[ai] = alpha;
91 | webglGraphicsData.glPoints[ai] = alpha;
92 | }
93 | webglGraphicsData.dirty = true;
94 | });
95 | });
96 | }
97 | }
98 | superUpdate.call(this);
99 | };
100 | }
101 |
102 | PixiLinePath.prototype = Object.create(PixiDrawable.prototype);
103 | PixiDrawable.register(LinePath, PixiLinePath);
104 |
105 | export default PixiLinePath;
--------------------------------------------------------------------------------
/src/components/SelectChartType.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import { Map as map } from 'immutable';
4 |
5 | import charts from '../charts';
6 | import { defaultTree } from '../evolution';
7 |
8 | /*
9 | Material UI components
10 | */
11 | import withStyles from '@material-ui/core/styles/withStyles';
12 | import GridListTileBar from '@material-ui/core/GridListTileBar';
13 |
14 | import Tip from './Tip';
15 | import Section from './Section';
16 |
17 | const styles = theme => ({
18 | root: {
19 | display: 'flex',
20 | flexDirection: 'column',
21 | justifyContent: 'center',
22 | flex: 1,
23 | minHeight: 0
24 | },
25 | main: {
26 | flex: 1,
27 | overflowY: 'auto',
28 | margin: `${theme.spacing.unit}px 10%`
29 | },
30 | gridList: {
31 | display: 'grid',
32 | gridGap: `${theme.spacing.unit}px`,
33 | gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 256px))',
34 | gridAutoRows: 'minmax(150px, 256px)',
35 | justifyContent: 'center'
36 | },
37 | '@media (max-width: 450px), (max-height: 450px)': {
38 | gridList: {
39 | gridTemplateColumns: 'repeat(auto-fill, 140px)',
40 | gridAutoRows: '140px'
41 | },
42 | instructions: {
43 | margin: `${theme.spacing.unit * 1}px 0`
44 | }
45 | },
46 | tile: {
47 | cursor: 'pointer',
48 | border: '4px solid transparent',
49 | boxSizing: 'border-box',
50 | display: 'inline-block',
51 | position: 'relative',
52 | padding: theme.spacing.unit,
53 |
54 | '& > img': {
55 | maxWidth: '100%',
56 | maxHeight: '100%'
57 | }
58 | },
59 | selected: {
60 | border: `4px solid ${theme.palette.primary.main}`
61 | }
62 | });
63 |
64 | const chartTypes = Object.values(charts);
65 |
66 | const Def = class SelectChartType extends Section {
67 | setChartType = chartType => {
68 | this.props.setData(data => {
69 | const previousChartType = data.get('chartType');
70 | if (previousChartType !== chartType) {
71 | data = data.set('chartType', chartType);
72 |
73 | // add default gene tree with single node for chartType
74 | const allTreesData = map().set(chartType, defaultTree(charts[chartType]));
75 | data = data.set('tree', allTreesData);
76 | }
77 |
78 | return data;
79 | });
80 | }
81 |
82 | render() {
83 | const { classes, data, onNext/*, navigation*/ } = this.props;
84 | const chartType = data.get('chartType') || '';
85 | return
86 |
87 |
88 | Step 3 - Design
89 | Choose any chart type, then select Organize to prepare visualization.
90 |
91 |
92 |
93 | {chartTypes.map(chart =>
94 |
{
100 | this.setChartType(chart.key);
101 | if (onNext) {
102 | setTimeout(onNext, 0);
103 | }
104 | }}
105 | >
106 | { }
109 |
113 |
114 | )}
115 |
116 |
117 |
118 | {/*navigation*/}
119 | ;
120 |
121 | }
122 | };
123 |
124 | const SelectChartType = withStyles(styles)(Def);
125 | export default SelectChartType;
--------------------------------------------------------------------------------
/src/components/asyncComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | const queue = new Set();
4 |
5 | function isConnected() {
6 | return navigator.onLine === undefined || navigator.onLine;
7 | }
8 |
9 | function renderComponent(component) {
10 | return typeof component === 'function' ?
11 | React.createElement(component) :
12 | component || null;
13 | }
14 |
15 | function asyncComponent(importComponent, options = {}) {
16 | const {
17 | load: loadingComponents,
18 | fail: failComponents,
19 | defer
20 | } = options;
21 |
22 | let preloadTimeout = 0;
23 |
24 | function load() {
25 | clearTimeout(preloadTimeout);
26 | queue.add(importComponent);
27 |
28 | const p = importComponent().then(component => {
29 | queue.delete(importComponent);
30 | return component;
31 | });
32 | p.catch(() => {
33 | queue.delete(importComponent);
34 | });
35 | return p;
36 | }
37 |
38 | function attemptPreload() {
39 | if (queue.size) {
40 | preloadTimeout = setTimeout(attemptPreload, 500);
41 | } else {
42 | load();
43 | }
44 | }
45 |
46 | class AsyncComponent extends Component {
47 | state = {
48 | component: null,
49 | connected: isConnected(),
50 | requested: false,
51 | failed: false,
52 | attempts: 0
53 | }
54 |
55 | isMounted = false
56 |
57 | load = () => {
58 | if (this.state.requested) {
59 | // don't request twice
60 | return;
61 | }
62 |
63 |
64 | this.setState({
65 | component: null,
66 | requested: true,
67 | attempts: this.state.attempts + 1
68 | });
69 |
70 | // todo: maybe load doesn't return a promise?
71 | load().then(component => new Promise(resolve => {
72 | if (this.isMounted) {
73 | this.setState({
74 | // handle both es imports and cjs
75 | component: component.default ? component.default : component,
76 | requested: false
77 | }, () => resolve());
78 | }
79 | })).catch(err => {
80 | console.error('Failed to load component', err);
81 | if (this.isMounted) {
82 | this.setState({
83 | requested: false,
84 | failed: true
85 | });
86 | }
87 | });
88 | }
89 |
90 | retry = () => {
91 | if (this.state.attempts >= 3 && !this.state.requested) {
92 | window.location.reload();
93 | } else {
94 | this.load();
95 | }
96 | }
97 |
98 | updateOnlineStatus = () => {
99 | const connected = isConnected();
100 | if (connected && !queue.size) {
101 | this.load();
102 | }
103 | this.setState({
104 | connected
105 | });
106 | }
107 |
108 | componentDidMount() {
109 | this.isMounted = true;
110 | window.addEventListener('online', this.updateOnlineStatus);
111 | window.addEventListener('offline', this.updateOnlineStatus);
112 | this.load();
113 | }
114 |
115 | componentWillUnmount() {
116 | this.isMounted = false;
117 | window.removeEventListener('online', this.updateOnlineStatus);
118 | window.removeEventListener('offline', this.updateOnlineStatus);
119 | }
120 |
121 | render() {
122 | const C = this.state.component;
123 | if (C) {
124 | return
;
125 | }
126 |
127 | if (this.state.failed && !this.state.requested && failComponents) {
128 | if (typeof failComponents === 'function') {
129 | const F = failComponents;
130 | return
;
131 | }
132 | return failComponents;
133 | }
134 |
135 | return renderComponent(loadingComponents);
136 | }
137 | }
138 |
139 | if (!defer) {
140 | setTimeout(attemptPreload, 200);
141 | }
142 |
143 | return AsyncComponent;
144 | }
145 |
146 | export default asyncComponent;
--------------------------------------------------------------------------------
/src/components/SampleMenuItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | /*
5 | Material UI components
6 | */
7 | import PropTypes from 'prop-types';
8 | import withStyles from '@material-ui/core/styles/withStyles';
9 | import MenuItem from '@material-ui/core/MenuItem';
10 | import Typography from '@material-ui/core/Typography';
11 | import Collapse from '@material-ui/core/Collapse';
12 | import InfoIcon from '@material-ui/icons/Info';
13 |
14 | // import { emphasize } from '@material-ui/core/styles/colorManipulator';
15 |
16 | const styles = theme => ({
17 | menuItem: {
18 | display: 'block',
19 | height: 'auto',
20 | minHeight: 24,
21 | paddingLeft: 0,
22 | lineHeight: 'initial',
23 | overflow: 'auto',
24 | borderBottom: `1px solid ${theme.palette.divider}`
25 | },
26 | topLine: {
27 | display: 'flex',
28 | justifyContent: 'space-between'
29 | },
30 | infoIcon: {
31 | cursor: 'pointer',
32 | color: theme.palette.text.hint
33 | },
34 | caption: {
35 | fontSize: '0.6em'
36 | },
37 | title: {},
38 | description: {
39 | whiteSpace: 'pre-wrap'
40 | },
41 | metadata: {
42 | padding: theme.spacing.unit * 2,
43 | // backgroundColor: emphasize(theme.palette.background.paper, 0.05),
44 | boxShadow: `inset 0 11px 15px -11px rgba(0, 0, 0, 0.2)`,
45 | '& > p': {
46 | margin: 0
47 | },
48 | '& > p > a': {
49 | // textDecoration: 'none',
50 | color: theme.palette.text.hint
51 | },
52 | '& > p > a:hover': {
53 | color: theme.palette.text.secondary
54 | }
55 | },
56 | expanded: {
57 | '& > $topLine > $title': {
58 | fontWeight: 'bold'
59 | },
60 | '& > $topLine > $infoIcon': {
61 | color: theme.palette.text.primary
62 | }
63 | }
64 | });
65 |
66 | function noClick(evt) {
67 | evt.stopPropagation();
68 | }
69 |
70 | function metadataLine(text, link, className) {
71 | if (!text) {
72 | return null;
73 | }
74 | return {
75 | link ?
76 | {text} :
77 | text
78 | } ;
79 | }
80 |
81 | const Def = class SampleMenuItem extends React.Component {
82 | static propTypes = {
83 | classes: PropTypes.object.isRequired,
84 | onClick: PropTypes.func.isRequired,
85 | title: PropTypes.string.isRequired,
86 | source: PropTypes.string,
87 | license: PropTypes.string,
88 | licenseLink: PropTypes.string,
89 | description: PropTypes.string
90 | }
91 |
92 | state = {
93 | expanded: false
94 | };
95 |
96 | toggleExpanded = evt => {
97 | const expanded = !this.state.expanded;
98 | this.setState({ expanded });
99 | evt.stopPropagation();
100 | }
101 |
102 | render() {
103 | const {
104 | classes,
105 | onClick,
106 | title,
107 | source,
108 | license,
109 | licenseLink,
110 | description
111 | } = this.props;
112 |
113 | const { expanded } = this.state;
114 | return
117 |
118 | {title}
119 |
120 |
121 |
122 |
123 | {metadataLine(description, null, classNames(classes.caption, classes.description))}
124 | {metadataLine(source && 'Source', source, classes.caption)}
125 | {metadataLine(license, licenseLink, classes.caption)}
126 |
127 |
128 | ;
129 | }
130 | };
131 |
132 | const SampleMenuItem = withStyles(styles)(Def);
133 | export default SampleMenuItem;
--------------------------------------------------------------------------------
/src/charts/bar/index.js:
--------------------------------------------------------------------------------
1 | import draw from './draw';
2 | export default {
3 | key: 'bar',
4 | name: 'Bar Chart',
5 | preview: require('./thumb.png'),
6 | draw,
7 | required: ['height', 'width', 'x', 'y'],
8 | valid: fields =>
9 | fields.has('height') ||
10 | fields.has('width') ||
11 | fields.has('x') ||
12 | fields.has('y'),
13 | randomize(fields) {
14 | if (!fields.length) {
15 | return {};
16 | }
17 |
18 | const horizontal = Math.random() > 0.5;
19 | const dimField = horizontal ? 'width' : 'height';
20 | const posField = horizontal ? 'y' : 'x';
21 |
22 | const fieldNames = [dimField, posField];
23 |
24 | const indices = Object.keys(fields).map(parseFloat);
25 | let numericFieldIndices = indices.filter(index => {
26 | const f = fields[index];
27 | const type = f.type;
28 | return type === 'int' || type === 'float';
29 | });
30 |
31 | const fieldMap = {};
32 | fieldNames.forEach(name => {
33 | if (!numericFieldIndices.length) {
34 | numericFieldIndices = indices;
35 | }
36 | const count = numericFieldIndices.length;
37 | const i = Math.floor(Math.random() * count) % count;
38 | const index = numericFieldIndices[i];
39 | numericFieldIndices.splice(i, 1);
40 | fieldMap[name] = index;
41 | });
42 | return fieldMap;
43 | },
44 | properties: [
45 | {
46 | key: 'height',
47 | name: 'Height'
48 | },
49 | {
50 | key: 'width',
51 | name: 'Width'
52 | },
53 | {
54 | key: 'x',
55 | name: 'X Position'
56 | },
57 | {
58 | key: 'y',
59 | name: 'Y Position'
60 | },
61 | {
62 | key: 'order',
63 | name: 'Order'
64 | },
65 | {
66 | key: 'color',
67 | name: 'Color'
68 | },
69 | {
70 | key: 'label',
71 | name: 'Label'
72 | }
73 | ],
74 | geneCategories: [
75 | {
76 | category: 'Color',
77 | genes: [
78 | ['Hue Range', 'hueRange'],
79 | ['Hue Offset', 'hueOffset'],
80 | ['Saturation Offset', 'saturationOffset'],
81 | ['Saturation Factor', 'saturationValueFactor'],
82 | ['Lightness Offset', 'lightnessOffset'],
83 | ['Lightness Factor', 'lightnessValueFactor']
84 | ]
85 | },
86 | {
87 | category: 'Width',
88 | genes: [
89 | ['Offset', 'widthOffset'],
90 | ['Rank Factor', 'widthRankFactor'],
91 | ['Random Factor', 'widthRandomFactor']
92 | ]
93 | },
94 | {
95 | category: 'Height',
96 | genes: [
97 | ['Offset', 'heightOffset'],
98 | ['Rank Factor', 'heightRankFactor'],
99 | ['Random Factor', 'heightRandomFactor']
100 | ]
101 | },
102 | {
103 | category: 'X Position',
104 | genes: [
105 | ['Offset', 'xOffset'],
106 | ['Rank Factor', 'xRankFactor'],
107 | ['Random Factor', 'xRandomFactor']
108 | ]
109 | },
110 | {
111 | category: 'Y Position',
112 | genes: [
113 | ['Offset', 'yOffset'],
114 | ['Rank Factor', 'yRankFactor'],
115 | ['Random Factor', 'yRandomFactor']
116 | ]
117 | },
118 | {
119 | category: 'Rotation',
120 | genes: [
121 | ['Offset', 'rotationOffset'],
122 | ['Rank Factor', 'rotationRankFactor'],
123 | // ['Value Factor', 'rotationValueFactor'],
124 | ['Random Factor', 'rotationRandomFactor']
125 | ]
126 | }
127 | ],
128 | genes: {
129 | widthOffset: 0,
130 | widthRankFactor: 0,
131 | widthRandomFactor: 0,
132 |
133 | heightOffset: 0,
134 | heightRankFactor: 0,
135 | heightRandomFactor: 0,
136 |
137 | xOffset: 0,
138 | xRankFactor: 0,
139 | xRandomFactor: 0,
140 |
141 | yOffset: 0,
142 | yRankFactor: 0,
143 | yRandomFactor: 0,
144 |
145 | rotationOffset: 0,
146 | rotationRankFactor: 0,
147 | rotationValueFactor: 0,
148 | rotationRandomFactor: 0,
149 |
150 | hueOffset: 0,
151 | hueRange: 0.6,
152 | saturationOffset: 1,
153 | saturationValueFactor: 0,
154 | lightnessOffset: 0.5,
155 | lightnessValueFactor: 0
156 | }
157 | };
--------------------------------------------------------------------------------
/src/util/svgToMesh.js:
--------------------------------------------------------------------------------
1 | /*
2 | Adapted from https://github.com/mattdesl/svg-mesh-3d/
3 | MIT License
4 | - Code style change for readability
5 | - Use custom adaptation of normalize function (don't convert lines to curves)
6 | - Don't convert points to 3D or flip Y axis
7 | - Remove modules we don't plan to run to save on size
8 | */
9 |
10 | import parseSVG from 'parse-svg-path';
11 | import getContours from './contours';
12 | import cdt2d from 'cdt2d';
13 | import cleanPSLG from 'clean-pslg';
14 | // import getBounds from 'bound-points';
15 | // import normalize from 'normalize-path-scale';
16 | // import random from 'random-float';
17 | import assign from 'object-assign';
18 | // import simplify from 'simplify-path';
19 |
20 | // function to3D(positions) {
21 | // for (let i = 0; i < positions.length; i++) {
22 | // const xy = positions[i];
23 | // xy[1] *= -1;
24 | // xy[2] = xy[2] || 0;
25 | // }
26 | // }
27 |
28 | // function addRandomPoints(positions, bounds, count) {
29 | // const min = bounds[0];
30 | // const max = bounds[1];
31 |
32 | // for (let i = 0; i < count; i++) {
33 | // positions.push([ // random [ x, y ]
34 | // random(min[0], max[0]),
35 | // random(min[1], max[1])
36 | // ]);
37 | // }
38 | // }
39 |
40 | function denestPolyline(nested) {
41 | const positions = [];
42 | const edges = [];
43 |
44 | for (let i = 0; i < nested.length; i++) {
45 | const path = nested[i];
46 | const loop = [];
47 | for (let j = 0; j < path.length; j++) {
48 | const pos = path[j];
49 | let idx = positions.indexOf(pos);
50 | if (idx === -1) {
51 | positions.push(pos);
52 | idx = positions.length - 1;
53 | }
54 | loop.push(idx);
55 | }
56 | edges.push(loop);
57 | }
58 | return {
59 | positions: positions,
60 | edges: edges
61 | };
62 | }
63 |
64 | function svgToMesh(svgPath, opt) {
65 | if (typeof svgPath !== 'string') {
66 | throw new TypeError('must provide a string as first parameter');
67 | }
68 |
69 | opt = assign({
70 | delaunay: true,
71 | clean: true,
72 | exterior: false,
73 | randomization: 0,
74 | simplify: 0,
75 | scale: 1
76 | }, opt);
77 |
78 | // parse string as a list of operations
79 | const svg = parseSVG(svgPath);
80 |
81 | // convert curves into discrete points
82 | const contours = getContours(svg, opt.scale);
83 |
84 | // optionally simplify the path for faster triangulation and/or aesthetics
85 | // if (opt.simplify > 0 && typeof opt.simplify === 'number') {
86 | // for (let i = 0; i < contours.length; i++) {
87 | // contours[i] = simplify(contours[i], opt.simplify);
88 | // }
89 | // }
90 |
91 | // prepare for triangulation
92 | const polyline = denestPolyline(contours);
93 | const positions = polyline.positions;
94 | // const bounds = getBounds(positions);
95 |
96 | // optionally add random points for aesthetics
97 | // const randomization = opt.randomization;
98 | // if (typeof randomization === 'number' && randomization > 0) {
99 | // addRandomPoints(positions, bounds, randomization);
100 | // }
101 |
102 | const loops = polyline.edges;
103 | const edges = [];
104 | for (let i = 0; i < loops.length; ++i) {
105 | const loop = loops[i];
106 | for (let j = 0; j < loop.length; j++) {
107 | edges.push([loop[j], loop[(j + 1) % loop.length]]);
108 | }
109 | }
110 |
111 | // this updates points/edges so that they now form a valid PSLG
112 | if (opt.clean !== false) {
113 | cleanPSLG(positions, edges);
114 | }
115 |
116 | // triangulate mesh
117 | const cells = cdt2d(positions, edges, opt);
118 |
119 | // rescale to [-1 ... 1]
120 | // if (opt.normalize !== false) {
121 | // normalize(positions, bounds);
122 | // }
123 |
124 | // convert to 3D representation and flip on Y axis for convenience w/ OpenGL
125 | // to3D(positions);
126 |
127 | return {
128 | positions: positions,
129 | cells: cells
130 | };
131 | }
132 |
133 | export default svgToMesh;
--------------------------------------------------------------------------------
/src/charts/radialArea/index.js:
--------------------------------------------------------------------------------
1 | import draw from './draw';
2 | import { defaultColorValues } from '../geneColor';
3 |
4 | export default {
5 | key: 'radialArea',
6 | name: 'Radial Area',
7 | preview: require('./thumb.png'),
8 | required: ['radius'],
9 | // valid: fields =>
10 | // fields.has('height') ||
11 | // fields.has('width') ||
12 | // fields.has('x') ||
13 | // fields.has('y'),
14 | randomize(fields) {
15 | if (!fields.length) {
16 | return {};
17 | }
18 |
19 | const fieldNames = ['radius', 'time'];
20 |
21 | if (Math.random() > 0.75) {
22 | fieldNames.push('angle');
23 | }
24 |
25 | const indices = Object.keys(fields).map(parseFloat);
26 | let numericFieldIndices = indices.filter(index => {
27 | const f = fields[index];
28 | const type = f.type;
29 | return type === 'int' || type === 'float';
30 | });
31 |
32 | const fieldMap = {};
33 | fieldNames.forEach(name => {
34 | if (!numericFieldIndices.length) {
35 | numericFieldIndices = indices;
36 | }
37 | const count = numericFieldIndices.length;
38 | const i = Math.floor(Math.random() * count) % count;
39 | const index = numericFieldIndices[i];
40 | numericFieldIndices.splice(i, 1);
41 | fieldMap[name] = index;
42 | });
43 |
44 | let stringFieldIndices = indices.filter(index => {
45 | const f = fields[index];
46 | return f.type === 'string';
47 | });
48 | if (!stringFieldIndices.length) {
49 | stringFieldIndices = indices;
50 | }
51 | if (!indices.length) {
52 | stringFieldIndices = Object.keys(fields).map(parseFloat);
53 | }
54 | const count = stringFieldIndices.length;
55 | const i = Math.floor(Math.random() * count) % count;
56 | const index = numericFieldIndices[i];
57 | numericFieldIndices.splice(i, 1);
58 | fieldMap.group = index;
59 |
60 | return fieldMap;
61 | },
62 | draw,
63 | properties: [
64 | {
65 | key: 'group',
66 | name: 'Group'
67 | },
68 | {
69 | key: 'time',
70 | name: 'Time'
71 | },
72 | {
73 | key: 'radius',
74 | name: 'Radius'
75 | },
76 | {
77 | key: 'angle',
78 | name: 'Angle'
79 | },
80 | {
81 | key: 'order',
82 | name: 'Depth Order'
83 | },
84 | {
85 | key: 'color',
86 | name: 'Color'
87 | },
88 | {
89 | key: 'label',
90 | name: 'Label'
91 | }
92 | ],
93 | geneCategories: [
94 | {
95 | category: 'Color',
96 | genes: [
97 | ['Hue Range', 'hueRange'],
98 | ['Hue Offset', 'hueOffset'],
99 | ['Saturation Offset', 'saturationOffset'],
100 | ['Saturation Factor', 'saturationValueFactor'],
101 | ['Lightness Offset', 'lightnessOffset'],
102 | ['Lightness Factor', 'lightnessValueFactor']
103 | ]
104 | },
105 | {
106 | category: 'X Position',
107 | genes: [
108 | ['Offset', 'xOffset'],
109 | ['Rank Factor', 'xRankFactor'],
110 | ['Random Factor', 'xRandomFactor']
111 | ]
112 | },
113 | {
114 | category: 'Y Position',
115 | genes: [
116 | ['Offset', 'yOffset'],
117 | ['Rank Factor', 'yRankFactor'],
118 | ['Random Factor', 'yRandomFactor']
119 | ]
120 | },
121 | {
122 | category: 'Scale',
123 | genes: [
124 | ['Offset', 'scaleOffset'],
125 | ['Rank Factor', 'scaleRankFactor']
126 | ]
127 | },
128 | {
129 | category: 'Rotation',
130 | genes: [
131 | ['Offset', 'rotationOffset'],
132 | ['Rank Factor', 'rotationRankFactor'],
133 | // ['Value Factor', 'rotationValueFactor'],
134 | ['Random Factor', 'rotationRandomFactor']
135 | ]
136 | }
137 | ],
138 | genes: {
139 | xOffset: 0,
140 | xRankFactor: 0,
141 | xRandomFactor: 0,
142 |
143 | yOffset: 0,
144 | yRankFactor: 0,
145 | yRandomFactor: 0,
146 |
147 | rotationOffset: 0,
148 | rotationRankFactor: 0,
149 | rotationValueFactor: 0,
150 | rotationRandomFactor: 0,
151 |
152 | scaleOffset: 0.9,
153 | scaleRankFactor: 0,
154 |
155 | ...defaultColorValues
156 | }
157 | };
--------------------------------------------------------------------------------
/src/components/DataTable.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /*
4 | Material UI components
5 | */
6 | import PropTypes from 'prop-types';
7 | import withStyles from '@material-ui/core/styles/withStyles';
8 | import Table from '@material-ui/core/Table';
9 | import TableBody from '@material-ui/core/TableBody';
10 | import TableCell from '@material-ui/core/TableCell';
11 | import TableHead from '@material-ui/core/TableHead';
12 | import TableRow from '@material-ui/core/TableRow';
13 | import TableFooter from '@material-ui/core/TableFooter';
14 | import TablePagination from '@material-ui/core/TablePagination';
15 |
16 | const styles = () => ({
17 | // todo: figure out scrolling, make rows thinner
18 | tableBody: {
19 | maxHeight: 200,
20 | overflow: 'auto'
21 | },
22 | tableCell: {
23 | whiteSpace: 'nowrap'
24 | }
25 |
26 | });
27 |
28 | const valueFormats = {
29 | int: v => v,
30 | float: f => f,
31 |
32 | datetime: v => {
33 | const d = new Date(v);
34 | return d.toLocaleDateString() + ' ' + d.toLocaleTimeString();
35 | },
36 | boolean: v => v ? 'true' : 'false',
37 | string: v => v
38 | };
39 |
40 | class DataTable extends React.Component {
41 | state = {
42 | page: 0,
43 | rowsPerPage: 10
44 | }
45 |
46 | static propTypes = {
47 | classes: PropTypes.object.isRequired,
48 | data: PropTypes.object
49 | }
50 |
51 | handleChangePage = (event, page) => {
52 | this.setState({ page });
53 | }
54 |
55 | handleChangeRowsPerPage = event => {
56 | this.setState({ rowsPerPage: event.target.value });
57 | }
58 |
59 | render() {
60 | const { classes, data } = this.props;
61 |
62 | const dataFields = data && data.get && data.get('fields');
63 | if (dataFields && dataFields.forEach) {
64 | /*
65 | Checking for Alberto's bug
66 | */
67 | dataFields.forEach(field => {
68 | if (!field || !field.get) {
69 | console.warn('Missing data field', field, data);
70 | }
71 | });
72 | }
73 |
74 |
75 | const rows = data && data.get && data.get('rows');
76 | const fields = data && data.get && data.get('fields')
77 | .filter(field => field && field.get);
78 | const { page, rowsPerPage } = this.state;
79 | const rowCount = rows && rows.length;
80 | const pageRows = rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
81 | const emptyRows = rowsPerPage - Math.min(rowsPerPage, rowCount - page * rowsPerPage);
82 |
83 | return
84 |
85 |
86 | {/*numeric={numFieldTypes.indexOf(fields[j].type) >= 0}*/}
87 | {fields.map((field, j) => {field.get('name')} )}
92 |
93 |
94 |
95 | {Object.keys(pageRows).map(i => {
96 | const row = pageRows[i];
97 | return (
98 |
99 | {/*numeric={numFieldTypes.indexOf(fields[j].type) >= 0}*/}
100 | {fields.map((field, j) => {valueFormats[field.get('type') || 'string'](row[j])} )}
105 |
106 | );
107 | })}
108 | {emptyRows > 0 &&
109 |
110 |
111 |
112 | }
113 |
114 |
115 |
116 |
123 |
124 |
125 |
;
126 | }
127 | }
128 |
129 | const StyleDataTable = withStyles(styles)(DataTable);
130 | export default StyleDataTable;
--------------------------------------------------------------------------------
/src/components/Tip.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | import withStyles from '@material-ui/core/styles/withStyles';
5 | import PropTypes from 'prop-types';
6 | import Typography from '@material-ui/core/Typography';
7 | import ArrowDropUp from '@material-ui/icons/ArrowDropUp';
8 | import HelpIcon from '@material-ui/icons/Help';
9 | import morphLogo from '../images/morph-logo-text.svg';
10 | // import SvgIcon from '@material'
11 |
12 | const styles = theme => ({
13 | root: {
14 | position: 'relative'
15 | },
16 | tip: {
17 | marginTop: theme.spacing.unit * 4,
18 | marginBottom: theme.spacing.unit * 4,
19 | marginLeft: theme.spacing.unit * 2,
20 | marginRight: theme.spacing.unit * 2,
21 | fontSize: '1.1em',
22 | fontWeight: 'normal',
23 |
24 | '& > p': {
25 | marginBottom: 0,
26 | marginTop: '0.5em'
27 | },
28 |
29 | '& > p:first-child': {
30 | marginTop: 0,
31 | fontSize: '1.25em'
32 | }
33 | },
34 | canCompact: {
35 | marginBottom: theme.spacing.unit * 2,
36 | borderBottom: `${theme.palette.divider} solid 1px`,
37 | '& > a > $logo': {
38 | display: 'none'
39 | }
40 | },
41 | icon: {
42 | position: 'absolute',
43 | bottom: 0,
44 | right: 6,
45 | width: 24,
46 | height: 36,
47 | fill: 'white'
48 | },
49 | logo: {
50 | position: 'absolute',
51 | top: 0,
52 | left: 0,
53 | height: '100%',
54 | boxSizing: 'border-box',
55 | padding: theme.spacing.unit,
56 | backgroundColor: theme.palette.background.default
57 | },
58 | compact: {
59 | '& > $tip': {
60 | margin: theme.spacing.unit,
61 | padding: `0 40px`
62 | }
63 | }
64 | });
65 |
66 | // global for now
67 | let expanded = false;
68 | const compactMediaQuery = window.matchMedia('(max-width: 620px), (max-height: 450px)');
69 |
70 | const Def = class Tip extends React.Component {
71 | state = {
72 | expanded,
73 | isCompact: compactMediaQuery.matches
74 | }
75 |
76 | static propTypes = {
77 | classes: PropTypes.object.isRequired,
78 | className: PropTypes.string,
79 | theme: PropTypes.object.isRequired,
80 | children: PropTypes.oneOfType([
81 | PropTypes.arrayOf(PropTypes.node),
82 | PropTypes.node
83 | ]),
84 | compact: PropTypes.string,
85 | color: PropTypes.string
86 | }
87 | toggle = () => {
88 | expanded = !expanded;
89 | this.setState({
90 | expanded
91 | });
92 | }
93 |
94 | setCompact = () => {
95 | this.setState({
96 | isCompact: compactMediaQuery.matches
97 | });
98 | }
99 |
100 | componentDidMount() {
101 | compactMediaQuery.addListener(this.setCompact);
102 | }
103 |
104 | componentWillUnmount() {
105 | compactMediaQuery.removeListener(this.setCompact);
106 | }
107 |
108 | render() {
109 | const {classes, children, compact, color, theme, ...otherProps} = this.props;
110 | const { isCompact } = this.state;
111 | const canCompact = compact && isCompact;
112 | const content = canCompact && !expanded && compact || children;
113 | const Arrow = expanded ? ArrowDropUp : HelpIcon;
114 |
115 | const backgroundColor = color || 'transparent';
116 | const textColor = backgroundColor !== 'transparent' ?
117 | theme.palette.getContrastText(color) :
118 | theme.palette.text.primary;
119 |
120 | return
131 |
138 | {content}
139 |
140 | { canCompact ?
: null }
141 |
142 | ;
143 | }
144 | };
145 |
146 | const Tip = withStyles(styles, { withTheme: true })(Def);
147 | export default Tip;
--------------------------------------------------------------------------------
/src/charts/PanZoom.js:
--------------------------------------------------------------------------------
1 | import {
2 | ZOOM_SPEED,
3 | MOVE_THRESHOLD
4 | } from './constants';
5 |
6 | export default function PanZoom(element, onPan, onZoom) {
7 | function wheel(event) {
8 | const delta = event.deltaY === undefined && event.detail !== undefined ? -event.detail : -event.deltaY || 0;
9 | const wheelScale = event.deltaMode === 1 ? 100 : 1;
10 |
11 | event.preventDefault();
12 |
13 | onZoom(Math.pow(1.0001, delta * wheelScale * ZOOM_SPEED));
14 | }
15 |
16 | let dragging = false;
17 | let moved = false;
18 | let zoomed = false;
19 | let pinchStartDist = 0;
20 | let dragStartX = 0;
21 | let dragStartY = 0;
22 | let dragX = 0;
23 | let dragY = 0;
24 |
25 | function start(x, y) {
26 | dragging = true;
27 | dragX = dragStartX = x;
28 | dragY = dragStartY = y;
29 | }
30 |
31 | function move(x, y) {
32 | if (dragging) {
33 |
34 | if (Math.abs(dragStartX - x) <= MOVE_THRESHOLD &&
35 | Math.abs(dragStartY - y) <= MOVE_THRESHOLD) {
36 | return;
37 | }
38 |
39 | moved = true;
40 |
41 | const dX = x - dragX;
42 | const dY = y - dragY;
43 | dragX = x;
44 | dragY = y;
45 |
46 | onPan(dX, dY);
47 | }
48 | }
49 |
50 | function mouseDown(event) {
51 | const target = event.target;
52 | let el = element;
53 | while (el !== target && el.parentNode) {
54 | el = el.parentNode;
55 | }
56 | if (el === target) {
57 | start(event.pageX, event.pageY);
58 | event.preventDefault();
59 | }
60 | }
61 |
62 | function stop() {
63 | dragging = false;
64 | moved = false;
65 | zoomed = false;
66 | }
67 |
68 | function mouseMove(event) {
69 | const x = event.pageX;
70 | const y = event.pageY;
71 | move(x, y);
72 | }
73 |
74 | function touchStart(event) {
75 | if (event.touches.length === 1) {
76 | start(event.touches[0].pageX, event.touches[0].pageY);
77 | } else if (event.touches.length === 2) {
78 | const t0 = event.touches[0];
79 | const t1 = event.touches[1];
80 | const dx = t0.pageX - t1.pageX;
81 | const dy = t0.pageY - t1.pageY;
82 | pinchStartDist = Math.sqrt(dx * dx + dy * dy);
83 | }
84 | }
85 |
86 | function touchMove(event) {
87 | event.preventDefault();
88 |
89 | if (event.touches.length === 2) {
90 | const t0 = event.touches[0];
91 | const t1 = event.touches[1];
92 | const dx = t0.pageX - t1.pageX;
93 | const dy = t0.pageY - t1.pageY;
94 | const pinchDist = Math.sqrt(dx * dx + dy * dy);
95 | const pinchDelta = pinchDist - pinchStartDist;
96 | const factor = pinchDelta > 0 ? 1.01 : 0.99;
97 |
98 | zoomed = true;
99 |
100 | onZoom(factor);
101 |
102 | event.preventDefault();
103 | event.stopPropagation();
104 |
105 | return;
106 | }
107 |
108 | if (!zoomed) {
109 | //todo: invert x direction on iOS
110 | move(event.touches[0].pageX, event.touches[0].pageY);
111 | }
112 | }
113 |
114 | function touchEnd(event) {
115 | if (!event.touches.length || event.touches.length < 2 && zoomed) {
116 | stop();
117 | }
118 | }
119 |
120 | this.start = () => {
121 | // todo: handle touch/pinch as well
122 | window.addEventListener('wheel', wheel);
123 | element.addEventListener('mousedown', mouseDown);
124 | window.addEventListener('mouseup', stop);
125 | window.addEventListener('mousemove', mouseMove);
126 |
127 | element.addEventListener('touchstart', touchStart);
128 | element.addEventListener('touchmove', touchMove);
129 | element.addEventListener('touchend', touchEnd);
130 | };
131 |
132 | this.stop = () => {
133 | stop();
134 |
135 | // todo: handle touch/pinch as well
136 | window.removeEventListener('wheel', wheel);
137 | element.removeEventListener('mousedown', mouseDown);
138 | window.removeEventListener('mouseup', stop);
139 | window.removeEventListener('mousemove', mouseMove);
140 |
141 | element.removeEventListener('touchstart', touchStart);
142 | element.removeEventListener('touchmove', touchMove);
143 | element.removeEventListener('touchend', touchEnd);
144 | };
145 |
146 | Object.defineProperties(this, {
147 | dragging: {
148 | get: () => dragging
149 | },
150 | moved: {
151 | get: () => moved
152 | },
153 | changed: {
154 | get: () => moved || zoomed
155 | }
156 | });
157 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "morph",
3 | "version": "0.0.1",
4 | "description": "Morph is an open-source tool for creating designs, animations or interactive visualizations from data.",
5 | "homepage": "https://morph.graphics",
6 | "author": {
7 | "name": "Brian Chirls",
8 | "company": "Datavized Technologies",
9 | "url": "https://github.com/brianchirls"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/datavized/morph.git"
14 | },
15 | "bugs": {
16 | "url": "https://github.com/datavized/morph/issues"
17 | },
18 | "scripts": {
19 | "lint": "eslint ./; true",
20 | "lint-fix": "eslint --fix ./; true",
21 | "start": "NODE_ENV=development webpack-dev-server",
22 | "dev": "NODE_ENV=development webpack",
23 | "build": "NODE_ENV=production webpack",
24 | "test": "echo \"Error: no test specified\" && exit 1"
25 | },
26 | "license": "MPL-2.0",
27 | "devDependencies": {
28 | "@babel/core": "^7.1.0",
29 | "@babel/plugin-proposal-class-properties": "^7.1.0",
30 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
31 | "@babel/plugin-syntax-dynamic-import": "^7.0.0",
32 | "@babel/plugin-transform-react-jsx": "^7.0.0",
33 | "@babel/plugin-transform-react-jsx-self": "^7.0.0",
34 | "@babel/plugin-transform-react-jsx-source": "^7.0.0",
35 | "@babel/plugin-transform-runtime": "^7.1.0",
36 | "@babel/polyfill": "^7.0.0",
37 | "@babel/preset-env": "^7.1.0",
38 | "@babel/preset-react": "^7.0.0",
39 | "autoprefixer": "^9.1.5",
40 | "babel-eslint": "^9.0.0",
41 | "babel-loader": "^8.0.2",
42 | "babel-plugin-transform-react-remove-prop-types": "^0.4.18",
43 | "case-sensitive-paths-webpack-plugin": "^2.1.2",
44 | "clean-webpack-plugin": "^0.1.19",
45 | "copy-webpack-plugin": "^4.5.2",
46 | "css-loader": "^1.0.0",
47 | "datavized-code-style": "github:datavized/code-style",
48 | "dotenv": "^6.0.0",
49 | "eslint": "^5.6.0",
50 | "eslint-config-crockford": "^2.0.0",
51 | "eslint-loader": "^2.1.1",
52 | "eslint-plugin-import": "^2.14.0",
53 | "eslint-plugin-jsx-a11y": "^6.1.1",
54 | "eslint-plugin-react": "^7.11.1",
55 | "exports-loader": "^0.7.0",
56 | "fast-async": "^7.0.6",
57 | "favicons-webpack-plugin": "0.0.9",
58 | "file-loader": "^2.0.0",
59 | "html-loader": "^0.5.5",
60 | "html-webpack-plugin": "^3.2.0",
61 | "imagemin-mozjpeg": "^7.0.0",
62 | "imagemin-webpack-plugin": "^2.3.0",
63 | "postcss-flexbugs-fixes": "^4.1.0",
64 | "postcss-loader": "^3.0.0",
65 | "react-dev-utils": "^5.0.2",
66 | "react-hot-loader": "^4.3.11",
67 | "style-loader": "^0.23.0",
68 | "unused-files-webpack-plugin": "^3.4.0",
69 | "url-loader": "^1.1.1",
70 | "webpack": "^4.19.1",
71 | "webpack-build-notifier": "^0.1.29",
72 | "webpack-bundle-analyzer": "^3.0.2",
73 | "webpack-cli": "^3.1.1",
74 | "webpack-dev-server": "^3.1.8",
75 | "webpack-glsl-loader": "^1.0.1",
76 | "webpack-merge": "^4.1.4",
77 | "webpack-sources": "^1.3.0",
78 | "workbox-webpack-plugin": "^3.6.1"
79 | },
80 | "dependencies": {
81 | "@material-ui/core": "^3.1.0",
82 | "@material-ui/icons": "^3.0.1",
83 | "@pixi/app": "5.0.0-alpha.2",
84 | "@pixi/constants": "5.0.0-alpha.2",
85 | "@pixi/core": "5.0.0-alpha.2",
86 | "@pixi/display": "5.0.0-alpha.2",
87 | "@pixi/graphics": "5.0.0-alpha.2",
88 | "@pixi/math": "5.0.0-alpha.2",
89 | "@pixi/mesh": "5.0.0-alpha.2",
90 | "@pixi/sprite": "5.0.0-alpha.2",
91 | "@pixi/text": "5.0.0-alpha.2",
92 | "abs-svg-path": "^0.1.1",
93 | "adaptive-bezier-curve": "^1.0.3",
94 | "cdt2d": "^1.0.0",
95 | "classnames": "^2.2.6",
96 | "clean-pslg": "^1.1.2",
97 | "clipboard-copy": "^2.0.1",
98 | "error-stack-parser": "^2.0.2",
99 | "event-emitter": "^0.3.5",
100 | "file-saver": "^1.3.8",
101 | "gif.js": "^0.2.0",
102 | "gif.js.optimized": "^1.0.1",
103 | "idb-keyval": "^3.1.0",
104 | "immutable": "^3.8.2",
105 | "ismobilejs": "^0.4.1",
106 | "jszip": "^3.1.5",
107 | "object-assign": "^4.1.1",
108 | "parse-svg-path": "^0.1.2",
109 | "prop-types": "^15.6.2",
110 | "quadtree-js": "github:timohausmann/quadtree-js#hitman",
111 | "quick-gif.js": "0.0.1",
112 | "rc-slider": "^8.6.3",
113 | "react": "^16.5.2",
114 | "react-color": "^2.14.1",
115 | "react-confirm": "^0.1.18",
116 | "react-dom": "^16.5.2",
117 | "react-dropzone": "^5.1.0",
118 | "serialize-error": "^2.1.0",
119 | "vec2-copy": "^1.0.0",
120 | "webm-writer": "^0.2.1",
121 | "worker-loader": "^2.0.0",
122 | "xlsx": "^0.14.0"
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/components/UploadDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import withStyles from '@material-ui/core/styles/withStyles';
4 | import PropTypes from 'prop-types';
5 | import Dialog from '@material-ui/core/Dialog';
6 | import DialogActions from '@material-ui/core/DialogActions';
7 | import DialogContent from '@material-ui/core/DialogContent';
8 | import DialogTitle from '@material-ui/core/DialogTitle';
9 | import LinearProgress from '@material-ui/core/LinearProgress';
10 | import Button from '@material-ui/core/Button';
11 | import Select from '@material-ui/core/Select';
12 | import List from '@material-ui/core/List';
13 | import MenuItem from '@material-ui/core/MenuItem';
14 | import FormControl from '@material-ui/core/FormControl';
15 | import Typography from '@material-ui/core/Typography';
16 |
17 | import OverwriteWarning from './OverwriteWarning';
18 | import ListEntry from './ListEntry';
19 |
20 | const styles = theme => ({
21 | dialog: {
22 | minWidth: '35%',
23 | maxWidth: '90%'
24 | },
25 | uploadInstructions: {
26 | cursor: 'pointer',
27 | color: theme.palette.text.primary
28 | }
29 | });
30 |
31 | const Def = ({
32 | classes,
33 | open,
34 | onClose,
35 | onClick,
36 | overwriteWarning,
37 | fileError,
38 | waiting,
39 | worksheet,
40 | worksheets,
41 | cancelUpload,
42 | handleChangeWorksheet,
43 | onSubmitWorksheetSelection
44 | }) => {
45 | // todo: display any errors
46 |
47 | let content = null;
48 |
49 | if (waiting) {
50 | content =
51 | Uploading...
52 |
53 | ;
54 | } else if (worksheets) {
55 | content =
56 | Select a worksheet
57 |
58 | {/*{state.file.name} */}
59 |
60 |
65 | {worksheets.map((name, i) => {name} )}
66 |
67 |
68 |
69 | Cancel
70 | Select
71 |
72 |
73 | ;
74 | } else {
75 | content =
76 | Upload File
77 |
78 |
82 | {/**/}
83 |
84 | Drop file here or click to select.
85 | File types supported: .xls, .xlsx, .csv, .ods
86 | Maximum file size: 2MB
87 | Up to 300 rows of data
88 |
89 |
90 | { overwriteWarning ? : null }
91 | {fileError && {fileError} }
92 |
93 | Cancel
94 |
95 |
96 | ;
97 | }
98 |
99 | return
113 | {content}
114 | ;
115 | };
116 |
117 | Def.propTypes = {
118 | classes: PropTypes.object.isRequired,
119 | className: PropTypes.string,
120 | children: PropTypes.oneOfType([
121 | PropTypes.arrayOf(PropTypes.node),
122 | PropTypes.node
123 | ]),
124 | open: PropTypes.bool,
125 | onClose: PropTypes.func.isRequired,
126 | onClick: PropTypes.func.isRequired,
127 | cancelUpload: PropTypes.func.isRequired,
128 | handleChangeWorksheet: PropTypes.func.isRequired,
129 | onSubmitWorksheetSelection: PropTypes.func.isRequired,
130 | overwriteWarning: PropTypes.bool,
131 | fileError: PropTypes.string,
132 | waiting: PropTypes.bool,
133 | worksheet: PropTypes.string,
134 | worksheets: PropTypes.arrayOf(PropTypes.object)
135 | };
136 |
137 | const UploadDialog = withStyles(styles)(Def);
138 | export default UploadDialog;
--------------------------------------------------------------------------------
/src/components/SocialShareDialog.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import copy from 'clipboard-copy';
3 |
4 | import withStyles from '@material-ui/core/styles/withStyles';
5 | import PropTypes from 'prop-types';
6 | import Dialog from '@material-ui/core/Dialog';
7 | import DialogActions from '@material-ui/core/DialogActions';
8 | import DialogContent from '@material-ui/core/DialogContent';
9 | import List from '@material-ui/core/List';
10 | import DialogTitle from '@material-ui/core/DialogTitle';
11 | import Button from '@material-ui/core/Button';
12 | import ListEntry from './ListEntry';
13 |
14 | // import ImageIcon from '@material-ui/icons/Image';
15 | import ShareIcon from '@material-ui/icons/Share';
16 | import MailIcon from '@material-ui/icons/Mail';
17 | import TwitterIcon from './icons/Twitter';
18 | import FacebookIcon from './icons/Facebook';
19 | import {
20 | shareFacebook,
21 | shareTwitter,
22 | shareNative,
23 | shareEmail
24 | } from '../util/share';
25 | import { SHARE_HASHTAGS } from '../constants';
26 |
27 |
28 | const styles = theme => ({
29 | dialog: {
30 | minWidth: '30%',
31 | maxHeight: '60%',
32 | width: 'min-content',
33 | maxWidth: '90%'
34 | },
35 | dialogContent: {
36 | display: 'flex',
37 | flexDirection: 'column',
38 | color: theme.palette.text.primary
39 | },
40 | dialogListContainer: {
41 | display: 'block',
42 | overflow: 'auto',
43 | '& a': {
44 | color: 'inherit',
45 | textDecoration: 'none'
46 | },
47 | '& svg': {
48 | marginRight: theme.spacing.unit
49 | }
50 | },
51 | listEntry: {
52 | cursor: 'pointer'
53 | },
54 | copy: {
55 | display: 'flex',
56 | justifyItems: 'stretch',
57 | '& > input': {
58 | flex: 1,
59 | border: 'none',
60 | fontSize: 14,
61 | minWidth: 40,
62 | background: 'transparent',
63 | color: theme.palette.text.primary
64 | },
65 | marginTop: theme.spacing.unit,
66 | padding: `0 ${theme.spacing.unit}px`,
67 | border: `${theme.palette.divider} solid 1px`,
68 | backgroundColor: theme.palette.background.default
69 | }
70 | });
71 |
72 | const selectInput = evt => evt.target.select();
73 |
74 | const Def = class SocialShareDialog extends React.Component {
75 | static propTypes = {
76 | classes: PropTypes.object.isRequired,
77 | open: PropTypes.bool,
78 | onClose: PropTypes.func.isRequired,
79 | title: PropTypes.string.isRequired,
80 | text: PropTypes.string.isRequired,
81 | url: PropTypes.string.isRequired
82 | }
83 |
84 | static defaultProps = {
85 | title: '',
86 | text: ''
87 | }
88 |
89 | copyURL = () => copy(this.props.url)
90 |
91 | render() {
92 |
93 | const {
94 | classes,
95 | open,
96 | onClose,
97 | url,
98 | title,
99 | text,
100 | ...otherProps
101 | } = this.props;
102 | const shareActions = navigator.share ?
103 | [ shareNative(title, text, url, SHARE_HASHTAGS)} className={classes.listEntry}> Share ] :
104 | [
105 | shareTwitter(title, text, url, SHARE_HASHTAGS)} className={classes.listEntry}> Twitter ,
106 | shareFacebook(url)} className={classes.listEntry}> Facebook
107 | ];
108 | // shareActions.push(
109 | //
110 | // email
111 | //
112 | // );
113 | shareActions.push( shareEmail(title, text, url)} className={classes.listEntry}>
114 | Email
115 | );
116 |
117 | return
128 | Share
129 |
130 |
131 | {/**/}
132 |
133 | {shareActions}
134 |
135 |
136 |
137 |
138 | Copy
139 |
140 |
141 | Close
142 |
143 |
144 | ;
145 | }
146 | };
147 |
148 | const SocialShareDialog = withStyles(styles)(Def);
149 | export default SocialShareDialog;
--------------------------------------------------------------------------------
/src/drawing/SpritePool.js:
--------------------------------------------------------------------------------
1 | import { RenderTexture as RenderTexture } from '@pixi/core';
2 | import { SCALE_MODES } from '@pixi/constants';
3 |
4 | import { Renderer } from '@pixi/core';
5 | import { Sprite as PIXISprite, SpriteRenderer } from '@pixi/sprite';
6 | Renderer.registerPlugin('sprite', SpriteRenderer);
7 |
8 | import { nextLog2 } from '../util/nextPowerOfTwo';
9 |
10 | function Sprite(spriteLevel, index) {
11 | const rt = RenderTexture.create(spriteLevel.size, spriteLevel.size, SCALE_MODES.LINEAR, 1);
12 | rt.baseTexture.mipmap = true;
13 |
14 | this.pixiSprite = new PIXISprite(rt);
15 | this.rt = rt;
16 | this.client = null;
17 | this.spriteLevel = spriteLevel;
18 | this.level = spriteLevel.level;
19 | this.index = index;
20 |
21 | const scale = 1 / spriteLevel.size;
22 | this.pixiSprite.scale.set(scale, scale);
23 |
24 | this.destroy = () => {
25 | rt.destroy();
26 | this.pixiSprite.destroy();
27 | };
28 | }
29 |
30 | function SpriteLevel(level, maxTextureSize) {
31 | /* eslint-disable no-bitwise */
32 | const size = 1 << level;
33 | /* eslint-enable no-bitwise */
34 | const d = maxTextureSize / size;
35 | const numSprites = d * d;
36 | const available = new Set(); // sprite.index
37 | const availableQueue = [];
38 | const sprites = [];
39 | const claims = new Map(); // index on client
40 |
41 | this.level = level;
42 | this.size = size;
43 | this.dimension = d;
44 |
45 | const getSprite = () =>{
46 | let index = -1;
47 | if (sprites.length < numSprites) {
48 | index = sprites.length;
49 | } else if (availableQueue.length) {
50 | index = availableQueue.shift();
51 | available.delete(index);
52 | } else {
53 | return null;
54 | }
55 |
56 | if (!sprites[index]) {
57 | sprites[index] = new Sprite(this, index);
58 | }
59 | return sprites[index];
60 | };
61 |
62 | this.release = client => {
63 | const claim = claims.get(client);
64 | if (claim) {
65 | const sprite = claim.sprite;
66 | if (claim.exclusive && sprite && !available.has(sprite.index)) {
67 | available.add(sprite.index);
68 | availableQueue.push(sprite.index);
69 | }
70 | claim.exclusive = false;
71 | }
72 | };
73 |
74 | this.requestSprite = client => {
75 | let claim = claims.get(client);
76 | if (!claim) {
77 | claim = {
78 | exclusive: false,
79 | sprite: null
80 | };
81 | claims.set(client, claim);
82 | }
83 |
84 | const sprite = claim.sprite || getSprite();
85 | if (!sprite) {
86 | return null;
87 | }
88 |
89 | // re-using an existing sprite and making it exclusive
90 | // so make sure it's not available anymore
91 | if (sprite === claim.sprite && available.has(sprite.index)) {
92 | const i = availableQueue.indexOf(sprite.index);
93 | availableQueue.splice(i, 1);
94 | available.delete(sprite.index);
95 | }
96 |
97 | if (sprite.client && sprite.client !== client) {
98 | const otherClaim = claims.get(sprite.client);
99 | if (otherClaim) {
100 | otherClaim.sprite = null;
101 | otherClaim.exclusive = false;
102 | }
103 | }
104 | claim.sprite = sprite;
105 | claim.exclusive = true;
106 |
107 | return sprite;
108 | };
109 |
110 | this.destroy = () => {
111 | sprites.forEach(s => s.destroy());
112 | sprites.length = 0;
113 |
114 | claims.clear();
115 |
116 | available.clear();
117 | availableQueue.length = 0;
118 |
119 | // brt.destroy();
120 | };
121 | }
122 |
123 | function SpritePool({
124 | max = 1024,
125 | min = 64,
126 | maxTextureSize = 4096
127 | } = {}) {
128 |
129 | const minLevel = nextLog2(min);
130 | const maxLevel = nextLog2(Math.min(max, maxTextureSize));
131 |
132 | const levels = new Map();
133 | const clients = new Set();
134 |
135 | function getSpriteLevel(level) {
136 | if (levels.has(level)) {
137 | return levels.get(level);
138 | }
139 |
140 | const spriteLevel = new SpriteLevel(level, maxTextureSize);
141 | levels.set(level, spriteLevel);
142 | return spriteLevel;
143 | }
144 |
145 | this.get = name => {
146 | let visible = false;
147 | const client = {
148 | name,
149 | release() {
150 | if (visible) {
151 | levels.forEach(level => level.release(client));
152 | visible = false;
153 | }
154 | },
155 | render(minResolution, callback, forceRender) {
156 | const level = Math.max(minLevel, Math.min(maxLevel, nextLog2(minResolution)));
157 | let sprite = null;
158 | for (let l = maxLevel; l >= level && !sprite; l--) {
159 | sprite = getSpriteLevel(l).requestSprite(client);
160 | }
161 |
162 | visible = true;
163 |
164 | if (!sprite) {
165 | return null;
166 | }
167 |
168 | if (forceRender || sprite.client !== client) {
169 | sprite.client = client;
170 | callback(sprite.rt);
171 | }
172 |
173 | return sprite.pixiSprite;
174 | },
175 | destroy() {
176 | client.release();
177 | clients.delete(client);
178 | }
179 | };
180 | clients.add(client);
181 | return client;
182 | };
183 |
184 | this.destroy = () => {
185 | clients.forEach((claims, client) => client.destroy());
186 | levels.forEach(level => level.destroy());
187 | };
188 | }
189 |
190 | export default SpritePool;
--------------------------------------------------------------------------------
/src/util/normalize.js:
--------------------------------------------------------------------------------
1 | /*
2 | Adapted from https://github.com/jkroso/normalize-svg-path
3 | MIT License
4 | - Code style change for readability
5 | - Don't convert lines to curves, since they waste points
6 | */
7 |
8 | const π = Math.PI;
9 | const c120 = radians(120);
10 |
11 | function line(x1, y1, x2, y2) {
12 | // return ['C', x1, y1, x2, y2, x2, y2];
13 | return ['L', x2, y2];
14 | }
15 |
16 | function radians(degress) {
17 | return degress * (π / 180);
18 | }
19 |
20 | function rotate(x, y, rad) {
21 | return {
22 | x: x * Math.cos(rad) - y * Math.sin(rad),
23 | y: x * Math.sin(rad) + y * Math.cos(rad)
24 | };
25 | }
26 |
27 | /* eslint-disable max-params */
28 | function quadratic(x1, y1, cx, cy, x2, y2) {
29 | return [
30 | 'C',
31 | x1 / 3 + 2 / 3 * cx,
32 | y1 / 3 + 2 / 3 * cy,
33 | x2 / 3 + 2 / 3 * cx,
34 | y2 / 3 + 2 / 3 * cy,
35 | x2,
36 | y2
37 | ];
38 | }
39 |
40 | // This function is ripped from
41 | // github.com/DmitryBaranovskiy/raphael/blob/4d97d4/raphael.js#L2216-L2304
42 | // which references w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
43 | // TODO: make it human readable
44 |
45 | function arc(x1, y1, rx, ry, angle, largeArcFlag, sweepFlag, x2, y2, recursive) {
46 | let f1 = 0;
47 | let f2 = 0;
48 | let cx = 0;
49 | let cy = 0;
50 |
51 | if (!recursive) {
52 | let xy = rotate(x1, y1, -angle);
53 | x1 = xy.x;
54 | y1 = xy.y;
55 |
56 | xy = rotate(x2, y2, -angle);
57 | x2 = xy.x;
58 | y2 = xy.y;
59 |
60 | const x = (x1 - x2) / 2;
61 | const y = (y1 - y2) / 2;
62 | let h = x * x / (rx * rx) + y * y / (ry * ry);
63 | if (h > 1) {
64 | h = Math.sqrt(h);
65 | rx = h * rx;
66 | ry = h * ry;
67 | }
68 |
69 | const rx2 = rx * rx;
70 | const ry2 = ry * ry;
71 | let k = (largeArcFlag === sweepFlag ? -1 : 1) *
72 | Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)));
73 | if (k === Infinity) {
74 | // neutralize
75 | k = 1;
76 | }
77 | cx = k * rx * y / ry + (x1 + x2) / 2;
78 | cy = k * -ry * x / rx + (y1 + y2) / 2;
79 | f1 = Math.asin(((y1 - cy) / ry).toFixed(9));
80 | f2 = Math.asin(((y2 - cy) / ry).toFixed(9));
81 |
82 | f1 = x1 < cx ? π - f1 : f1;
83 | f2 = x2 < cx ? π - f2 : f2;
84 |
85 | if (f1 < 0) {
86 | f1 = π * 2 + f1;
87 | }
88 | if (f2 < 0) {
89 | f2 = π * 2 + f2;
90 | }
91 | if (sweepFlag && f1 > f2) {
92 | f1 = f1 - π * 2;
93 | }
94 | if (!sweepFlag && f2 > f1) {
95 | f2 = f2 - π * 2;
96 | }
97 | } else {
98 | [f1, f2, cx, cy] = recursive;
99 | }
100 |
101 | // greater than 120 degrees requires multiple segments
102 | let res = 0;
103 | if (Math.abs(f2 - f1) > c120) {
104 | const f2old = f2;
105 | const x2old = x2;
106 | const y2old = y2;
107 | f2 = f1 + c120 * (sweepFlag && f2 > f1 ? 1 : -1);
108 | x2 = cx + rx * Math.cos(f2);
109 | y2 = cy + ry * Math.sin(f2);
110 | res = arc(x2, y2, rx, ry, angle, 0, sweepFlag, x2old, y2old, [f2, f2old, cx, cy]);
111 | }
112 | const t = Math.tan((f2 - f1) / 4);
113 | const hx = 4 / 3 * rx * t;
114 | const hy = 4 / 3 * ry * t;
115 | let curve = [
116 | 2 * x1 - (x1 + hx * Math.sin(f1)),
117 | 2 * y1 - (y1 - hy * Math.cos(f1)),
118 | x2 + hx * Math.sin(f2),
119 | y2 - hy * Math.cos(f2),
120 | x2,
121 | y2
122 | ];
123 | if (recursive) {
124 | return curve;
125 | }
126 | if (res) {
127 | curve = curve.concat(res);
128 | }
129 | for (let i = 0; i < curve.length;) {
130 | const rot = rotate(curve[i], curve[i + 1], angle);
131 | curve[i++] = rot.x;
132 | curve[i++] = rot.y;
133 | }
134 | return curve;
135 | }
136 | /* eslint-enable max-params */
137 |
138 | export default function normalize(path) {
139 | // init state;
140 | const result = [];
141 | let prev;
142 | let bezierX = 0;
143 | let bezierY = 0;
144 | let startX = 0;
145 | let startY = 0;
146 | let quadX = null;
147 | let quadY = null;
148 | let x = 0;
149 | let y = 0;
150 |
151 | path.forEach(seg => {
152 | const command = seg[0];
153 |
154 | if (command === 'M') {
155 | startX = seg[1];
156 | startY = seg[2];
157 | } else if (command === 'A') {
158 | seg = arc(x, y, seg[1], seg[2], radians(seg[3]), seg[4], seg[5], seg[6], seg[7]);
159 | // split multi part
160 | seg.unshift('C');
161 | if (seg.length > 7) {
162 | result.push(seg.splice(0, 7));
163 | seg.unshift('C');
164 | }
165 | } else if (command === 'S') {
166 | // default control point;
167 | let cx = x;
168 | let cy = y;
169 | if (prev === 'C' || prev === 'S') {
170 | cx += cx - bezierX; // reflect the previous command's control
171 | cy += cy - bezierY; // point relative to the current point
172 | }
173 | seg = ['C', cx, cy, seg[1], seg[2], seg[3], seg[4]];
174 | } else if (command === 'T') {
175 | if (prev === 'Q' || prev === 'T') {
176 | quadX = x * 2 - quadX; // as with 'S' reflect previous control point;
177 | quadY = y * 2 - quadY;
178 | } else {
179 | quadX = x;
180 | quadY = y;
181 | }
182 | seg = quadratic(x, y, quadX, quadY, seg[1], seg[2]);
183 | } else if (command === 'Q') {
184 | quadX = seg[1];
185 | quadY = seg[2];
186 | seg = quadratic(x, y, seg[1], seg[2], seg[3], seg[4]);
187 | } else if (command === 'H') {
188 | seg = line(x, y, seg[1], y);
189 | } else if (command === 'V') {
190 | seg = line(x, y, x, seg[1]);
191 | } else if (command === 'Z') {
192 | seg = line(x, y, startX, startY);
193 | }
194 |
195 | // update state
196 | prev = command;
197 | x = seg[seg.length - 2];
198 | y = seg[seg.length - 1];
199 | if (seg.length > 4) {
200 | bezierX = seg[seg.length - 4];
201 | bezierY = seg[seg.length - 3];
202 | } else {
203 | bezierX = x;
204 | bezierY = y;
205 | }
206 | result.push(seg);
207 | });
208 |
209 | return result;
210 | }
211 |
--------------------------------------------------------------------------------
/src/images/morph-logo-text.svg:
--------------------------------------------------------------------------------
1 | Morph
--------------------------------------------------------------------------------
/src/images/morph-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
13 |
15 |
17 |
21 |
23 |
25 |
27 |
30 |
32 |
36 |
38 |
42 |
45 |
48 |
50 |
54 |
56 |
57 |
59 |
62 |
65 |
69 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Morph
4 |
5 | Morph is an open-source web tool for creating designs, animations or interactive visualizations from data. Morph has been developed by [Datavized Technologies](https://datavized.com) with support from [Google News Initiative](https://newsinitiative.withgoogle.com).
6 |
7 | ## How does Morph work?
8 |
9 | The tool involves five simple steps to create [generative art](https://en.wikipedia.org/wiki/Generative_art) from [tabular data](https://en.wikipedia.org/wiki/Table_(information)) (e.g. spreadsheets and comma-separated values).
10 | 1. Data: Upload your own data or select one of the sample spreadsheets (headers are required to detect fields, maximum file size is 2MB, up to 2,000 rows with 300 rows visible)
11 | 2. Review: Examine your data in the tabular preview
12 | 3. Design: Choose a chart type (Pie Chart, Bar Chart, Scatter Plot, Line Chart, Area Timeline, Radial Area) to prepare visualization
13 | 4. Organize: Choose different fields to build your chart or fill random
14 | 5. Evolve: Click your chart to evolve your tree, click again to generate new nodes or leaves, then select the Editor to modify and save your leaf. Export any visual, including your original, as a Still Image or Animation.
15 |
16 | The data uploaded to Morph is processed fully in the web browser: no server-side operations or storage is performed. It is also optimized for mobile and designed as a cross-platform Progressive Web App so the tool can be installed on any web connected device.
17 |
18 | ## Who uses Morph?
19 |
20 | Morph is built to be fast, fun and easy to use, but allows for output to industry-standard formats so that users can share their creations to social media or download them for use in professional design projects or presentations. The software uses a generative algorithm to create graphics based on data from a spreadsheet and the user designs by guiding the evolution of their artwork through simple taps or clicks. A progressive web app, it allows users to install the app to their device directly from the browser. Morph works on mobile, tablet and desktop, and aims to bring data and design capabilities to a wider audience. We welcome everyone who would like to contribute to improving the platform. There’s a lot of great tools available for serious data analysts and scientists. We wanted to make something creative for non-technical people who are often intimidated by data and design software. Morph works great in a classroom setting where beginners can make artworks in minutes, but also professional users like it for the randomness and speed it offers them for rapid-prototyping ideas.
21 |
22 | ## What is Morph’s goal?
23 |
24 | Morph exists to engage users in the creative expression of data without having to code. Generative art based algorithms turn data into a visual representation and the user can affect how their data interacts with the final visual via the algorithm. The algorithms themselves are not fixed; the user can randomly mutate, evolve and generate new algorithms creating new visuals, encouraging the sense of creative exploration and discovery. Through an intuitive UI to change parameters, the user can change the algorithms without any code. The tool focuses on random creation rather than preset templates. Where data visualization tools like RawGraphs and Flourish allow the user to turn spreadsheet data into charts and graphs, Morph enables the user to iterate on visual chart types through random mutation and generation of algorithms that can be continuously evolved by the user. The tool is also designed for creative expression, discovery and error handling. There are no restrictions on the types of variables that are assigned as is the case with traditional chart visualization tools.
25 |
26 | ## How can your organization benefit from using Morph?
27 |
28 | Organizations can benefit using Morph as a way to inspire a data-driven culture, use it as a data icebreaker, invite users from all departments and teams to play with Morph. Curate some of your organization’s data for users to get started and share what they make and celebrate it internally on Slack or digital dashboards, or share widely on social media and in your next presentation or event. Turn your annual report or customer data into generative art. This is a great tool for individuals and organizations without the resources to hire in-house developers or design teams. Data-driven art projects usually require a lot of money, people and time to produce but Morph now lets anyone create something great in minutes with free software, even if they only have a smartphone. The possibilities are endless. What will you make with Morph?
29 |
30 | - Web App: https://app.morph.graphics
31 | - Project official page: https://morph.graphics
32 | - Documentation: https://github.com/datavized/morph/
33 |
34 | ## Usage
35 |
36 | The easiest way to use Morph is by accessing the most updated version on the official app page at [morph.graphics](https://app.morph.graphics). However, Morph can also run locally on your machine: see the installation instructions below. Share your creations with the community [@FeedMorph on Twitter](https://twitter.com/FeedMorph).
37 |
38 | ## Developing
39 |
40 | You can run your own build of Morph. You can make changes to customize for your own purposes, and contributions are welcome if you make any improvements or bug fixes.
41 |
42 | ### Requirements
43 | - [git](https://git-scm.com/book/en/Getting-Started-Installing-Git)
44 | - [node.js/npm](https://www.npmjs.com/get-npm)
45 |
46 | ### Installation
47 |
48 | Clone the Morph git repository from the command line:
49 | ```sh
50 | git clone https://github.com/datavized/morph.git
51 | ```
52 |
53 | Navigate to the Morph repository directory
54 | ```sh
55 | cd morph
56 | ```
57 |
58 | Install dependencies
59 | ```sh
60 | npm install
61 | ```
62 | ### Build
63 |
64 | To run in development mode, which will run a local web server on port 9000 and automatically rebuild when any source code files are changed.
65 | ```sh
66 | npm run start
67 | ```
68 |
69 | To compile a production build
70 | ```sh
71 | npm run build
72 | ```
73 |
74 | ## Built With
75 | - [React](https://reactjs.org/)
76 | - [Material UI](https://material-ui.com/)
77 | - [PixiJS](http://www.pixijs.com/)
78 |
79 | ## Core Team
80 | Morph is maintained by [Datavized Technologies](https://datavized.com) with support from [Google News Initiative](https://newsinitiative.withgoogle.com) and key collaborator [Alberto Cairo](http://www.thefunctionalart.com/).
81 |
82 | If you want to know more about Morph, how it works and future developments, please visit the official website. For any specific request or comment we suggest you to use Github. You can also write to us at contact@datavized.com.
83 |
84 | ## Contributing
85 |
86 | We welcome and appreciate contributions, in the form of code pull requests, bug reports, feature requests or additions to our gallery. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our [Code of Conduct](CODE_OF_CONDUCT.md) and submission process. By participating, you are expected to uphold this code. Please report unacceptable behavior to support@datavized.com.
87 |
88 | ## License
89 |
90 | This software is licensed under the [MPL 2.0](LICENSE)
91 |
--------------------------------------------------------------------------------
/src/components/NodeInspector.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | import fieldMappedTable from '../util/fieldMappedTable';
5 |
6 | /*
7 | Material UI components
8 | */
9 | import PropTypes from 'prop-types';
10 | import withStyles from '@material-ui/core/styles/withStyles';
11 | import Paper from '@material-ui/core/Paper';
12 | import Typography from '@material-ui/core/Typography';
13 | import Button from '@material-ui/core/Button';
14 | import ExpandLessIcon from '@material-ui/icons/ExpandLess';
15 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
16 |
17 | import Tip from './Tip';
18 | import ChartPreview from './ChartPreview';
19 |
20 | import translucentBackgroundColor from '../util/translucentBackgroundColor';
21 |
22 | const compactMediaQuery = window.matchMedia('(max-width: 576px)');
23 |
24 | const styles = theme => ({
25 | root: {
26 | display: 'flex',
27 | flexDirection: 'column',
28 | justifyContent: 'center',
29 | flex: 1,
30 | minHeight: 0,
31 |
32 | '& > *': {
33 | margin: `0 10% ${theme.spacing.unit * 2}px`
34 | },
35 | '& > *:first-child': {
36 | margin: `0 0 ${theme.spacing.unit * 2}px`
37 | }
38 | },
39 | main: {
40 | display: 'flex',
41 | flexDirection: 'row',
42 | flex: 1,
43 | position: 'relative',
44 | minHeight: 0
45 | },
46 | preview: {
47 | backgroundColor: `${theme.palette.background.default} !important`,
48 | flex: 1,
49 | overflow: 'hidden'
50 | },
51 | controlsBox: {
52 | margin: `0px ${theme.spacing.unit * 4}px 0 0`,
53 | padding: `${theme.spacing.unit}px ${theme.spacing.unit * 2}px`,
54 | flex: '0 0 240px',
55 | display: 'flex',
56 | flexDirection: 'column',
57 | minHeight: 0
58 | },
59 | controlsHeader: {
60 | display: 'flex',
61 | flexDirection: 'row',
62 | justifyContent: 'space-between',
63 | '& + *': {
64 | marginTop: theme.spacing.unit * 2
65 | }
66 | },
67 | controlsButton: {
68 | padding: `0px ${theme.spacing.unit}px`,
69 | height: 24,
70 | minWidth: 'auto',
71 | minHeight: 'auto',
72 | display: 'none'
73 | },
74 | '@media (max-width: 704px)': {
75 | main: {
76 | margin: `${theme.spacing.unit}px calc(50% - 288px)`
77 | }
78 | },
79 | '@media (max-width: 576px)': {
80 | main: {
81 | margin: `${theme.spacing.unit}px 0`,
82 | paddingTop: theme.spacing.unit * 6
83 | },
84 | controlsBox: {
85 | position: 'absolute',
86 | top: 0,
87 | left: 0,
88 | maxHeight: 'calc(100% - 16px)',
89 | minWidth: 230,
90 | margin: 0,
91 | backgroundColor: translucentBackgroundColor(theme.palette.background.paper, 0.75),
92 | zIndex: 2
93 | },
94 | controlsButton: {
95 | display: 'inherit'
96 | }
97 | },
98 | '@media (max-width: 370px)': {
99 | controlsBox: {
100 | padding: `${theme.spacing.unit}px ${theme.spacing.unit / 2}px`
101 | }
102 | }
103 | });
104 |
105 |
106 | const Def = class NodeInspector extends React.Component {
107 | static propTypes = {
108 | classes: PropTypes.object.isRequired,
109 | className: PropTypes.string,
110 | theme: PropTypes.object.isRequired,
111 | children: PropTypes.oneOfType([
112 | PropTypes.arrayOf(PropTypes.node),
113 | PropTypes.node
114 | ]),
115 | tip: PropTypes.oneOfType([
116 | PropTypes.node,
117 | PropTypes.string
118 | ]),
119 | data: PropTypes.object.isRequired,
120 | genes: PropTypes.object.isRequired,
121 | title: PropTypes.string.isRequired,
122 | sourceData: PropTypes.object,
123 | onClose: PropTypes.func.isRequired,
124 | PreviewComponent: PropTypes.func,
125 | previewProps: PropTypes.object,
126 | navigation: PropTypes.object,
127 | highlightColor: PropTypes.string,
128 | tipCompact: PropTypes.string
129 | }
130 |
131 | state = {
132 | previewSize: 600,
133 | controlsExpanded: true,
134 | isCompact: compactMediaQuery.matches,
135 | sourceData: null
136 | }
137 |
138 | chartPreview = null
139 |
140 | setCompact = () => {
141 | this.setState({
142 | isCompact: compactMediaQuery.matches
143 | });
144 | }
145 |
146 | // eslint-disable-next-line camelcase
147 | UNSAFE_componentWillMount() {
148 | const sourceData = this.props.sourceData || fieldMappedTable(this.props.data);
149 | this.setState({ sourceData });
150 | }
151 |
152 | // eslint-disable-next-line camelcase
153 | UNSAFE_componentWillReceiveProps(newProps) {
154 | let sourceData = newProps.sourceData;
155 | if (!sourceData) {
156 | if (!newProps.data.equals(this.props.data) && !this.props.sourceData) {
157 | sourceData = fieldMappedTable(newProps.data);
158 | } else {
159 | sourceData = this.state.sourceData;
160 | }
161 | }
162 | this.setState({ sourceData });
163 | }
164 |
165 | toggleExpanded = () => {
166 | this.setState({
167 | controlsExpanded: !this.state.controlsExpanded
168 | });
169 | }
170 |
171 | onClose = () => {
172 | if (this.props.onClose) {
173 | this.props.onClose();
174 | }
175 | }
176 |
177 | onResize = () => {
178 | const previewElement = this.chartPreview && this.chartPreview.wrapper;
179 | if (!previewElement) {
180 | return;
181 | }
182 | const previewContainer = previewElement.parentElement;
183 | const previewSize = Math.min(previewContainer.offsetWidth, previewContainer.offsetHeight);
184 | this.setState({ previewSize });
185 | }
186 |
187 | previewRef = chartPreview => {
188 | this.chartPreview = chartPreview;
189 | this.onResize();
190 | }
191 |
192 | componentDidMount() {
193 | compactMediaQuery.addListener(this.setCompact);
194 | window.addEventListener('resize', this.onResize);
195 | }
196 |
197 | componentWillUnmount() {
198 | compactMediaQuery.removeListener(this.setCompact);
199 | window.removeEventListener('resize', this.onResize);
200 | }
201 |
202 | render() {
203 | const {
204 | sourceData,
205 | previewSize
206 | } = this.state;
207 |
208 | if (!sourceData) {
209 | return null;
210 | }
211 |
212 | const {
213 | classes,
214 | children,
215 | className,
216 | title,
217 | tip,
218 | navigation,
219 | data,
220 | genes,
221 | previewProps
222 | } = this.props;
223 |
224 | const chartType = data.get('chartType');
225 | const {
226 | controlsExpanded,
227 | isCompact
228 | } = this.state;
229 |
230 | const PreviewComponent = this.props.PreviewComponent || ChartPreview;
231 |
232 | return
233 |
234 | {tip && typeof tip === 'object' ? tip :
{tip} }
235 |
236 |
237 |
238 | {title}
239 | { !controlsExpanded ? Back : null}
245 | { controlsExpanded ?
251 | Hide :
252 | Show }
253 |
254 |
255 | {controlsExpanded || !isCompact ? children : null}
256 |
257 |
270 |
271 |
272 | {navigation}
273 | ;
274 | }
275 | };
276 |
277 | const NodeInspector = withStyles(styles, { withTheme: true })(Def);
278 | export default NodeInspector;
--------------------------------------------------------------------------------