├── public ├── robots.txt └── index.html ├── .eslintignore ├── src ├── util │ ├── fmod.js │ ├── num.js │ ├── downloadFile.js │ ├── setColorArray.js │ ├── translucentBackgroundColor.js │ ├── nextPowerOfTwo.js │ ├── getContrastTextColor.js │ ├── canvasToBlob.js │ ├── requestIdleCallback.js │ ├── VideoWriter │ │ ├── webm.js │ │ ├── gif.js │ │ ├── gif-optimized.js │ │ ├── index.js │ │ ├── quick-gif.js │ │ └── png.js │ ├── random.js │ ├── fieldMappedTable.js │ ├── hexColor.js │ ├── reportError.js │ ├── analytics.js │ ├── onNewServiceWorker.js │ ├── hsl2rgb.js │ ├── randomizeTimeline.js │ ├── share.js │ ├── hsv2rgb.js │ ├── contours.js │ ├── exportSVG.js │ ├── Quadtree.js │ ├── touch-mouse.js │ ├── svgToMesh.js │ └── normalize.js ├── charts │ ├── bar │ │ ├── thumb.png │ │ └── index.js │ ├── line │ │ ├── thumb.png │ │ └── index.js │ ├── pie │ │ ├── thumb.png │ │ └── index.js │ ├── scatter │ │ ├── thumb.png │ │ └── index.js │ ├── radialArea │ │ ├── thumb.png │ │ └── index.js │ ├── timeline │ │ ├── thumb.png │ │ └── index.js │ ├── index.js │ ├── drawable.js │ ├── constants.js │ ├── geneColor.js │ └── PanZoom.js ├── images │ ├── charts │ │ ├── alluvial.png │ │ └── hexpreview.jpg │ ├── morph-logo-text.svg │ └── morph-logo.svg ├── index.css ├── drawing │ ├── shapes │ │ ├── rectangle.js │ │ ├── PathGeometry.js │ │ └── arc.js │ ├── createSVGElement.js │ ├── shader │ │ ├── shape.frag │ │ ├── shape.vert │ │ └── index.js │ ├── Text.js │ ├── Path.js │ ├── LinePath.js │ ├── SVGPath.js │ ├── drawPixi.js │ ├── SVGLinePath.js │ ├── drawTitle.js │ ├── PixiText.js │ ├── PixiDrawable.js │ ├── Drawable.js │ ├── PixiPath.js │ ├── SVGDrawable.js │ ├── SVGText.js │ ├── PixiLinePath.js │ └── SpritePool.js ├── components │ ├── FileUploadIcon.js │ ├── icons │ │ ├── Facebook.js │ │ └── Twitter.js │ ├── Main.js │ ├── ListEntry.js │ ├── Section.js │ ├── SectionLoader.js │ ├── ChartPreview.js │ ├── RadioOption.js │ ├── Theme.js │ ├── OverwriteWarning.js │ ├── UpgradePrompt.js │ ├── ReviewData.js │ ├── Stepper.js │ ├── LoadFailure.js │ ├── ConfirmationDialog.js │ ├── SampleDataDialog.js │ ├── ColorPicker.js │ ├── Slider.js │ ├── SelectChartType.js │ ├── asyncComponent.js │ ├── SampleMenuItem.js │ ├── DataTable.js │ ├── Tip.js │ ├── UploadDialog.js │ ├── SocialShareDialog.js │ └── NodeInspector.js ├── constants.js ├── export │ ├── formats.js │ └── template.html ├── index.js ├── evolution │ └── index.js └── data │ ├── index.js │ └── european-parliament-2004.json ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .eslintrc.js ├── tools └── inject-manifest-plugin.js ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /src/util/fmod.js: -------------------------------------------------------------------------------- 1 | const fmod = (x, y) => x - y * Math.floor(x / y); 2 | export default fmod; 3 | -------------------------------------------------------------------------------- /src/charts/bar/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datavized/morph/HEAD/src/charts/bar/thumb.png -------------------------------------------------------------------------------- /src/charts/line/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datavized/morph/HEAD/src/charts/line/thumb.png -------------------------------------------------------------------------------- /src/charts/pie/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datavized/morph/HEAD/src/charts/pie/thumb.png -------------------------------------------------------------------------------- /src/charts/scatter/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datavized/morph/HEAD/src/charts/scatter/thumb.png -------------------------------------------------------------------------------- /src/charts/radialArea/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datavized/morph/HEAD/src/charts/radialArea/thumb.png -------------------------------------------------------------------------------- /src/charts/timeline/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datavized/morph/HEAD/src/charts/timeline/thumb.png -------------------------------------------------------------------------------- /src/images/charts/alluvial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datavized/morph/HEAD/src/images/charts/alluvial.png -------------------------------------------------------------------------------- /src/images/charts/hexpreview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datavized/morph/HEAD/src/images/charts/hexpreview.jpg -------------------------------------------------------------------------------- /src/util/num.js: -------------------------------------------------------------------------------- 1 | export default function num(n, alt) { 2 | return n === undefined || isNaN(n) ? alt : n; 3 | } 4 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | height: 100%; 6 | width: 100%; 7 | } -------------------------------------------------------------------------------- /src/util/downloadFile.js: -------------------------------------------------------------------------------- 1 | import { saveAs } from 'file-saver'; 2 | 3 | export default function downloadFile(fileName, blob) { 4 | saveAs(blob, fileName); 5 | } -------------------------------------------------------------------------------- /src/drawing/shapes/rectangle.js: -------------------------------------------------------------------------------- 1 | export default function rectangle(x = 0, y = 0, width = 1, height = 1) { 2 | return `M ${x} ${y} h ${width} v ${height} h ${-width} Z`; 3 | } -------------------------------------------------------------------------------- /src/drawing/createSVGElement.js: -------------------------------------------------------------------------------- 1 | const NS = 'http://www.w3.org/2000/svg'; 2 | export default function createSVGElement(tag, doc) { 3 | return (doc || document).createElementNS(NS, tag); 4 | } 5 | -------------------------------------------------------------------------------- /src/drawing/shader/shape.frag: -------------------------------------------------------------------------------- 1 | // precision mediump float; 2 | 3 | uniform vec4 uColor; 4 | 5 | void main() { 6 | 7 | vec3 rgb = uColor.rgb; 8 | float alpha = uColor.a; 9 | gl_FragColor = vec4(rgb * alpha, alpha); 10 | } -------------------------------------------------------------------------------- /src/components/FileUploadIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | const Def = props => 5 | 6 | 7 | ; 8 | 9 | const FileUploadIcon = Def; 10 | export default FileUploadIcon; -------------------------------------------------------------------------------- /src/drawing/shader/shape.vert: -------------------------------------------------------------------------------- 1 | // precision mediump float; 2 | 3 | attribute vec2 aVertexPosition; 4 | 5 | uniform mat3 translationMatrix; 6 | uniform mat3 projectionMatrix; 7 | 8 | void main() { 9 | 10 | gl_Position = vec4((projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); 11 | 12 | } -------------------------------------------------------------------------------- /src/charts/index.js: -------------------------------------------------------------------------------- 1 | import pie from './pie'; 2 | import bar from './bar'; 3 | import line from './line'; 4 | import timeline from './timeline'; 5 | import radialArea from './radialArea'; 6 | import scatter from './scatter'; 7 | 8 | export default { 9 | pie, 10 | bar, 11 | scatter, 12 | line, 13 | timeline, 14 | radialArea 15 | }; 16 | -------------------------------------------------------------------------------- /src/util/setColorArray.js: -------------------------------------------------------------------------------- 1 | export default function setColorArray(array, color, alpha = 1) { 2 | /* eslint-disable no-bitwise */ 3 | array[0] = (color >> 16 & 255) / 255; // red 4 | array[1] = (color >> 8 & 255) / 255; // green 5 | array[2] = (color & 255) / 255; // blue 6 | array[3] = alpha; 7 | /* eslint-enable no-bitwise */ 8 | 9 | return array; 10 | } -------------------------------------------------------------------------------- /src/util/translucentBackgroundColor.js: -------------------------------------------------------------------------------- 1 | import { decomposeColor, recomposeColor } from '@material-ui/core/styles/colorManipulator'; 2 | 3 | export default function translucentBackgroundColor(paletteColor, alpha) { 4 | const { values } = decomposeColor(paletteColor); 5 | return recomposeColor({ 6 | type: 'rgba', 7 | values: [...values, alpha] 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/icons/Facebook.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createSvgIcon from '@material-ui/icons/utils/createSvgIcon'; 3 | 4 | export default createSvgIcon( 5 | , 6 | 'Facebook' 7 | ); 8 | -------------------------------------------------------------------------------- /src/drawing/shader/index.js: -------------------------------------------------------------------------------- 1 | import assign from 'object-assign'; 2 | import { Shader } from '@pixi/core'; 3 | 4 | import vertexShader from './shape.vert'; 5 | import fragmentShader from './shape.frag'; 6 | 7 | export default function ShapeShader(uniforms) { 8 | return Shader.from(vertexShader, fragmentShader, assign({ 9 | uColor: [0.5, 0.5, 0.5, 1] 10 | }, uniforms)); 11 | } -------------------------------------------------------------------------------- /src/components/Main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import App from './App'; 4 | import Theme from './Theme'; 5 | 6 | const Def = class Main extends React.Component { 7 | componentDidCatch(error) { 8 | if (this.props.onError) { 9 | this.props.onError(error); 10 | } 11 | } 12 | 13 | render() { 14 | return ; 15 | } 16 | }; 17 | 18 | const Main = Def; 19 | export default Main; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # configuration 13 | .env 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | # misc 20 | .DS_Store 21 | /report.html 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /src/util/nextPowerOfTwo.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | const nextLog2 = Math.clz32 ? 3 | n => { 4 | const isNotPOT = !n || n & n - 1; 5 | return (isNotPOT ? 32 : 31) - Math.clz32(n); 6 | } : 7 | n => Math.ceil(Math.log(n) / Math.LN2); 8 | 9 | /* 10 | Nearest (larger) power of two 11 | */ 12 | const nextPowerOfTwo = n => 1 << nextLog2(n); 13 | /* eslint-enable no-bitwise */ 14 | 15 | export default nextPowerOfTwo; 16 | export { nextLog2 }; 17 | -------------------------------------------------------------------------------- /src/charts/drawable.js: -------------------------------------------------------------------------------- 1 | import drawPixi from '../drawing/drawPixi'; 2 | import chartTypes from './index'; 3 | import assign from 'object-assign'; 4 | 5 | const drawableCharts = {}; 6 | for (const k in chartTypes) { 7 | if (chartTypes.hasOwnProperty(k)) { 8 | const chartDefinition = chartTypes[k]; 9 | drawableCharts[k] = assign({}, chartDefinition, { 10 | draw: drawPixi(chartDefinition.draw) 11 | }); 12 | } 13 | } 14 | 15 | export default drawableCharts; -------------------------------------------------------------------------------- /src/util/getContrastTextColor.js: -------------------------------------------------------------------------------- 1 | export function luminance(r, g, b) { 2 | return 0.2126 * r + 0.7152 * g + 0.0722 * b; 3 | } 4 | 5 | export default function getContrastTextColor(backgroundColorObject) { 6 | if (!backgroundColorObject || !backgroundColorObject.a) { 7 | return 0x888888; // gray 8 | } 9 | 10 | const {r, g, b} = backgroundColorObject; 11 | if (luminance(r / 255, g / 255, b / 255) < 0.5) { 12 | return 0xffffff; // white 13 | } 14 | 15 | return 0x0; // black 16 | } -------------------------------------------------------------------------------- /src/util/canvasToBlob.js: -------------------------------------------------------------------------------- 1 | export default function canvasToBlob(canvas, callback, type = 'image/png', quality) { 2 | if (canvas.toBlob) { 3 | canvas.toBlob(callback, type, quality); 4 | return; 5 | } 6 | 7 | const binStr = atob(canvas.toDataURL(type, quality).split(',')[1]); 8 | const len = binStr.length; 9 | const arr = new Uint8Array(len); 10 | 11 | for (let i = 0; i < len; i++) { 12 | arr[i] = binStr.charCodeAt(i); 13 | } 14 | 15 | callback(new Blob([arr], { type })); 16 | } -------------------------------------------------------------------------------- /src/drawing/Text.js: -------------------------------------------------------------------------------- 1 | import Drawable from './Drawable'; 2 | 3 | function Text(text) { 4 | Drawable.call(this); 5 | 6 | let color = 0x888888; 7 | 8 | this.text = text; 9 | this.size = 26; 10 | 11 | Object.defineProperties(this, { 12 | color: { 13 | get: () => color, 14 | set: c => { 15 | if (c !== color) { 16 | color = c; 17 | this.emit('update'); 18 | } 19 | } 20 | } 21 | }); 22 | } 23 | 24 | Text.prototype = Object.create(Drawable.prototype); 25 | 26 | export default Text; -------------------------------------------------------------------------------- /src/util/requestIdleCallback.js: -------------------------------------------------------------------------------- 1 | // shim requestIdleCallback 2 | const requestIdleCallback = window.requestIdleCallback || 3 | function (cb) { 4 | return setTimeout(() => { 5 | const start = Date.now(); 6 | cb({ 7 | didTimeout: false, 8 | timeRemaining: function () { 9 | return Math.max(0, 10 - (Date.now() - start)); 10 | } 11 | }); 12 | }, 1); 13 | }; 14 | const cancelIdleCallback = window.cancelIdleCallback || (id => clearTimeout(id)); 15 | 16 | export default requestIdleCallback; 17 | export { requestIdleCallback, cancelIdleCallback }; 18 | -------------------------------------------------------------------------------- /src/util/VideoWriter/webm.js: -------------------------------------------------------------------------------- 1 | import WebMWriter from 'webm-writer'; 2 | import assign from 'object-assign'; 3 | 4 | export default function webm({quality, frameRate, ...options}, videoWriter, finish) { 5 | // WebM image quality from 0.0 (worst) to 1.0 (best) 6 | quality = quality || 1; 7 | frameRate = frameRate || 30; 8 | 9 | const webmWriter = new WebMWriter(assign({ 10 | quality, 11 | frameRate 12 | }, options)); 13 | 14 | return { 15 | addFrame(canvas) { 16 | webmWriter.addFrame(canvas); 17 | }, 18 | finish() { 19 | webmWriter.complete().then(finish); 20 | } 21 | }; 22 | } -------------------------------------------------------------------------------- /src/drawing/Path.js: -------------------------------------------------------------------------------- 1 | /* 2 | This PIXI Transform object is pretty good, 3 | so let's borrow it for now 4 | */ 5 | import Drawable from './Drawable'; 6 | 7 | function Path(svgPathString) { 8 | Drawable.call(this); 9 | 10 | let color = 0x888888; 11 | 12 | this.path = svgPathString; 13 | 14 | Object.defineProperties(this, { 15 | color: { 16 | get: () => color, 17 | set: c => { 18 | if (c !== color) { 19 | color = c; 20 | this.emit('update'); 21 | } 22 | } 23 | } 24 | }); 25 | } 26 | 27 | Path.prototype = Object.create(Drawable.prototype); 28 | 29 | export default Path; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /src/util/random.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | /* eslint-disable no-extra-parens */ 3 | export default function random(initial) { 4 | let seed = (initial | 0) + 0x2F6E2B1; 5 | seed = (seed + 0x7ED55D16 + seed << 12) & 0xFFFFFFFF; 6 | seed = (seed ^ 0xC761C23C ^ seed >>> 19) & 0xFFFFFFFF; 7 | seed = (seed + 0x165667B1 + seed << 5) & 0xFFFFFFFF; 8 | seed = (seed + 0xD3A2646C ^ seed << 9) & 0xFFFFFFFF; 9 | seed = (seed + 0xFD7046C5 + seed << 3) & 0xFFFFFFFF; 10 | seed = (seed ^ 0xB55A4F09 ^ seed >>> 16) & 0xFFFFFFFF; 11 | 12 | const rand = (seed & 0xFFFFFFF) / 0x10000000; 13 | return rand * 2 - 1; 14 | } 15 | /* eslint-enable no-extra-parens */ 16 | /* eslint-enable no-bitwise */ 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser: false */ 2 | const eslintConfig = require('datavized-code-style'); 3 | module.exports = Object.assign(eslintConfig, { 4 | plugins: eslintConfig.plugins.concat([ 5 | 'react', 6 | 'jsx-a11y' 7 | ]), 8 | env: { 9 | browser: true, 10 | es6: true, 11 | commonjs: true 12 | }, 13 | extends: eslintConfig.extends.concat([ 14 | 'plugin:react/recommended' 15 | ]), 16 | rules: Object.assign(eslintConfig.rules, { 17 | 'react/jsx-uses-react': 'error', 18 | 'react/jsx-uses-vars': 'error', 19 | 'no-invalid-this': 0, 20 | 'react/forbid-foreign-prop-types': 'error' 21 | }), 22 | settings: { 23 | react: { 24 | version: require('react').version 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/ListEntry.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import PropTypes from 'prop-types'; 5 | import ListItem from '@material-ui/core/ListItem'; 6 | 7 | const styles = theme => ({ 8 | root: { 9 | height: theme.spacing.unit * 4 10 | } 11 | }); 12 | 13 | const Def = ({children, ...props}) => 14 | 15 | {children} 16 | ; 17 | 18 | Def.propTypes = { 19 | children: PropTypes.oneOfType([ 20 | PropTypes.arrayOf(PropTypes.node), 21 | PropTypes.node 22 | ]), 23 | className: PropTypes.string 24 | }; 25 | 26 | const ListEntry = withStyles(styles)(Def); 27 | export default ListEntry; -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // hard limit on max number of rows allowed in spreadsheet. 2 | export const UPLOAD_ROW_LIMIT = 2000; 3 | export const MAX_DATA_FILE_SIZE = 2000000; // 2MB 4 | 5 | // https://color.adobe.com/vaporwave-4-color-theme-10814044/ 6 | export const SECTION_COLORS = [ 7 | '#AC66F2', 8 | '#07F285', 9 | '#05DBF2', 10 | '#F7F391', 11 | '#F76DC8' 12 | ]; 13 | 14 | export const SHARE_HASHTAGS = ['feedmorph']; 15 | export const IMGUR_CLIENT_ID = '866e7b15476035e'; 16 | export const IMGUR_UPLOAD_URL = 'https://api.imgur.com/3/image'; 17 | export const IMGUR_LINK_PREFIX = 'https://imgur.com/'; 18 | export const IMGUR_DESCRIPTION = `Made with Morph ${SHARE_HASHTAGS.map(t => '#' + t).join(' ')}\n` + 19 | location.origin; 20 | -------------------------------------------------------------------------------- /src/charts/constants.js: -------------------------------------------------------------------------------- 1 | // chart drawing 2 | export const DISPLAY_ROW_LIMIT = 300; 3 | 4 | // tree drawing stuff 5 | export const MIN_SCALE = 0.05; 6 | export const MAX_SCALE = 2; 7 | export const ZOOM_SPEED = 4; 8 | export const DEFAULT_ZOOM = 1 / 8; //0.35; 9 | export const MOVE_THRESHOLD = 2; // pixels 10 | export const PAN_ZOOM_TIME = 1; 11 | 12 | // tree and tree node 13 | export const MAX_SPRITE_SIZE = 512; 14 | export const CHILD_SPACING_ANGLE = 0.55; 15 | export const CHILD_SPACING_RADIUS = 3; 16 | export const COLLISION_MARGIN = 2 * Math.sqrt(0.5); 17 | export const LINE_WIDTH = 0.03; 18 | export const ANIMATION_DURATION = 1000; 19 | 20 | export const NUM_CHILDREN = 7; 21 | export const MUTATION_RATE = 1.5 / 7; 22 | export const MAX_DRIFT = 0.7;// / 20; 23 | -------------------------------------------------------------------------------- /src/util/fieldMappedTable.js: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | 3 | export default function fieldMappedTable(data) { 4 | const rows = data.get('rows'); 5 | const normalized = data.get('normalized'); 6 | const chartType = data.get('chartType'); 7 | const fieldMap = data.getIn(['fieldMap', chartType]) || new Map(); 8 | return { 9 | data, 10 | fieldMap, 11 | tree: data.getIn(['tree', chartType]), 12 | count: rows && rows.length || 0, 13 | normalized, 14 | rows, 15 | field: name => fieldMap.get(name), 16 | has: name => fieldMap.get(name) >= 0 && fieldMap.get(name) < data.get('fields').size, 17 | value: (name, rowIndex) => normalized[rowIndex][fieldMap.get(name)], 18 | original: (name, rowIndex) => rows[rowIndex][fieldMap.get(name)] 19 | }; 20 | } -------------------------------------------------------------------------------- /src/components/Section.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* 4 | Material UI components 5 | */ 6 | import PropTypes from 'prop-types'; 7 | 8 | class Section extends React.Component { 9 | static propTypes = { 10 | onBack: PropTypes.func, 11 | onNext: PropTypes.func, 12 | navigation: PropTypes.object, 13 | data: PropTypes.object.isRequired, 14 | setData: PropTypes.func.isRequired, 15 | classes: PropTypes.object.isRequired, 16 | className: PropTypes.string, 17 | highlightColor: PropTypes.string 18 | } 19 | 20 | onBack = () => { 21 | if (this.props.onBack) { 22 | this.props.onBack(); 23 | } 24 | } 25 | 26 | onNext = () => { 27 | if (this.props.onBack) { 28 | this.props.onNext(); 29 | } 30 | } 31 | } 32 | 33 | export default Section; 34 | -------------------------------------------------------------------------------- /src/util/hexColor.js: -------------------------------------------------------------------------------- 1 | import { decomposeColor } from '@material-ui/core/styles/colorManipulator'; 2 | 3 | export default function hexColor(color, defaultVal) { 4 | if (color !== undefined) { 5 | if (color === 'number') { 6 | return color; 7 | } 8 | 9 | if (typeof color === 'string') { 10 | /* 11 | warning: this assumes RGB. if we need to support other formats, 12 | we'll need a lot more code here 13 | */ 14 | if (color[0] === '#') { 15 | return parseInt(color.substring(1), 16); 16 | } 17 | 18 | const { type, values } = decomposeColor(color); 19 | if (type === 'rgb' || type === 'rgba') { 20 | const [r, g, b] = values; 21 | 22 | // eslint-disable-next-line no-bitwise 23 | return r << 16 ^ g << 8 ^ b << 0; 24 | } 25 | } 26 | } 27 | return defaultVal; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/icons/Twitter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createSvgIcon from '@material-ui/icons/utils/createSvgIcon'; 3 | 4 | export default createSvgIcon( 5 | , 6 | 'Twitter' 7 | ); 8 | -------------------------------------------------------------------------------- /src/drawing/LinePath.js: -------------------------------------------------------------------------------- 1 | /* 2 | This PIXI Transform object is pretty good, 3 | so let's borrow it for now 4 | */ 5 | import Drawable from './Drawable'; 6 | 7 | function Path(points) { 8 | Drawable.call(this); 9 | 10 | let color = 0x888888; 11 | let lineWidth = 1; 12 | 13 | this.points = points; 14 | 15 | Object.defineProperties(this, { 16 | lineWidth: { 17 | get: () => lineWidth, 18 | set: w => { 19 | w = Math.max(0, w); 20 | if (w !== lineWidth && w >= 0) { 21 | lineWidth = w; 22 | this.emit('update'); 23 | } 24 | } 25 | }, 26 | 27 | color: { 28 | get: () => color, 29 | set: c => { 30 | if (c !== color) { 31 | color = c; 32 | this.emit('update'); 33 | } 34 | } 35 | } 36 | }); 37 | } 38 | 39 | Path.prototype = Object.create(Drawable.prototype); 40 | 41 | export default Path; -------------------------------------------------------------------------------- /src/components/SectionLoader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import withStyles from '@material-ui/core/styles/withStyles'; 5 | import CircularProgress from '@material-ui/core/CircularProgress'; 6 | 7 | const styles = theme => ({ 8 | root: { 9 | display: 'flex', 10 | flexDirection: 'column', 11 | justifyContent: 'center', 12 | alignItems: 'center', 13 | flex: 1 14 | }, 15 | circle: { 16 | color: theme.palette.grey[500] 17 | } 18 | }); 19 | 20 | const Def = ({classes, ...otherProps}) => 21 |
22 | 23 |
; 24 | 25 | Def.propTypes = { 26 | classes: PropTypes.object.isRequired 27 | }; 28 | 29 | const SectionLoader = withStyles(styles)(Def); 30 | export default SectionLoader; -------------------------------------------------------------------------------- /src/components/ChartPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Sketch from './Sketch'; 5 | import chartTypes from '../charts/drawable'; 6 | 7 | function nothing() { 8 | return Object.create(null); 9 | } 10 | 11 | const Def = class ChartPreview extends React.Component { 12 | static propTypes = { 13 | className: PropTypes.string, 14 | chartType: PropTypes.string.isRequired 15 | } 16 | render() { 17 | const { 18 | chartType 19 | } = this.props; 20 | const chartDefinition = chartTypes[chartType]; 21 | 22 | return ( 23 | this.wrapper = ref && ref.wrapper} 28 | {...this.props} 29 | /> 30 | ); 31 | 32 | } 33 | }; 34 | 35 | const ChartPreview = Def; 36 | export default ChartPreview; -------------------------------------------------------------------------------- /src/util/VideoWriter/gif.js: -------------------------------------------------------------------------------- 1 | import GIF from 'gif.js'; 2 | import assign from 'object-assign'; 3 | 4 | export default function gif({quality, frameRate, ...options}, videoWriter, finish) { 5 | // todo: set quality correctly - the lower the better 6 | quality = quality || 10; 7 | frameRate = frameRate || 30; 8 | 9 | // todo: figure out what to do about transparency 10 | 11 | const gifWriter = new GIF(assign({ 12 | workerScript: require('!!file-loader!gif.js/dist/gif.worker.js'), 13 | workers: 4, 14 | quality 15 | }, options)); 16 | 17 | const frameOptions = { 18 | delay: 1000 / frameRate 19 | }; 20 | 21 | gifWriter.on('progress', p => videoWriter.emit('progress', p)); 22 | gifWriter.on('finished', finish); 23 | 24 | return { 25 | reportsProgress: true, 26 | addFrame: canvas => gifWriter.addFrame(canvas, frameOptions), 27 | finish: () => gifWriter.render(), 28 | destroy: () => gifWriter.abort() 29 | }; 30 | } -------------------------------------------------------------------------------- /src/util/VideoWriter/gif-optimized.js: -------------------------------------------------------------------------------- 1 | import GIF from 'gif.js.optimized'; 2 | import assign from 'object-assign'; 3 | 4 | export default function gif({quality, frameRate, ...options}, videoWriter, finish) { 5 | // todo: set quality correctly - the lower the better 6 | quality = 8; //quality || 1; 7 | frameRate = frameRate || 30; 8 | 9 | // todo: figure out what to do about transparency 10 | 11 | const gifWriter = new GIF(assign({ 12 | workerScript: require('!!file-loader!gif.js/dist/gif.worker.js'), 13 | workers: 4, 14 | quality 15 | }, options)); 16 | 17 | const frameOptions = { 18 | delay: 1000 / frameRate 19 | }; 20 | 21 | gifWriter.on('progress', p => videoWriter.emit('progress', p)); 22 | gifWriter.on('finished', finish); 23 | 24 | return { 25 | reportsProgress: true, 26 | addFrame: canvas => gifWriter.addFrame(canvas, frameOptions), 27 | finish: () => gifWriter.render(), 28 | destroy: () => gifWriter.abort() 29 | }; 30 | } -------------------------------------------------------------------------------- /src/util/reportError.js: -------------------------------------------------------------------------------- 1 | import ErrorStackParser from 'error-stack-parser'; 2 | 3 | export default function reportError(event) { 4 | const error = event.error || event; 5 | console.error(error); 6 | 7 | let details = ''; 8 | 9 | const errorDetails = ErrorStackParser.parse(error); 10 | if (errorDetails && errorDetails.length) { 11 | const stackTop = errorDetails[0]; 12 | details = [ 13 | stackTop.fileName, 14 | stackTop.lineNumber, 15 | stackTop.columnNumber, 16 | stackTop.functionName 17 | ].filter(val => val !== undefined).join(':'); 18 | } else { 19 | details = event instanceof window.ErrorEvent ? 20 | [ 21 | (event.filename || '').replace(window.location.origin, ''), 22 | event.lineno, 23 | event.colno 24 | ].join(':') : 25 | event.toString(); 26 | } 27 | 28 | if (window.ga && details) { 29 | window.ga('send', 'exception', { 30 | exDescription: `${error.message} [${details}]` 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/RadioOption.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import PropTypes from 'prop-types'; 5 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 6 | import Radio from '@material-ui/core/Radio'; 7 | 8 | const styles = theme => ({ 9 | root: { 10 | height: theme.spacing.unit * 4 11 | } 12 | }); 13 | 14 | const Def = ({classes, children, ...otherProps}) => { 15 | const label = children && typeof children === 'string' ? 16 | children : 17 | ''; 18 | const control = !label && children && children.length ? children : ; 19 | return ; 24 | }; 25 | 26 | Def.propTypes = { 27 | classes: PropTypes.object.isRequired, 28 | children: PropTypes.oneOfType([ 29 | PropTypes.arrayOf(PropTypes.node), 30 | PropTypes.node 31 | ]) 32 | }; 33 | 34 | const RadioOption = withStyles(styles)(Def); 35 | export default RadioOption; -------------------------------------------------------------------------------- /src/components/Theme.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import PropTypes from 'prop-types'; 4 | import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; 5 | import yellow from '@material-ui/core/colors/yellow'; 6 | 7 | const globalTheme = createMuiTheme({ 8 | palette: { 9 | type: 'dark', 10 | background: { 11 | default: '#212121', 12 | paper: '#323232' 13 | }, 14 | primary: { 15 | light: '#6ff9ff', 16 | main: '#26c6da', // Cyan[400] 17 | dark: '#0095a8', 18 | contrastText: '#000' 19 | }, 20 | secondary: { 21 | light: '#ffad42', 22 | main: '#f57c00', // Orange[700] 23 | dark: '#bb4d00', 24 | contrastText: '#000' 25 | }, 26 | divider: '#e0f7fa', 27 | error: yellow 28 | } 29 | }); 30 | 31 | const Def = ({children}) => 32 | {children}; 33 | 34 | Def.propTypes = { 35 | children: PropTypes.oneOfType([ 36 | PropTypes.arrayOf(PropTypes.node), 37 | PropTypes.node 38 | ]) 39 | }; 40 | const Theme = Def; 41 | export default Theme; -------------------------------------------------------------------------------- /src/components/OverwriteWarning.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import withStyles from '@material-ui/core/styles/withStyles'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import WarningIcon from '@material-ui/icons/Warning'; 7 | 8 | const styles = theme => ({ 9 | root: { 10 | marginTop: theme.spacing.unit 11 | }, 12 | iconContainer: { 13 | display: 'inline-flex', 14 | alignSelf: 'center', 15 | marginRight: theme.spacing.unit / 2, 16 | color: theme.palette.error.main 17 | }, 18 | icon: { 19 | position: 'relative', 20 | top: '0.25em', 21 | fontSize: 20 22 | } 23 | }); 24 | 25 | const Def = ({classes}) => 26 | 27 | 28 | 29 | 30 | This will replace existing data 31 | ; 32 | 33 | Def.propTypes = { 34 | classes: PropTypes.object.isRequired 35 | }; 36 | 37 | const OverwriteWarning = withStyles(styles)(Def); 38 | export default OverwriteWarning; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Build version** 11 | Look for build number from developer console. e.g. Morph by Datavized Technologies (build 15d9336) 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/drawing/shapes/PathGeometry.js: -------------------------------------------------------------------------------- 1 | import svgToMesh from '../../util/svgToMesh'; 2 | import { Renderer, Geometry } from '@pixi/core'; 3 | import { MeshRenderer } from '@pixi/mesh'; 4 | Renderer.registerPlugin('mesh', MeshRenderer); 5 | 6 | // quick and dirty implementation 7 | function flatten(source) { 8 | if (!source || !source.length) { 9 | return [source]; 10 | } 11 | 12 | if (source.flatten) { 13 | return source.flatten(); 14 | } 15 | 16 | let out = []; 17 | for (let i = 0; i < source.length; i++) { 18 | const val = source[i]; 19 | if (val && val.length) { 20 | out = out.concat(flatten(val)); 21 | } else { 22 | out.push(val); 23 | } 24 | } 25 | 26 | return out; 27 | } 28 | 29 | function PathGeometry(path) { 30 | Geometry.call(this); 31 | 32 | const { positions, cells } = svgToMesh(path, { 33 | normalize: false, 34 | scale: 200 35 | }); 36 | const vPositions = flatten(positions); 37 | const indices = flatten(cells); 38 | 39 | this.addAttribute('aVertexPosition', vPositions, 2) 40 | .addIndex(indices); 41 | } 42 | 43 | PathGeometry.prototype = Object.create(Geometry.prototype); 44 | 45 | export default PathGeometry; -------------------------------------------------------------------------------- /src/drawing/SVGPath.js: -------------------------------------------------------------------------------- 1 | import SVGDrawable from './SVGDrawable'; 2 | import Path from './Path'; 3 | import createSVGElement from './createSVGElement'; 4 | 5 | function SVGPath(parent, drawable) { 6 | SVGDrawable.call(this, parent, drawable); 7 | 8 | const path = drawable && drawable.path; 9 | 10 | const superUpdate = this.update; 11 | this.update = () => { 12 | // create path or container if haven't done it yet 13 | if (!this.element) { 14 | if (path) { 15 | this.element = createSVGElement('path'); 16 | this.element.setAttribute('d', path); 17 | this.element.setAttribute('stroke', 'transparent'); 18 | this.element.setAttribute('stroke-width', 0); 19 | } else { 20 | this.element = createSVGElement('g'); 21 | } 22 | if (parent) { 23 | parent.appendChild(this.element); 24 | } 25 | } 26 | 27 | if (drawable && path) { 28 | const hex = ('00000' + drawable.color.toString(16)).slice(-6); 29 | this.element.setAttribute('fill', '#' + hex); 30 | } 31 | superUpdate.call(this); 32 | }; 33 | } 34 | 35 | SVGPath.prototype = Object.create(SVGDrawable.prototype); 36 | SVGDrawable.register(Path, SVGPath); 37 | 38 | export default SVGPath; -------------------------------------------------------------------------------- /src/charts/geneColor.js: -------------------------------------------------------------------------------- 1 | import hsv2rgb from '../util/hsv2rgb'; 2 | import fmod from '../util/fmod'; 3 | 4 | export const defaultColorValues = { 5 | hueOffset: 0, 6 | hueRange: 0.6, 7 | saturationOffset: 1, 8 | saturationValueFactor: 0, 9 | lightnessOffset: 0.6, 10 | lightnessValueFactor: 0 11 | }; 12 | 13 | export default function geneColor(genes, colorVal) { 14 | const hueOffset = genes.get('hueOffset') || 0; 15 | const hueRange = genes.get('hueRange') || 0; 16 | const saturationOffset = genes.get('saturationOffset') || 0; 17 | const saturationValueFactor = genes.get('saturationValueFactor') || 0; 18 | const lightnessOffset = genes.get('lightnessOffset') || 0; 19 | const lightnessValueFactor = genes.get('lightnessValueFactor') || 0; 20 | 21 | const hue = fmod(hueOffset + hueRange * colorVal + 0.5, 1); 22 | const saturation = saturationOffset + saturationValueFactor * colorVal; 23 | const lightness = lightnessOffset + lightnessValueFactor * colorVal; 24 | 25 | /* 26 | We're using HSV instead of HSL, even though it's called lightness. 27 | The hope is that this will result in more saturated colors. 28 | */ 29 | return hsv2rgb(hue, Math.abs(saturation), lightness); 30 | } -------------------------------------------------------------------------------- /src/util/analytics.js: -------------------------------------------------------------------------------- 1 | /* 2 | todo: log these config properties 3 | - margin 4 | - showLabels 5 | - showTitle 6 | - bounce 7 | - transparentBackground 8 | */ 9 | import exportFormats from '../export/formats'; 10 | 11 | const metrics = { 12 | resolution: 1, 13 | frameRate: 2, 14 | duration: 3 15 | }; 16 | 17 | const dimensions = { 18 | format: 1 19 | }; 20 | 21 | const animationProperties = new Set(['frameRate', 'duration']); 22 | 23 | const ga = self.ga; 24 | 25 | export default function logExportEvent(config, action = 'save') { 26 | const data = {}; 27 | const format = exportFormats[config.format]; 28 | const animation = !!format.anim; 29 | 30 | Object.keys(metrics).forEach(key => { 31 | const val = config[key]; 32 | if (val !== undefined && (animation || !animationProperties.has(key))) { 33 | const saveKey = 'metric' + metrics[key]; 34 | data[saveKey] = val; 35 | } 36 | }); 37 | Object.keys(dimensions).forEach(key => { 38 | const val = config[key]; 39 | if (val !== undefined && (animation || !animationProperties.has(key))) { 40 | const saveKey = 'dimension' + dimensions[key]; 41 | data[saveKey] = val; 42 | } 43 | }); 44 | ga('send', 'event', 'export', action, data); 45 | 46 | } -------------------------------------------------------------------------------- /src/util/onNewServiceWorker.js: -------------------------------------------------------------------------------- 1 | const keys = ['installing', 'waiting', 'active']; 2 | export default function onNewServiceWorker(reg, callback) { 3 | const key = keys.find(k => reg[k]); 4 | const original = reg[key]; 5 | 6 | if (reg.waiting) { 7 | // SW is waiting to activate. Can occur if multiple clients open and 8 | // one of the clients is refreshed. 9 | return callback(); 10 | } 11 | 12 | function listenInstalledStateChange() { 13 | const key = keys.find(k => reg[k] !== original && reg[k]); 14 | const next = reg[key]; 15 | if (!next) { 16 | return; 17 | } 18 | 19 | if (next.state === 'installed' || next !== original && next.state === 'activated') { 20 | callback(); 21 | } else { 22 | next.addEventListener('statechange', () => { 23 | // console.log('statechange', event.target, event.target === next); 24 | if (next.state === 'installed') { 25 | callback(); 26 | } 27 | }); 28 | } 29 | } 30 | 31 | if (reg.installing) { 32 | return listenInstalledStateChange(); 33 | } 34 | 35 | // We are currently controlled so a new SW may be found... 36 | // Add a listener in case a new SW is found, 37 | reg.addEventListener('updatefound', listenInstalledStateChange); 38 | } 39 | -------------------------------------------------------------------------------- /src/util/VideoWriter/index.js: -------------------------------------------------------------------------------- 1 | import eventEmitter from 'event-emitter'; 2 | import allOff from 'event-emitter/all-off'; 3 | 4 | /* 5 | */ 6 | 7 | export default function VideoWriter(implementation, options) { 8 | eventEmitter(this); 9 | 10 | let started = false; 11 | let destroyed = false; 12 | 13 | const finish = blob => { 14 | if (!destroyed) { 15 | this.emit('finish', blob); 16 | } 17 | }; 18 | 19 | const impl = implementation(options || {}, this, finish); 20 | 21 | 22 | /* 23 | todo: 24 | - implement detection for whether the format is supported 25 | - report mime type 26 | - report pixi renderer options? 27 | */ 28 | 29 | Object.defineProperty(this, 'reportsProgress', { 30 | value: impl.reportsProgress === true 31 | }); 32 | 33 | this.start = () => { 34 | if (!started && impl.start) { 35 | impl.start(); 36 | } 37 | started = true; 38 | }; 39 | 40 | this.finish = () => { 41 | if (started && impl.finish) { 42 | impl.finish(); 43 | } 44 | started = false; 45 | }; 46 | 47 | this.addFrame = canvas => { 48 | if (impl.addFrame) { 49 | impl.addFrame(canvas); 50 | } 51 | }; 52 | 53 | this.destroy = () => { 54 | destroyed = true; 55 | if (impl.destroy) { 56 | impl.destroy(); 57 | } 58 | allOff(this); 59 | }; 60 | } -------------------------------------------------------------------------------- /src/drawing/drawPixi.js: -------------------------------------------------------------------------------- 1 | import Path from './Path'; 2 | import PixiPath from './PixiPath'; 3 | import './PixiText'; 4 | import './PixiLinePath'; 5 | 6 | export default function wrapDraw(drawDefinition, parentPath) { 7 | return function (pixiContainer, app) { 8 | const pathContainer = parentPath || new Path(); 9 | 10 | const renderableContainer = new PixiPath(pixiContainer, pathContainer); 11 | const draw = drawDefinition(pathContainer); 12 | return { 13 | update(opts) { 14 | const { a, r, g, b } = opts.backgroundColor || { a: 0 }; 15 | if (a > 0) { 16 | // eslint-disable-next-line no-bitwise 17 | app.renderer.backgroundColor = r << 16 ^ g << 8 ^ b << 0; 18 | } else if (opts.bgColor && opts.bgColor[0] === '#') { 19 | app.renderer.backgroundColor = parseInt(opts.bgColor.substring(1), 16); 20 | } 21 | 22 | draw.update(opts); 23 | renderableContainer.update(); 24 | }, 25 | resize(w, h) { 26 | if (draw.resize) { 27 | draw.resize(w, h); 28 | renderableContainer.update(); 29 | } 30 | }, 31 | destroy() { 32 | if (draw.destroy) { 33 | draw.destroy(); 34 | } 35 | if (!parentPath) { 36 | pathContainer.destroyAll(); 37 | } 38 | renderableContainer.destroy(); 39 | } 40 | }; 41 | }; 42 | } -------------------------------------------------------------------------------- /src/export/formats.js: -------------------------------------------------------------------------------- 1 | const exportFormats = { 2 | png: { 3 | name: 'PNG (Raster)', 4 | mime: 'image/png', 5 | ext: 'png', 6 | share: true 7 | }, 8 | svg: { 9 | name: 'SVG (Vector)', 10 | mime: 'image/svg+xml', 11 | ext: 'svg' 12 | }, 13 | html: { 14 | name: 'HTML (Vector/Interactive)', 15 | mime: 'text/html', 16 | ext: 'html' 17 | }, 18 | gif: { 19 | name: 'Animated GIF', 20 | mime: 'image/gif', 21 | ext: 'gif', 22 | anim: true, 23 | transparent: false, 24 | maxFrameRate: 30, 25 | share: true 26 | }, 27 | webm: { 28 | name: 'WebM Video', 29 | mime: 'video/webm', 30 | ext: 'webm', 31 | anim: true, 32 | transparent: false, 33 | maxFrameRate: 60, 34 | share: true 35 | }, 36 | frames: { 37 | name: 'PNG Frame Sequence', 38 | mime: 'application/zip', 39 | ext: 'zip', 40 | anim: true 41 | } 42 | }; 43 | 44 | const supportsExportWebM = (function () { 45 | try { 46 | const tempCanvas = document.createElement('canvas'); 47 | tempCanvas.width = tempCanvas.height = 1; 48 | const dataURL = tempCanvas.toDataURL('image/webp'); 49 | return /^data:image\/webp/.test(dataURL); 50 | } catch (e) {} 51 | return false; 52 | }()); 53 | 54 | if (!supportsExportWebM) { 55 | delete exportFormats.webm; 56 | } 57 | 58 | export default exportFormats; -------------------------------------------------------------------------------- /src/drawing/shapes/arc.js: -------------------------------------------------------------------------------- 1 | import fmod from '../../util/fmod'; 2 | 3 | const TAU = Math.PI * 2; 4 | const EPSILON = 0.000001; 5 | 6 | const fix = n => Math.round(n * 1e9) / 1e9; 7 | 8 | /* 9 | Our code for converting SVG arcs to meshes doesn't do well when 10 | we give it a circle, so as a workaround, we'll give it two half-circles 11 | */ 12 | export function circle(x, y, radius) { 13 | const startX = x + radius; 14 | const halfX = x - radius; 15 | return [ 16 | 'M', startX, y, 17 | 'A', radius, radius, 0, 1, 0, halfX, y, 18 | 'A', radius, radius, 0, 1, 0, startX, y 19 | ].join(' '); 20 | } 21 | 22 | export default function arc(x, y, radius, startAngle, endAngle) { 23 | if (Math.abs(fmod(startAngle - endAngle, TAU)) < EPSILON) { 24 | return circle(x, y, radius); 25 | } 26 | 27 | const startX = fix(x + radius * Math.cos(endAngle)); 28 | const startY = fix(y + radius * Math.sin(endAngle)); 29 | const endX = fix(x + radius * Math.cos(startAngle)); 30 | const endY = fix(y + radius * Math.sin(startAngle)); 31 | const deltaAngle = endAngle - startAngle; 32 | 33 | const largeArcFlag = deltaAngle <= Math.PI ? 0 : 1; 34 | 35 | const path = [ 36 | 'M', x, y, 37 | 'L', startX, startY, 38 | 'A', radius, radius, 0, largeArcFlag, 0, endX, endY 39 | ]; 40 | 41 | return path.join(' '); 42 | } -------------------------------------------------------------------------------- /src/util/hsl2rgb.js: -------------------------------------------------------------------------------- 1 | // convert HSL to RGB as a single integer usable by PIXI 2 | // assume HSL are already normalized 0 to 1 3 | export default function color(hue, sat, lum) { 4 | function calcHue(h, m1, m2) { 5 | h = h < 0 ? 6 | h + 1 : 7 | h > 1 ? h - 1 : h; 8 | 9 | if (h * 6 < 1) { 10 | return m1 + (m2 - m1) * h * 6; 11 | } 12 | if (h * 2 < 1) { 13 | return m2; 14 | } 15 | if (h * 3 < 2) { 16 | return m1 + (m2 - m1) * (2 / 3 - h) * 6; 17 | } 18 | 19 | return m1; 20 | } 21 | 22 | /* 23 | Sometimes we get negative lum values, which results in 24 | a lot of black once you clamp it. To keep things interesting, 25 | if we get a negative, we'll use it as an indication to invert the 26 | color like a photo negative. 27 | */ 28 | const invert = lum < 0; 29 | if (invert) { 30 | lum = 1 + lum; 31 | } 32 | 33 | lum = Math.max(0, Math.min(1, lum)); 34 | sat = Math.max(0, Math.min(1, sat)); 35 | 36 | const m2 = lum <= 0.5 ? lum * (sat + 1) : lum + sat - lum * sat; 37 | const m1 = lum * 2 - m2; 38 | 39 | let r = calcHue(hue + 1 / 3, m1, m2); 40 | let g = calcHue(hue, m1, m2); 41 | let b = calcHue(hue - 1 / 3, m1, m2); 42 | if (invert) { 43 | r = 1 - r; 44 | g = 1 - g; 45 | b = 1 - b; 46 | } 47 | 48 | // eslint-disable-next-line no-bitwise 49 | return r * 255 << 16 ^ g * 255 << 8 ^ b * 255 << 0; 50 | } -------------------------------------------------------------------------------- /src/export/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 22 | INSERT_TITLE_HERE 23 | 24 | INSERT_SVG_HERE 25 | 53 | -------------------------------------------------------------------------------- /src/drawing/SVGLinePath.js: -------------------------------------------------------------------------------- 1 | import SVGDrawable from './SVGDrawable'; 2 | import LinePath from './LinePath'; 3 | import createSVGElement from './createSVGElement'; 4 | 5 | function SVGLinePath(parent, drawable) { 6 | SVGDrawable.call(this, parent, drawable); 7 | 8 | const points = drawable && drawable.points; 9 | 10 | const superUpdate = this.update; 11 | this.update = () => { 12 | // create path or container if haven't done it yet 13 | if (!this.element) { 14 | if (points) { 15 | const [ 16 | first, 17 | ...rest 18 | ] = points; 19 | const path = 'M ' + first.join(' ') + 20 | rest.map(p => 'L ' + p.join(' ')).join(' '); 21 | 22 | this.element = createSVGElement('path'); 23 | this.element.setAttribute('d', path); 24 | this.element.setAttribute('fill', 'transparent'); 25 | } else { 26 | this.element = createSVGElement('g'); 27 | } 28 | if (parent) { 29 | parent.appendChild(this.element); 30 | } 31 | } 32 | 33 | if (drawable && points) { 34 | const hex = ('00000' + drawable.color.toString(16)).slice(-6); 35 | this.element.setAttribute('stroke', '#' + hex); 36 | 37 | this.element.setAttribute('stroke-width', drawable.lineWidth); 38 | } 39 | superUpdate.call(this); 40 | }; 41 | } 42 | 43 | SVGLinePath.prototype = Object.create(SVGDrawable.prototype); 44 | SVGDrawable.register(LinePath, SVGLinePath); 45 | 46 | export default SVGLinePath; -------------------------------------------------------------------------------- /src/util/randomizeTimeline.js: -------------------------------------------------------------------------------- 1 | export default function randomize(fields) { 2 | if (!fields.length) { 3 | return {}; 4 | } 5 | 6 | const horizontal = Math.random() > 0.5; 7 | const dimField = horizontal ? 'y' : 'x'; 8 | 9 | const fieldNames = [dimField, 'time']; 10 | 11 | const indices = Object.keys(fields).map(parseFloat); 12 | let numericFieldIndices = indices.filter(index => { 13 | const f = fields[index]; 14 | const type = f.type; 15 | return type === 'int' || type === 'float'; 16 | }); 17 | 18 | const fieldMap = {}; 19 | fieldNames.forEach(name => { 20 | if (!numericFieldIndices.length) { 21 | numericFieldIndices = indices; 22 | } 23 | const count = numericFieldIndices.length; 24 | const i = Math.floor(Math.random() * count) % count; 25 | const index = numericFieldIndices[i]; 26 | numericFieldIndices.splice(i, 1); 27 | fieldMap[name] = index; 28 | }); 29 | 30 | let stringFieldIndices = indices.filter(index => { 31 | const f = fields[index]; 32 | return f.type === 'string'; 33 | }); 34 | if (!stringFieldIndices.length) { 35 | stringFieldIndices = indices; 36 | } 37 | if (!indices.length) { 38 | stringFieldIndices = Object.keys(fields).map(parseFloat); 39 | } 40 | const count = stringFieldIndices.length; 41 | const i = Math.floor(Math.random() * count) % count; 42 | const index = stringFieldIndices[i]; 43 | stringFieldIndices.splice(i, 1); 44 | fieldMap.group = index; 45 | 46 | return fieldMap; 47 | } -------------------------------------------------------------------------------- /src/components/UpgradePrompt.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Button from '@material-ui/core/Button'; 5 | import Snackbar from '@material-ui/core/Snackbar'; 6 | 7 | let reloadRequested = false; 8 | 9 | const Def = class UpgradePrompt extends React.Component { 10 | static propTypes = { 11 | upgradeReady: PropTypes.bool 12 | } 13 | 14 | state = { 15 | dismissed: false 16 | } 17 | 18 | onDismiss = () => { 19 | this.setState({ 20 | dismissed: true 21 | }); 22 | } 23 | 24 | onReload = () => { 25 | if (reloadRequested) { 26 | return; 27 | } 28 | reloadRequested = true; 29 | this.setState({ 30 | dismissed: true 31 | }); 32 | window.location.reload(); 33 | } 34 | 35 | render() { 36 | const { upgradeReady } = this.props; 37 | 38 | if (!upgradeReady || this.state.dismissed) { 39 | return null; 40 | } 41 | return Morph has upgraded. Reload for the latest version.} 52 | action={[ 53 | 56 | ]} 57 | />; 58 | } 59 | }; 60 | 61 | const UpgradePrompt = Def; 62 | export default UpgradePrompt; 63 | -------------------------------------------------------------------------------- /src/util/share.js: -------------------------------------------------------------------------------- 1 | // Opens a pop-up with twitter sharing dialog 2 | export function popup(url, params) { 3 | const query = Object.keys(params) 4 | .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key] || '')) 5 | .join('&'); 6 | window.open( 7 | url + '?' + query, 8 | '', 9 | 'left=0,top=0,width=550,height=450,personalbar=0,toolbar=0,scrollbars=0,resizable=0' 10 | ); 11 | } 12 | 13 | export function shareFacebook(u) { 14 | popup('https://www.facebook.com/sharer/sharer.php', { 15 | u 16 | }); 17 | } 18 | 19 | export function shareTwitter(title = '', text, url, hashtags) { 20 | if (Array.isArray(hashtags)) { 21 | hashtags = hashtags.join(','); 22 | } 23 | text = [text]; 24 | if (title) { 25 | text.unshift(title); 26 | } 27 | popup('https://twitter.com/intent/tweet', { 28 | text: text.join('\n'), 29 | url, 30 | hashtags 31 | }); 32 | } 33 | 34 | export function shareEmail(title = 'Morph image', text, url) { 35 | const mailLink = `mailto:?body=${encodeURIComponent(text + '\n' + url)}&subject=${encodeURIComponent(title)}`; 36 | window.open(mailLink); 37 | } 38 | 39 | export function shareNative(title = '', text = '', url, hashtags) { 40 | if (navigator.share) { 41 | hashtags = hashtags.map(t => '#' + t).join(' '); 42 | if (hashtags) { 43 | text = text + ' ' + hashtags; 44 | } 45 | return navigator.share({ 46 | title, 47 | text, 48 | url 49 | }); 50 | } 51 | return Promise.reject(new Error('Native social sharing not supported')); 52 | } -------------------------------------------------------------------------------- /src/components/ReviewData.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | /* 5 | Material UI components 6 | */ 7 | import withStyles from '@material-ui/core/styles/withStyles'; 8 | 9 | import Tip from './Tip'; 10 | import Section from './Section'; 11 | import DataTable from './DataTable'; 12 | 13 | const styles = theme => ({ 14 | root: { 15 | display: 'flex', 16 | flexDirection: 'column', 17 | justifyContent: 'center', 18 | flex: 1, 19 | minHeight: 0 20 | }, 21 | main: { 22 | flex: 1, 23 | overflowY: 'auto', 24 | margin: `${theme.spacing.unit}px 10%` 25 | }, 26 | '@media (max-width: 704px)': { 27 | main: { 28 | margin: `${theme.spacing.unit}px calc(50% - 288px)` 29 | } 30 | }, 31 | '@media (max-width: 576px)': { 32 | main: { 33 | margin: `${theme.spacing.unit}px 0` 34 | } 35 | } 36 | }); 37 | 38 | const Def = class ReviewData extends Section { 39 | render() { 40 | const { classes, data/*, navigation*/ } = this.props; 41 | return 42 |
43 | 44 |

Step 2 - Review

45 |

Examine your data below, then select Design to pick your chart type.

46 |
47 |
48 | 49 |
50 |
51 | {/*navigation*/} 52 |
; 53 | } 54 | }; 55 | 56 | const ReviewData = withStyles(styles)(Def); 57 | export default ReviewData; -------------------------------------------------------------------------------- /src/util/hsv2rgb.js: -------------------------------------------------------------------------------- 1 | // convert HSV to RGB as a single integer usable by PIXI 2 | import fmod from '../util/fmod'; 3 | 4 | // assume HSV are already normalized 0 to 1 5 | export default function hsv2rgb(hue, sat, lum) { 6 | /* 7 | Sometimes we get negative lum values, which results in 8 | a lot of black once you clamp it. To keep things interesting, 9 | if we get a negative, we'll use it as an indication to cycle the 10 | hue by half a rotation. 11 | */ 12 | const invert = lum < 0; 13 | if (invert) { 14 | lum = Math.abs(lum); 15 | hue = fmod(hue + 0.5, 1); 16 | } 17 | 18 | lum = Math.max(0, Math.min(1, lum)); 19 | sat = Math.max(0, Math.min(1, sat)); 20 | 21 | const v = lum; 22 | const i = Math.floor(hue * 6); 23 | const f = hue * 6 - i; 24 | const p = v * (1 - sat); 25 | const q = v * (1 - f * sat); 26 | const t = v * (1 - (1 - f) * sat); 27 | 28 | let r = 0; 29 | let g = 0; 30 | let b = 0; 31 | const range = i % 6; 32 | 33 | if (range === 0) { 34 | r = v; 35 | g = t; 36 | b = p; 37 | } else if (range === 1) { 38 | r = q; 39 | g = v; 40 | b = p; 41 | } else if (range === 2) { 42 | r = p; 43 | g = v; 44 | b = t; 45 | } else if (range === 3) { 46 | r = p; 47 | g = q; 48 | b = v; 49 | } else if (range === 4) { 50 | r = t; 51 | g = p; 52 | b = v; 53 | } else if (range === 5) { 54 | r = v; 55 | g = p; 56 | b = q; 57 | } 58 | 59 | // if (invert) { 60 | // r = 1 - r; 61 | // g = 1 - g; 62 | // b = 1 - b; 63 | // } 64 | 65 | // eslint-disable-next-line no-bitwise 66 | return r * 255 << 16 ^ g * 255 << 8 ^ b * 255 << 0; 67 | } -------------------------------------------------------------------------------- /src/components/Stepper.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 Stepper from '@material-ui/core/Stepper'; 10 | 11 | const styles = theme => ({ 12 | stepper: { 13 | backgroundColor: theme.palette.background.default, 14 | display: 'none' 15 | }, 16 | stepperRoot: { 17 | padding: theme.spacing.unit * 2, 18 | flexGrow: 1 19 | }, 20 | '@media (max-width: 620px)': { 21 | endButton: {}, // we need this here so the next bit works 22 | stepper: { 23 | '& $endButton': { 24 | display: 'none' 25 | } 26 | } 27 | }, 28 | '@media (min-width: 620px)': { 29 | stepper: { 30 | display: 'inherit' 31 | }, 32 | mobileStepper: { 33 | display: 'none' 34 | } 35 | } 36 | }); 37 | 38 | const Def = class StyledStepper extends React.Component { 39 | static propTypes = { 40 | className: PropTypes.string, 41 | classes: PropTypes.object.isRequired, 42 | children: PropTypes.oneOfType([ 43 | PropTypes.arrayOf(PropTypes.node), 44 | PropTypes.node 45 | ]) 46 | } 47 | 48 | render() { 49 | const { 50 | classes, 51 | children, 52 | ...otherProps 53 | } = this.props; 54 | 55 | return
56 | 60 | {children} 61 | 62 |
; 63 | } 64 | }; 65 | 66 | const StyledStepper = withStyles(styles)(Def); 67 | export default StyledStepper; -------------------------------------------------------------------------------- /src/util/VideoWriter/quick-gif.js: -------------------------------------------------------------------------------- 1 | import { GIF } from 'quick-gif.js'; 2 | import assign from 'object-assign'; 3 | 4 | const contexts = new WeakMap(); 5 | 6 | function getImageData(canvas) { 7 | /* 8 | todo: if canvas is 2d, don't bother copying 9 | */ 10 | if (canvas instanceof CanvasRenderingContext2D) { 11 | throw new Error('not implemented yet'); 12 | } 13 | let ctx = contexts.get(canvas); 14 | let c = ctx && ctx.canvas || null; 15 | if (!ctx) { 16 | c = document.createElement('canvas'); 17 | ctx = c.getContext('2d'); 18 | contexts.set(canvas, ctx); 19 | } 20 | c.width = canvas.width; 21 | c.height = canvas.height; 22 | 23 | ctx.drawImage(canvas, 0, 0); 24 | 25 | return ctx.getImageData(0, 0, c.width, c.height); 26 | } 27 | 28 | export default function gif({quality, frameRate, width, height, transparent, ...options}, videoWriter, finish) { 29 | // todo: set quality correctly - the lower the better 30 | quality = 6; //quality || 1; 31 | frameRate = frameRate || 30; 32 | 33 | const gifWriter = new GIF(width, height, assign({ 34 | workers: 4, 35 | quality 36 | }, { 37 | transparent: transparent ? 0 : null, 38 | ...options 39 | })); 40 | 41 | const frameOptions = { 42 | delay: 1000 / frameRate 43 | }; 44 | 45 | // gifWriter.on('progress', p => videoWriter.emit('progress', p)); 46 | gifWriter.on('finished', finish); 47 | 48 | return { 49 | start: () => gifWriter.start(), 50 | addFrame: canvas => { 51 | // todo: get ImageData from webgl canvas 52 | gifWriter.addFrame(getImageData(canvas), frameOptions); 53 | }, 54 | finish: () => gifWriter.finish(), 55 | destroy: () => gifWriter.destroy() 56 | }; 57 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* global COMMIT_HASH, DEBUG_SERVICE_WORKER */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import onNewServiceWorker from './util/onNewServiceWorker'; 6 | import reportError from './util/reportError'; 7 | 8 | import './index.css'; 9 | import Main from './components/Main'; 10 | 11 | if (window.ga) { 12 | window.ga('set', { 13 | appName: 'morph', 14 | appVersion: COMMIT_HASH 15 | }); 16 | window.addEventListener('error', reportError); 17 | } 18 | console.log(`Morph by Datavized Technologies (build ${COMMIT_HASH})`); 19 | 20 | const rootEl = document.getElementById('root'); 21 | 22 | let upgradeReady = false; 23 | let render = () => {}; 24 | const MainWithProps = () =>
; 25 | 26 | if (module.hot) { 27 | const { AppContainer } = require('react-hot-loader'); 28 | render = () => { 29 | ReactDOM.render(, rootEl); 30 | }; 31 | 32 | render(); 33 | module.hot.accept('./components/Main', render); 34 | } else { 35 | render = () => { 36 | ReactDOM.render(, rootEl); 37 | }; 38 | render(); 39 | } 40 | if (!module.hot || DEBUG_SERVICE_WORKER) { 41 | if ('serviceWorker' in navigator) { 42 | // Use the window load event to keep the page load performant 43 | window.addEventListener('load', () => { 44 | navigator.serviceWorker.register('/sw.js').then(reg => { 45 | // check once an hour 46 | setInterval(() => reg.update(), 1000 * 60 * 60); 47 | 48 | onNewServiceWorker(reg, () => { 49 | upgradeReady = true; 50 | render(); 51 | }); 52 | }); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/LoadFailure.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import withStyles from '@material-ui/core/styles/withStyles'; 5 | import Typography from '@material-ui/core/Typography'; 6 | import Button from '@material-ui/core/Button'; 7 | import ErrorIcon from '@material-ui/icons/Error'; 8 | 9 | const styles = theme => ({ 10 | root: { 11 | display: 'flex', 12 | flexDirection: 'column', 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | flex: 1 16 | }, 17 | warning: { 18 | // cursor: 'pointer', 19 | textAlign: 'center' 20 | }, 21 | icon: { 22 | color: theme.palette.error.main, 23 | width: theme.spacing.unit * 8, 24 | height: theme.spacing.unit * 8 25 | } 26 | }); 27 | 28 | const Def = class LoadFailure extends React.Component { 29 | static propTypes = { 30 | classes: PropTypes.object.isRequired, 31 | onRetry: PropTypes.func, 32 | connected: PropTypes.bool 33 | } 34 | 35 | onClick = () => { 36 | this.props.onRetry(); 37 | } 38 | 39 | render() { 40 | const { classes, connected } = this.props; 41 | return
42 |
43 | 44 | {connected ? 45 |
46 | Error loading app. 47 | 48 |
: 49 | Error loading app. Network offline. 50 | } 51 |
52 |
; 53 | } 54 | }; 55 | 56 | Def.propTypes = { 57 | classes: PropTypes.object.isRequired 58 | }; 59 | 60 | const LoadFailure = withStyles(styles)(Def); 61 | export default LoadFailure; -------------------------------------------------------------------------------- /src/drawing/drawTitle.js: -------------------------------------------------------------------------------- 1 | import Path from './Path'; 2 | import Text from './Text'; 3 | import getContrastTextColor from '../util/getContrastTextColor'; 4 | 5 | export default function drawTitle(drawDefinition/*, parentPath*/) { 6 | return function (container/*, app*/) { 7 | // const pathContainer = parentPath || new Path(); 8 | // const draw = drawDefinition(pathContainer); 9 | const drawingContainer = new Path(); 10 | container.add(drawingContainer); 11 | 12 | const text = new Text(''); 13 | text.size = 30; 14 | container.add(text); 15 | 16 | const draw = drawDefinition(drawingContainer); 17 | let width = 1; 18 | let height = 1; 19 | 20 | function updateText(backgroundColor) { 21 | if (text.visible) { 22 | drawingContainer.scale.set(0.85, 0.85); 23 | text.position.set(width / 2, height / 15); 24 | drawingContainer.position.set(0.075 * width, 0.15 * height); 25 | 26 | const textScale = width / 800; 27 | text.scale.set(textScale, textScale); 28 | 29 | if (backgroundColor) { 30 | text.color = getContrastTextColor(backgroundColor); 31 | } 32 | } else { 33 | drawingContainer.scale.set(1, 1); 34 | drawingContainer.position.set(0, 0); 35 | } 36 | } 37 | 38 | return { 39 | update(opts) { 40 | const title = opts.title ? opts.title.trim() : ''; 41 | text.text = title; 42 | text.visible = !!title; 43 | updateText(opts.backgroundColor); 44 | draw.update(opts); 45 | }, 46 | resize(w, h) { 47 | width = w; 48 | height = h; 49 | updateText(); 50 | if (draw.resize) { 51 | draw.resize(w, h); 52 | } 53 | }, 54 | destroy() { 55 | if (draw.destroy) { 56 | draw.destroy(); 57 | } 58 | } 59 | }; 60 | }; 61 | } -------------------------------------------------------------------------------- /src/util/contours.js: -------------------------------------------------------------------------------- 1 | /* 2 | Adapted from https://github.com/mattdesl/svg-path-contours 3 | MIT License 4 | - Code style change for readability 5 | - Don't convert lines to curves, since they waste points 6 | */ 7 | 8 | 9 | import bezier from 'adaptive-bezier-curve'; 10 | import abs from 'abs-svg-path'; 11 | // import norm from 'normalize-svg-path'; 12 | import norm from './normalize'; 13 | 14 | import copy from 'vec2-copy'; 15 | 16 | function set(out, x, y) { 17 | out[0] = x; 18 | out[1] = y; 19 | return out; 20 | } 21 | 22 | const tmp1 = [0, 0]; 23 | const tmp2 = [0, 0]; 24 | const tmp3 = [0, 0]; 25 | 26 | function bezierTo(points, scale, start, seg) { 27 | bezier(start, 28 | set(tmp1, seg[1], seg[2]), 29 | set(tmp2, seg[3], seg[4]), 30 | set(tmp3, seg[5], seg[6]), scale, points); 31 | } 32 | 33 | function lineTo(points, scale, start, seg) { 34 | const [x1, y1] = start; 35 | points.push( 36 | [x1, y1], 37 | [seg[1], seg[2]] 38 | ); 39 | } 40 | 41 | export default function contours(svg, scale) { 42 | const paths = []; 43 | 44 | const points = []; 45 | const pen = [0, 0]; 46 | norm(abs(svg)).forEach(segment => { 47 | if (segment[0] === 'M') { 48 | copy(pen, segment.slice(1)); 49 | if (points.length > 0) { 50 | paths.push(points); 51 | points.length = 0; 52 | } 53 | } else if (segment[0] === 'L') { 54 | lineTo(points, scale, pen, segment); 55 | set(pen, segment[1], segment[2]); 56 | } else if (segment[0] === 'C') { 57 | bezierTo(points, scale, pen, segment); 58 | set(pen, segment[5], segment[6]); 59 | } else { 60 | throw new Error('illegal type in SVG: ' + segment[0]); 61 | } 62 | }); 63 | 64 | if (points.length > 0) { 65 | paths.push(points); 66 | } 67 | return paths; 68 | } -------------------------------------------------------------------------------- /src/util/VideoWriter/png.js: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip'; 2 | 3 | const FRAME_DIGITS = 5; 4 | const MIME_TYPE = 'image/png'; 5 | const DATA_URL_SLICE = 'data:image/png;base64,'.length; 6 | const base64Opt = {base64: true}; 7 | 8 | function promise(fn) { 9 | return new Promise(resolve => fn(resolve)); 10 | } 11 | 12 | export default function png({ quality, digits }, videoWriter, finish) { 13 | digits = digits || FRAME_DIGITS; 14 | quality = quality || 1; 15 | 16 | const zip = new JSZip(); 17 | const promises = []; 18 | 19 | let frameCount = 0; 20 | 21 | let maxProgress = 0; 22 | 23 | function onUpdate({ percent, currentFile }) { 24 | if (percent < maxProgress) { 25 | console.warn('progress regressed', currentFile, percent); 26 | // debugger; 27 | } 28 | maxProgress = Math.max(maxProgress, percent); 29 | 30 | videoWriter.emit('progress', percent / 100); 31 | } 32 | 33 | return { 34 | reportsProgress: true, 35 | addFrame: canvas => { 36 | let frame = String(frameCount++); 37 | while (frame.length < digits) { 38 | frame = '0' + frame; 39 | } 40 | 41 | const fileName = 'image-' + frame + '.png'; 42 | if (canvas.toBlob) { 43 | promises.push(promise(cb => canvas.toBlob( 44 | blob => { 45 | zip.file(fileName, blob); 46 | cb(); 47 | }, 48 | MIME_TYPE, 49 | quality 50 | ))); 51 | } else { 52 | const dataURL = canvas.toDataURL(MIME_TYPE, quality); 53 | const base64 = dataURL.slice(DATA_URL_SLICE); 54 | zip.file(fileName, base64, base64Opt); 55 | } 56 | }, 57 | finish: () => Promise.all(promises) 58 | .then(() => zip.generateAsync({ type: 'blob' }, onUpdate)) 59 | .then(finish), 60 | destroy: () => zip.forEach(name => zip.remove(name)) 61 | }; 62 | } -------------------------------------------------------------------------------- /src/drawing/PixiText.js: -------------------------------------------------------------------------------- 1 | import PixiDrawable from './PixiDrawable'; 2 | import Text from './Text'; 3 | 4 | import { Container } from '@pixi/display'; 5 | import { Renderer } from '@pixi/core'; 6 | import { /*Sprite as PIXISprite, */SpriteRenderer } from '@pixi/sprite'; 7 | import { Text as PIXIText, TextStyle } from '@pixi/text'; 8 | Renderer.registerPlugin('sprite', SpriteRenderer); 9 | 10 | // just mapping by font size 11 | const styles = new Map(); 12 | function fontStyle(size) { 13 | let style = styles.get(size); 14 | if (!style) { 15 | style = new TextStyle({ 16 | fontSize: size 17 | }); 18 | styles.set(size, style); 19 | } 20 | return style; 21 | } 22 | 23 | function PixiText(parent, drawable) { 24 | PixiDrawable.call(this, parent, drawable); 25 | 26 | let textObject = null; 27 | 28 | const superUpdate = this.update; 29 | this.update = () => { 30 | const text = drawable && drawable.text || ''; 31 | if (!this.pixiObject) { 32 | this.pixiObject = new Container(); 33 | if (parent) { 34 | parent.addChild(this.pixiObject); 35 | } 36 | 37 | textObject = new PIXIText(text, fontStyle(drawable.size)); 38 | this.pixiObject.addChild(textObject); 39 | } else { 40 | textObject.text = text; 41 | textObject.style = fontStyle(drawable.size); 42 | } 43 | 44 | // center 45 | textObject.position.set( 46 | -textObject.width / 2, 47 | -textObject.height / 2 48 | ); 49 | 50 | const hex = ('00000' + drawable.color.toString(16)).slice(-6); 51 | textObject.style.fill = '#' + hex; 52 | textObject.alpha = drawable.opacity; 53 | 54 | superUpdate.call(this); 55 | }; 56 | } 57 | 58 | PixiText.prototype = Object.create(PixiDrawable.prototype); 59 | 60 | PixiDrawable.register(Text, PixiText); 61 | 62 | export default PixiText; -------------------------------------------------------------------------------- /src/evolution/index.js: -------------------------------------------------------------------------------- 1 | import { fromJS, Map, List } from 'immutable'; 2 | 3 | import { 4 | NUM_CHILDREN, 5 | MUTATION_RATE, 6 | MAX_DRIFT 7 | } from '../charts/constants'; 8 | 9 | export function defaultTree(chartInfo) { 10 | const genes = chartInfo.genes; 11 | return fromJS({ 12 | time: Date.now(), 13 | genes, 14 | children: [] 15 | }); 16 | } 17 | 18 | // centered around [-1 to 1] 19 | export const random = () => Math.random() * 2 - 1; 20 | 21 | export const mutate = (val, scale = 1) => { 22 | const drift = random() * random() * MAX_DRIFT; 23 | const result = val + drift * scale; 24 | 25 | // bounce back if result is out of [-1,1] range 26 | if (result < -1) { 27 | return -2 - result; 28 | } 29 | if (result > 1) { 30 | return 2 - result; 31 | } 32 | return result; 33 | }; 34 | 35 | export function spawnNodes(node, opts, timeOffset = 0, numChildren = NUM_CHILDREN) { 36 | const genes = node.get('genes'); 37 | let children = node.get('children'); 38 | 39 | for (let i = 0; i < numChildren; i++) { 40 | if (!children.has(i)) { 41 | let newGenes = genes.map(val => Math.random() < MUTATION_RATE ? mutate(val) : val); 42 | 43 | if (newGenes.equals(genes)) { 44 | // make sure we have at least one mutation 45 | const geneIndex = Math.floor(Math.random() * genes.size) % genes.size; 46 | const geneKey = genes.keySeq().get(geneIndex); 47 | newGenes = genes.update(geneKey, mutate); 48 | // newGenes = genes.set(geneKey, mutate(genes.get(geneKey))); 49 | } 50 | 51 | const child = new Map({ 52 | time: Date.now() + timeOffset, 53 | genes: newGenes, 54 | opts: new Map(opts), 55 | children: new List() 56 | }); 57 | children = children.set(i, child); 58 | } 59 | } 60 | 61 | return node.set('children', children); 62 | } -------------------------------------------------------------------------------- /src/util/exportSVG.js: -------------------------------------------------------------------------------- 1 | import fieldMappedTable from './fieldMappedTable'; 2 | import createSVGElement from '../drawing/createSVGElement'; 3 | import Path from '../drawing/Path'; 4 | import SVGPath from '../drawing/SVGPath'; 5 | import '../drawing/SVGText'; 6 | import '../drawing/SVGLinePath'; 7 | 8 | export default function exportSVG(options) { 9 | const { 10 | data, 11 | nodePath, 12 | drawFunction, 13 | resolution, 14 | backgroundColor 15 | } = options; 16 | const sourceData = fieldMappedTable(data); 17 | const genes = data.getIn(nodePath).get('genes'); 18 | 19 | const svg = createSVGElement('svg'); 20 | const attributes = { 21 | xmlns: 'http://www.w3.org/2000/svg', 22 | version: '1.1', 23 | baseProfile: 'full', 24 | width: resolution, 25 | height: resolution, 26 | viewBox: '0 0 1 1' 27 | }; 28 | Object.keys(attributes).forEach(key => { 29 | svg.setAttribute(key, attributes[key]); 30 | }); 31 | 32 | if (backgroundColor && backgroundColor.a) { 33 | const { 34 | r, g, b, a 35 | } = backgroundColor; 36 | svg.style.backgroundColor = `rgba(${r}, ${g}, ${b}, ${a})`; 37 | } 38 | 39 | const pathContainer = new Path(); 40 | if (options.margin) { 41 | pathContainer.scale.set(0.8, 0.8); 42 | pathContainer.position.set(0.1, 0.1); 43 | } 44 | 45 | const renderableContainer = new SVGPath(svg, pathContainer); 46 | const draw = drawFunction(pathContainer); 47 | if (draw.update) { 48 | draw.update({ 49 | sourceData, 50 | genes, 51 | backgroundColor: options.backgroundColor, 52 | title: options.showTitle && options.title, 53 | showLabels: !!options.showLabels, 54 | fontSize: options.labelFontSize 55 | }); 56 | } 57 | renderableContainer.update(); 58 | 59 | const svgSource = svg.outerHTML; 60 | 61 | // clean up 62 | if (draw.destroy) { 63 | draw.destroy(); 64 | } 65 | pathContainer.destroyAll(); 66 | renderableContainer.destroy(); 67 | 68 | return svgSource; 69 | } -------------------------------------------------------------------------------- /src/drawing/PixiDrawable.js: -------------------------------------------------------------------------------- 1 | const constructors = []; 2 | 3 | function PixiDrawable(parent, drawable) { 4 | const children = new Map(); 5 | this.drawable = drawable; 6 | this.pixiObject = null; 7 | 8 | function removeChild(childDrawable) { 9 | const pixiDrawable = children.get(childDrawable); 10 | if (pixiDrawable) { 11 | children.delete(childDrawable); 12 | } 13 | } 14 | 15 | this.update = () => { 16 | // update matrix - copy from drawable 17 | if (this.drawable) { 18 | this.pixiObject.visible = drawable.visible; 19 | this.pixiObject.transform.setFromMatrix(drawable.matrix); 20 | 21 | // update children 22 | drawable.children.forEach(childDrawable => { 23 | let pixiDrawable = children.get(childDrawable); 24 | if (!pixiDrawable) { 25 | let ChildConstructor = PixiDrawable; 26 | for (let i = 0; i < constructors.length; i++) { 27 | const [D, P] = constructors[i]; 28 | if (childDrawable instanceof D) { 29 | ChildConstructor = P; 30 | break; 31 | } 32 | } 33 | 34 | pixiDrawable = new ChildConstructor(this.pixiObject, childDrawable); 35 | children.set(childDrawable, pixiDrawable); 36 | } 37 | pixiDrawable.update(); 38 | }); 39 | } 40 | }; 41 | 42 | const callDestroy = () => this.destroy(); 43 | this.destroy = () => { 44 | if (parent && this.pixiObject) { 45 | parent.removeChild(this.pixiObject); 46 | } 47 | 48 | if (drawable) { 49 | drawable.off('destroy', callDestroy); 50 | drawable.off('remove', removeChild); 51 | } 52 | 53 | if (this.pixiObject) { 54 | this.pixiObject.destroy({ 55 | children: true 56 | }); 57 | } 58 | 59 | children.clear(); 60 | }; 61 | 62 | if (drawable) { 63 | drawable.on('destroy', callDestroy); 64 | drawable.on('remove', removeChild); 65 | } 66 | } 67 | 68 | PixiDrawable.register = (drawable, pixiDrawable) => constructors.push([drawable, pixiDrawable]); 69 | 70 | export default PixiDrawable; -------------------------------------------------------------------------------- /tools/inject-manifest-plugin.js: -------------------------------------------------------------------------------- 1 | /* 2 | We build a manifest.json full of icons with favicons-webpack-plugin 3 | but it does not properly inject it into the HTML 4 | (bug: https://github.com/jantimon/favicons-webpack-plugin/pull/10) 5 | 6 | This custom plugin lets us do that and modify it with some of 7 | our own preferred properties 8 | */ 9 | 10 | const { RawSource } = require('webpack-sources'); 11 | function InjectManifestPlugin(options) { 12 | this.options = options; 13 | } 14 | 15 | InjectManifestPlugin.prototype.apply = function (compiler) { 16 | let manifestLocation = ''; 17 | const options = this.options; 18 | 19 | compiler.hooks.compilation.tap('InjectManifestPlugin', (compilation) => { 20 | if (compilation.hooks.htmlWebpackPluginAfterHtmlProcessing) { 21 | compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync( 22 | 'InjectManifestPlugin', 23 | (data, cb) => { 24 | if (manifestLocation) { 25 | data.html = data.html.replace( 26 | /(<\/head>)/i, 27 | `` 28 | ); 29 | } 30 | cb(null, data); 31 | } 32 | ); 33 | } 34 | 35 | if (compilation.hooks.additionalAssets) { 36 | compilation.hooks.additionalAssets.tapAsync( 37 | 'InjectManifestPlugin', 38 | cb => { 39 | for (const name in compilation.assets) { 40 | if (compilation.assets.hasOwnProperty(name) && /manifest\.json$/.test(name)) { 41 | const asset = compilation.assets[name]; 42 | try { 43 | if (options) { 44 | const manifest = JSON.parse(asset.source()); 45 | const modifiedManifest = Object.assign(manifest, options || {}); 46 | compilation.assets[name] = new RawSource(JSON.stringify(modifiedManifest)); 47 | } 48 | manifestLocation = name; 49 | } catch (e) { 50 | console.warn('failed to update manifest', e); 51 | } 52 | } 53 | } 54 | 55 | cb(null); 56 | } 57 | ); 58 | } 59 | }); 60 | }; 61 | 62 | module.exports = InjectManifestPlugin; -------------------------------------------------------------------------------- /src/charts/line/index.js: -------------------------------------------------------------------------------- 1 | import draw from './draw'; 2 | import randomize from '../../util/randomizeTimeline'; 3 | import { defaultColorValues } from '../geneColor'; 4 | 5 | export default { 6 | key: 'line', 7 | name: 'Line Chart', 8 | preview: require('./thumb.png'), 9 | required: ['x', 'y', 'group', 'time'], 10 | valid: fields => 11 | fields.has('group') && fields.has('time') && 12 | (fields.has('x') || fields.has('y')), 13 | randomize, 14 | draw, 15 | properties: [ 16 | { 17 | key: 'group', 18 | name: 'Group' 19 | }, 20 | { 21 | key: 'time', 22 | name: 'Time' 23 | }, 24 | { 25 | key: 'x', 26 | name: 'Width' 27 | }, 28 | { 29 | key: 'y', 30 | name: 'Height' 31 | }, 32 | { 33 | key: 'order', 34 | name: 'Order' 35 | }, 36 | { 37 | key: 'color', 38 | name: 'Color' 39 | }, 40 | { 41 | key: 'label', 42 | name: 'Label' 43 | } 44 | ], 45 | geneCategories: [ 46 | { 47 | category: 'Color', 48 | genes: [ 49 | ['Hue Range', 'hueRange'], 50 | ['Hue Offset', 'hueOffset'], 51 | ['Saturation Offset', 'saturationOffset'], 52 | ['Saturation Factor', 'saturationValueFactor'], 53 | ['Lightness Offset', 'lightnessOffset'], 54 | ['Lightness Factor', 'lightnessValueFactor'] 55 | ] 56 | }, 57 | { 58 | category: 'X Position', 59 | genes: [ 60 | ['Offset', 'xOffset'], 61 | ['Rank Factor', 'xRankFactor'], 62 | ['Random Factor', 'xRandomFactor'] 63 | ] 64 | }, 65 | { 66 | category: 'Y Position', 67 | genes: [ 68 | ['Offset', 'yOffset'], 69 | ['Rank Factor', 'yRankFactor'], 70 | ['Random Factor', 'yRandomFactor'] 71 | ] 72 | }, 73 | { 74 | category: 'Scale', 75 | genes: [ 76 | ['Offset', 'scaleOffset'], 77 | ['Rank Factor', 'scaleRankFactor'] 78 | ] 79 | } 80 | ], 81 | genes: { 82 | xOffset: 0, 83 | xRankFactor: 0, 84 | xRandomFactor: 0, 85 | 86 | yOffset: 0, 87 | yRankFactor: 0, 88 | yRandomFactor: 0, 89 | 90 | scaleOffset: 0, 91 | scaleRankFactor: 0, 92 | 93 | ...defaultColorValues 94 | } 95 | }; -------------------------------------------------------------------------------- /src/charts/timeline/index.js: -------------------------------------------------------------------------------- 1 | import draw from './draw'; 2 | import randomize from '../../util/randomizeTimeline'; 3 | import { defaultColorValues } from '../geneColor'; 4 | 5 | export default { 6 | key: 'timeline', 7 | name: 'Area Timeline', 8 | preview: require('./thumb.png'), 9 | required: ['x', 'y', 'group', 'time'], 10 | valid: fields => 11 | fields.has('group') && fields.has('time') && 12 | (fields.has('x') || fields.has('y')), 13 | randomize, 14 | draw, 15 | properties: [ 16 | { 17 | key: 'group', 18 | name: 'Group' 19 | }, 20 | { 21 | key: 'time', 22 | name: 'Time' 23 | }, 24 | { 25 | key: 'x', 26 | name: 'Width' 27 | }, 28 | { 29 | key: 'y', 30 | name: 'Height' 31 | }, 32 | { 33 | key: 'order', 34 | name: 'Order' 35 | }, 36 | { 37 | key: 'color', 38 | name: 'Color' 39 | }, 40 | { 41 | key: 'label', 42 | name: 'Label' 43 | } 44 | ], 45 | geneCategories: [ 46 | { 47 | category: 'Color', 48 | genes: [ 49 | ['Hue Range', 'hueRange'], 50 | ['Hue Offset', 'hueOffset'], 51 | ['Saturation Offset', 'saturationOffset'], 52 | ['Saturation Factor', 'saturationValueFactor'], 53 | ['Lightness Offset', 'lightnessOffset'], 54 | ['Lightness Factor', 'lightnessValueFactor'] 55 | ] 56 | }, 57 | { 58 | category: 'X Position', 59 | genes: [ 60 | ['Offset', 'xOffset'], 61 | ['Rank Factor', 'xRankFactor'], 62 | ['Random Factor', 'xRandomFactor'] 63 | ] 64 | }, 65 | { 66 | category: 'Y Position', 67 | genes: [ 68 | ['Offset', 'yOffset'], 69 | ['Rank Factor', 'yRankFactor'], 70 | ['Random Factor', 'yRandomFactor'] 71 | ] 72 | }, 73 | { 74 | category: 'Scale', 75 | genes: [ 76 | ['Offset', 'scaleOffset'], 77 | ['Rank Factor', 'scaleRankFactor'] 78 | ] 79 | } 80 | ], 81 | genes: { 82 | xOffset: 0, 83 | xRankFactor: 0, 84 | xRandomFactor: 0, 85 | 86 | yOffset: 0, 87 | yRankFactor: 0, 88 | yRandomFactor: 0, 89 | 90 | scaleOffset: 0, 91 | scaleRankFactor: 0, 92 | 93 | ...defaultColorValues 94 | } 95 | }; -------------------------------------------------------------------------------- /src/charts/pie/index.js: -------------------------------------------------------------------------------- 1 | import draw from './draw'; 2 | import { defaultColorValues } from '../geneColor'; 3 | 4 | export default { 5 | key: 'pie', 6 | name: 'Pie Chart', 7 | draw, 8 | required: ['size'], 9 | randomize(fields) { 10 | if (!fields.length) { 11 | return {}; 12 | } 13 | const indices = Object.keys(fields).map(parseFloat); 14 | let numericFieldIndices = indices.filter(index => { 15 | const f = fields[index]; 16 | const type = f.type; 17 | return type === 'int' || type === 'float'; 18 | }); 19 | if (!numericFieldIndices.length) { 20 | numericFieldIndices = indices; 21 | } 22 | const count = numericFieldIndices.length; 23 | const size = numericFieldIndices[Math.floor(Math.random() * count) % count]; 24 | return { size }; 25 | }, 26 | preview: require('./thumb.png'), 27 | properties: [ 28 | { 29 | key: 'size', 30 | name: 'Slice Angle' 31 | }, 32 | { 33 | key: 'order', 34 | name: 'Order' 35 | }, 36 | { 37 | key: 'color', 38 | name: 'Color' 39 | }, 40 | { 41 | key: 'label', 42 | name: 'Label' 43 | } 44 | ], 45 | geneCategories: [ 46 | { 47 | category: 'Color', 48 | genes: [ 49 | ['Hue Range', 'hueRange'], 50 | ['Hue Offset', 'hueOffset'], 51 | ['Saturation Offset', 'saturationOffset'], 52 | ['Saturation Factor', 'saturationValueFactor'], 53 | ['Lightness Offset', 'lightnessOffset'], 54 | ['Lightness Factor', 'lightnessValueFactor'] 55 | ] 56 | }, 57 | { 58 | category: 'Radius', 59 | genes: [ 60 | ['Offset', 'radiusOffset'], 61 | ['Rank Factor', 'radiusRankFactor'], 62 | ['Value Factor', 'radiusValueFactor'], 63 | ['Random Factor', 'radiusRandomFactor'] 64 | ] 65 | }/*, 66 | { 67 | category: 'Rotation', 68 | genes: [ 69 | ['Offset', 'rotationOffset'], 70 | ['Rank Factor', 'rotationRankFactor'], 71 | ['Value Factor', 'rotationValueFactor'], 72 | ['Random Factor', 'rotationRandomFactor'] 73 | ] 74 | }*/ 75 | ], 76 | genes: { 77 | ...defaultColorValues, 78 | 79 | radiusOffset: 0, 80 | radiusRankFactor: 0, 81 | radiusValueFactor: 0, 82 | radiusRandomFactor: 0, 83 | 84 | // scale: 1, 85 | // scaleFactor: 0, 86 | 87 | rotationOffset: 0, 88 | rotationRankFactor: 0, 89 | rotationValueFactor: 0, 90 | rotationRandomFactor: 0 91 | } 92 | }; -------------------------------------------------------------------------------- /src/data/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | title: 'Largest cities in the world', 4 | source: 'https://en.wikipedia.org/wiki/List_of_largest_cities', 5 | license: 'Creative Commons Attribution-ShareAlike 3.0', 6 | licenseLink: 'https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License', 7 | load: () => import('./large-cities.json') 8 | }, 9 | { 10 | title: `Fisher's Iris data set`, 11 | description: `Fisher's Iris data set\nDua, D. and Karra Taniskidou, E. (2017). UCI Machine Learning Repository Irvine, CA: University of California, School of Information and Computer Science.`, 12 | source: 'http://archive.ics.uci.edu/ml/datasets/Iris', 13 | load: () => import('./iris.json') 14 | }, 15 | { 16 | title: 'European Parliament election, 2004', 17 | source: 'https://en.wikipedia.org/wiki/European_Parliament_election,_2004', 18 | license: 'Creative Commons Attribution-ShareAlike 3.0', 19 | licenseLink: 'https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License', 20 | load: () => import('./european-parliament-2004.json') 21 | }, 22 | { 23 | title: 'Honey Production in the USA (1998-2012)', 24 | description: 'Honey Production Figures and Prices by National Agricultural Statistics Service', 25 | source: 'https://www.kaggle.com/jessicali9530/honey-production', 26 | license: 'CC0: Public Domain', 27 | licenseLink: 'https://creativecommons.org/publicdomain/zero/1.0/', 28 | load: () => import('./honeyproduction.json') 29 | }, 30 | { 31 | title: 'Mathematical Number Sequences', 32 | description: 'Primes, powers of two, powers of ten, Fibonacci sequence', 33 | load: () => import('./number-sequences.json') 34 | }, 35 | { 36 | title: 'World Happiness Report', 37 | source: 'https://www.kaggle.com/unsdsn/world-happiness', 38 | description: 'Happiness scored according to economic production, social support, etc.', 39 | license: 'CC0: Public Domain', 40 | licenseLink: 'https://creativecommons.org/publicdomain/zero/1.0/', 41 | load: () => import('./world-happiness.json') 42 | }/*, 43 | { 44 | title: 'FIFA World Cup', 45 | source: 'https://www.kaggle.com/abecklas/fifa-world-cup', 46 | description: '', 47 | license: 'CC0: Public Domain', 48 | licenseLink: 'https://creativecommons.org/publicdomain/zero/1.0/', 49 | load: () => import('./fifa-world-cup.json') 50 | }*/ 51 | ]; 52 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Morph 2 | 3 | Thank you for taking the time to contribute to Morph! 4 | 5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests. 6 | 7 | ## Table Of Contents 8 | 9 | [Code of Conduct](#code-of-conduct) 10 | 11 | [I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question) 12 | 13 | [What should I know before I get started?](#what-should-i-know-before-i-get-started) 14 | 15 | [How Can I Contribute?](#how-can-i-contribute) 16 | - [Reporting Bugs](#reporting-bugs) 17 | - [Suggesting Enhancements](#suggesting-enhancements) 18 | - [Your First Code Contribution](#your-first-code-contribution) 19 | - [Pull Requests](#pull-requests) 20 | 21 | [Styleguides](#styleguides) 22 | - [Git Commit Messages](#git-commit-messages) 23 | - [JavaScript Styleguide](#javascript-styleguide) 24 | 25 | [Additional Notes](#additional-notes) 26 | - [Issue and Pull Request Labels](#issue-and-pull-request-labels) 27 | 28 | ## Code of Conduct 29 | 30 | This project and everyone participating in it is governed by a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [contact@datavized.com](mailto:contact@datavized.com). 31 | 32 | ## Styleguides 33 | 34 | ### Git Commit Messages 35 | 36 | - Use the present tense ("Add feature" not "Added feature") 37 | - Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 38 | - Limit the first line to 72 characters or less 39 | - Reference issues and pull requests liberally after the first line 40 | - Consider starting the commit message with an applicable emoji: 41 | - :art: `:art:` when improving the format/structure of the code 42 | - :racehorse: `:racehorse:` when improving performance 43 | - :non-potable_water: `:non-potable_water:` when plugging memory leaks 44 | - :memo: `:memo:` when writing docs 45 | - :bug: `:bug:` when fixing a bug 46 | - :fire: `:fire:` when removing code or files 47 | - :white_check_mark: `:white_check_mark:` when adding tests 48 | - :lock: `:lock:` when dealing with security 49 | - :arrow_up: `:arrow_up:` when upgrading dependencies 50 | - :arrow_down: `:arrow_down:` when downgrading dependencies 51 | - :shirt: `:shirt:` when removing linter warnings 52 | -------------------------------------------------------------------------------- /src/components/ConfirmationDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; // import classNames from 'classnames'; 2 | import { confirmable } from 'react-confirm'; 3 | 4 | /* 5 | Material UI components 6 | */ 7 | import PropTypes from 'prop-types'; 8 | // import withStyles from '@material-ui/core/styles/withStyles'; 9 | import Theme from './Theme'; 10 | import Dialog from '@material-ui/core/Dialog'; 11 | import DialogActions from '@material-ui/core/DialogActions'; 12 | import DialogContent from '@material-ui/core/DialogContent'; 13 | import DialogContentText from '@material-ui/core/DialogContentText'; 14 | import Button from '@material-ui/core/Button'; 15 | 16 | // const styles = () => ({ 17 | // root: { 18 | // } 19 | // }); 20 | 21 | const Def = class ConfirmationDialog extends React.Component { 22 | static propTypes = { 23 | show: PropTypes.bool, // from confirmable. indicates if the dialog is shown or not. 24 | proceed: PropTypes.func, // from confirmable. call to close the dialog with promise resolved. 25 | cancel: PropTypes.func, // from confirmable. call to close the dialog with promise rejected. 26 | dismiss: PropTypes.func, // from confirmable. call to only close the dialog. 27 | confirmation: PropTypes.string, // arguments of your confirm function 28 | options: PropTypes.object, // arguments of your confirm function 29 | 30 | classes: PropTypes.object 31 | } 32 | 33 | render() { 34 | const { 35 | classes, 36 | show, 37 | proceed, 38 | // dismiss, 39 | cancel, 40 | confirmation, 41 | options 42 | } = this.props; 43 | 44 | const yes = options && options.yes || 'Yes'; 45 | const no = options && options.no || 'No'; 46 | 47 | return 54 | {/*{"Use Google's location service?"}*/} 55 | 56 | 57 | {confirmation} 58 | 59 | 60 | 61 | 64 | 67 | 68 | ; 69 | } 70 | }; 71 | 72 | // const ConfirmationDialog = confirmable(withStyles(styles)(Def)); 73 | const ConfirmationDialog = confirmable(Def); 74 | export default ConfirmationDialog; -------------------------------------------------------------------------------- /src/util/Quadtree.js: -------------------------------------------------------------------------------- 1 | import Quadtree from 'exports-loader?Quadtree!quadtree-js'; 2 | 3 | function contains(outer, inner) { 4 | const outerRight = outer.x + outer.width; 5 | const outerBottom = outer.y + outer.height; 6 | const innerRight = inner.x + inner.width; 7 | const innerBottom = inner.y + inner.height; 8 | 9 | return inner.x >= outer.x && inner.y >= outer.y && innerRight < outerRight && innerBottom < outerBottom; 10 | } 11 | 12 | function Q(bounds, maxObjects, maxLevels) { 13 | const root = new Quadtree(bounds, maxObjects, maxLevels); 14 | 15 | this.getIndex = pRect => root.getIndex(pRect); 16 | this.getAll = () => root.getAll(); 17 | this.getObjectNode = obj => root.getObjectNode(obj); 18 | this.removeObject = obj => root.removeObject(obj); 19 | this.clear = () => root.clear(); 20 | this.cleanup = () => root.cleanup(); 21 | 22 | function retrieveByFunction(node, check) { 23 | if (!check(node.bounds)) { 24 | return null; 25 | } 26 | 27 | let objects = node.objects.filter(check); 28 | node.nodes.forEach(child => { 29 | const childObjects = child && retrieveByFunction(child, check); 30 | if (childObjects && childObjects.length) { 31 | objects = objects.concat(childObjects); 32 | } 33 | }); 34 | return objects; 35 | } 36 | 37 | this.retrieve = bounds => { 38 | if (typeof bounds === 'function') { 39 | return retrieveByFunction(root, bounds) || []; 40 | } 41 | 42 | const boundsRight = bounds.x + bounds.width; 43 | const boundsBottom = bounds.y + bounds.height; 44 | return root.retrieve(bounds).filter(obj => { 45 | const objRight = obj.x + obj.width; 46 | const objBottom = obj.y + obj.height; 47 | return obj.x < boundsRight && obj.y < boundsBottom && objRight >= bounds.x && objBottom >= bounds.y; 48 | }); 49 | }; 50 | 51 | this.insert = obj => { 52 | const bounds = root.bounds; 53 | 54 | if (!contains(bounds, obj)) { 55 | const halfWidth = bounds.width / 2; 56 | const halfHeight = bounds.height / 2; 57 | const cx = bounds.x + halfWidth; 58 | const cy = bounds.y + halfHeight; 59 | 60 | bounds.x = Math.min(cx - bounds.width, obj.x); 61 | bounds.y = Math.min(cy - bounds.height, obj.y); 62 | 63 | const newRight = Math.max(obj.x + obj.width, cx + bounds.width); 64 | bounds.width = newRight - bounds.x; 65 | 66 | const newBottom = Math.max(obj.y + obj.height, cy + bounds.height); 67 | bounds.height = newBottom - bounds.y; 68 | 69 | if (root.nodes.length) { 70 | root.cleanup(); 71 | } 72 | } 73 | 74 | root.insert(obj); 75 | }; 76 | } 77 | 78 | export default Q; -------------------------------------------------------------------------------- /src/drawing/Drawable.js: -------------------------------------------------------------------------------- 1 | /* 2 | This PIXI Transform object is pretty good, 3 | so let's borrow it for now 4 | */ 5 | import { Transform } from '@pixi/math'; 6 | import eventEmitter from 'event-emitter'; 7 | import allOff from 'event-emitter/all-off'; 8 | 9 | let id = 0; 10 | 11 | function Drawable() { 12 | eventEmitter(this); 13 | 14 | this.data = {}; 15 | this.id = id++; 16 | this.children = []; 17 | this.transform = new Transform(); 18 | this.visible = true; 19 | this.opacity = 1; 20 | 21 | this.update = () => { 22 | /* eslint-disable no-underscore-dangle */ 23 | const needsUpdate = this.transform._localID !== this.transform._currentLocalID; 24 | /* eslint-enable no-underscore-dangle */ 25 | this.transform.updateLocalTransform(); 26 | if (needsUpdate) { 27 | this.emit('update'); 28 | } 29 | }; 30 | 31 | this.add = child => { 32 | this.remove(child); 33 | this.children.push(child); 34 | this.emit('add', child); 35 | }; 36 | 37 | this.remove = child => { 38 | const index = this.children.indexOf(child); 39 | if (index >= 0) { 40 | this.children.splice(index, 1); 41 | this.emit('remove', child); 42 | } 43 | }; 44 | 45 | this.clear = () => { 46 | while (this.children.length) { 47 | this.remove(this.children[0]); 48 | } 49 | }; 50 | 51 | this.destroy = () => { 52 | this.emit('destroy'); 53 | allOff(this); 54 | }; 55 | 56 | this.destroyChildren = () => { 57 | while (this.children.length) { 58 | const child = this.children[0]; 59 | this.remove(child); 60 | child.destroyAll(); 61 | } 62 | }; 63 | 64 | this.destroyAll = () => { 65 | this.destroyChildren(); 66 | this.destroy(); 67 | }; 68 | 69 | Object.defineProperties(this, { 70 | position: { 71 | get: () => this.transform.position 72 | }, 73 | x: { 74 | get: () => this.transform.position.x, 75 | set: val => this.transform.position.x = val 76 | }, 77 | y: { 78 | get: () => this.transform.position.y, 79 | set: val => this.transform.position.y = val 80 | }, 81 | rotation: { 82 | get: () => this.transform.rotation, 83 | set: val => this.transform.rotation = val 84 | }, 85 | scale: { 86 | get: () => this.transform.scale 87 | }, 88 | pivot: { 89 | get: () => this.transform.pivot 90 | }, 91 | skew: { 92 | get: () => this.transform.skew 93 | }, 94 | matrix: { 95 | get: () => { 96 | this.update(); 97 | return this.transform.localTransform; 98 | } 99 | } 100 | }); 101 | } 102 | 103 | export default Drawable; -------------------------------------------------------------------------------- /src/data/european-parliament-2004.json: -------------------------------------------------------------------------------- 1 | { 2 | "fields": [ 3 | { 4 | "name": "Group", 5 | "type": "string" 6 | }, 7 | { 8 | "name": "Description", 9 | "type": "string" 10 | }, 11 | { 12 | "name": "Chaired by", 13 | "type": "string" 14 | }, 15 | { 16 | "name": "Seats", 17 | "type": "int", 18 | "format": "General", 19 | "min": 27, 20 | "max": 268, 21 | "scale": 0.0037313432835820895 22 | } 23 | ], 24 | "rows": [ 25 | [ 26 | "EPP-ED", 27 | "Conservatives and Christian Democrats", 28 | "Hans-Gert Pöttering", 29 | 268 30 | ], 31 | [ 32 | "PES", 33 | "Social Democrats", 34 | "Martin Schulz", 35 | 200 36 | ], 37 | [ 38 | "ALDE", 39 | "Liberals and Liberal Democrats", 40 | "Graham Watson", 41 | 88 42 | ], 43 | [ 44 | "G–EFA", 45 | "Greens and Regionalists", 46 | "Daniel Cohn-Bendit, Monica Frassoni", 47 | 42 48 | ], 49 | [ 50 | "EUL–NGL", 51 | "Communists, Democratic Socialists and the Far left", 52 | "Francis Wurtz", 53 | 41 54 | ], 55 | [ 56 | "ID", 57 | "Eurosceptics", 58 | "Jens-Peter Bonde, Nigel Farage", 59 | 37 60 | ], 61 | [ 62 | "UEN", 63 | "National Conservatives", 64 | "Brian Crowley, Cristiana Muscardini", 65 | 27 66 | ], 67 | [ 68 | "NI", 69 | "Independents and Far right", 70 | "none", 71 | 29 72 | ] 73 | ], 74 | "normalized": [ 75 | [ 76 | 0.14285714285714285, 77 | 0.14285714285714285, 78 | 0.5714285714285714, 79 | 1 80 | ], 81 | [ 82 | 0.8571428571428571, 83 | 1, 84 | 0.8571428571428571, 85 | 0.7462686567164178 86 | ], 87 | [ 88 | 0, 89 | 0.7142857142857143, 90 | 0.42857142857142855, 91 | 0.3283582089552239 92 | ], 93 | [ 94 | 0.42857142857142855, 95 | 0.42857142857142855, 96 | 0.14285714285714285, 97 | 0.15671641791044777 98 | ], 99 | [ 100 | 0.2857142857142857, 101 | 0, 102 | 0.2857142857142857, 103 | 0.15298507462686567 104 | ], 105 | [ 106 | 0.5714285714285714, 107 | 0.2857142857142857, 108 | 0.7142857142857143, 109 | 0.13805970149253732 110 | ], 111 | [ 112 | 1, 113 | 0.8571428571428571, 114 | 0, 115 | 0.10074626865671642 116 | ], 117 | [ 118 | 0.7142857142857143, 119 | 0.5714285714285714, 120 | 1, 121 | 0.10820895522388059 122 | ] 123 | ] 124 | } -------------------------------------------------------------------------------- /src/components/SampleDataDialog.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 Button from '@material-ui/core/Button'; 10 | import MenuList from '@material-ui/core/MenuList'; 11 | import LinearProgress from '@material-ui/core/LinearProgress'; 12 | 13 | import SampleMenuItem from './SampleMenuItem'; 14 | import OverwriteWarning from './OverwriteWarning'; 15 | 16 | import sampleData from '../data'; 17 | 18 | const styles = () => ({ 19 | dialog: { 20 | minWidth: '35%', 21 | maxHeight: '60%', 22 | width: 'min-content', 23 | maxWidth: '90%' 24 | }, 25 | dialogContent: { 26 | display: 'flex', 27 | flexDirection: 'column' 28 | }, 29 | dialogListContainer: { 30 | display: 'block', 31 | overflow: 'auto' 32 | } 33 | }); 34 | 35 | const Def = ({ 36 | classes, 37 | open, 38 | onClose, 39 | loadSampleData, 40 | waiting, 41 | overwriteWarning 42 | }) => { 43 | 44 | return 55 | { waiting ? 56 | 57 | Loading... 58 | 59 | 60 | : 61 | 62 | Select Sample Data 63 | 64 |
65 | {/**/} 66 | 67 | {sampleData.map(({load, ...metadata}, i) => 68 | loadSampleData(load, metadata)} {...metadata}/> 69 | )} 70 | 71 |
72 | { overwriteWarning ? : null } 73 | 74 | 75 | 76 |
77 |
78 | } 79 |
; 80 | }; 81 | 82 | Def.propTypes = { 83 | classes: PropTypes.object.isRequired, 84 | open: PropTypes.bool, 85 | onClose: PropTypes.func.isRequired, 86 | loadSampleData: PropTypes.func.isRequired, 87 | overwriteWarning: PropTypes.bool, 88 | waiting: PropTypes.bool 89 | }; 90 | const SampleDataDialog = withStyles(styles)(Def); 91 | export default SampleDataDialog; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | <%= htmlWebpackPlugin.options.title %> 17 | <% if (htmlWebpackPlugin.options.description) { %> 18 | 19 | <% } %> 20 | 67 | 68 | 69 | 72 |
73 | 74 | 75 | -------------------------------------------------------------------------------- /src/util/touch-mouse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simulate a mouse event based on a corresponding touch event 3 | * @param {Object} event A touch event 4 | * @param {String} simulatedType The corresponding mouse event 5 | */ 6 | function simulateMouseEvent(event, simulatedType) { 7 | 8 | // Ignore multi-touch events 9 | if (event.touches.length > 1) { 10 | return; 11 | } 12 | 13 | event.preventDefault(); 14 | 15 | const touch = event.changedTouches[0]; 16 | const simulatedEvent = document.createEvent('MouseEvents'); 17 | 18 | // Initialize the simulated mouse event using the touch event's coordinates 19 | simulatedEvent.initMouseEvent( 20 | simulatedType, // type 21 | true, // bubbles 22 | true, // cancelable 23 | window, // view 24 | 1, // detail 25 | touch.screenX, // screenX 26 | touch.screenY, // screenY 27 | touch.clientX, // clientX 28 | touch.clientY, // clientY 29 | false, // ctrlKey 30 | false, // altKey 31 | false, // shiftKey 32 | false, // metaKey 33 | 0, // button 34 | null // relatedTarget 35 | ); 36 | 37 | // Dispatch the simulated event to the target element 38 | event.target.dispatchEvent(simulatedEvent); 39 | } 40 | 41 | export default function handleTouch(element) { 42 | let touchHandled = false; 43 | let touchMoved = false; 44 | 45 | element.addEventListener('touchstart', event => { 46 | if (touchHandled) { 47 | // todo?: !self._mouseCapture(event.changedTouches[0]) 48 | return; 49 | } 50 | 51 | // Set the flag to prevent other widgets from inheriting the touch event 52 | touchHandled = true; 53 | 54 | // Track movement to determine if interaction was a click 55 | touchMoved = false; 56 | 57 | // Simulate the mouseover event 58 | simulateMouseEvent(event, 'mouseover'); 59 | 60 | // Simulate the mousemove event 61 | simulateMouseEvent(event, 'mousemove'); 62 | 63 | // Simulate the mousedown event 64 | simulateMouseEvent(event, 'mousedown'); 65 | }); 66 | 67 | element.addEventListener('touchmove', event => { 68 | // Ignore event if not handled 69 | if (!touchHandled) { 70 | return; 71 | } 72 | 73 | // Interaction was not a click 74 | touchMoved = true; 75 | 76 | // Simulate the mousemove event 77 | simulateMouseEvent(event, 'mousemove'); 78 | }); 79 | 80 | element.addEventListener('touchend', event => { 81 | // Ignore event if not handled 82 | if (!touchHandled) { 83 | return; 84 | } 85 | 86 | // Simulate the mouseup event 87 | simulateMouseEvent(event, 'mouseup'); 88 | 89 | // Simulate the mouseout event 90 | simulateMouseEvent(event, 'mouseout'); 91 | 92 | // If the touch interaction did not move, it should trigger a click 93 | if (!touchMoved) { 94 | 95 | // Simulate the click event 96 | simulateMouseEvent(event, 'click'); 97 | } 98 | 99 | // Unset the flag to allow other widgets to inherit the touch event 100 | touchHandled = false; 101 | }); 102 | } -------------------------------------------------------------------------------- /src/charts/scatter/index.js: -------------------------------------------------------------------------------- 1 | import draw from './draw'; 2 | import { defaultColorValues } from '../geneColor'; 3 | 4 | export default { 5 | key: 'scatter', 6 | name: 'Scatter Plot', 7 | preview: require('./thumb.png'), 8 | draw, 9 | required: ['area', 'x', 'y'], 10 | valid: fields => 11 | fields.has('area') || 12 | fields.has('x') || 13 | fields.has('y'), 14 | randomize(fields) { 15 | if (!fields.length) { 16 | return {}; 17 | } 18 | 19 | const horizontal = Math.random() > 0.5; 20 | const posField = horizontal ? 'y' : 'x'; 21 | 22 | const fieldNames = [posField, 'area']; 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: 'area', 47 | name: 'Area' 48 | }, 49 | { 50 | key: 'x', 51 | name: 'X Position' 52 | }, 53 | { 54 | key: 'y', 55 | name: 'Y Position' 56 | }, 57 | { 58 | key: 'order', 59 | name: 'Order' 60 | }, 61 | { 62 | key: 'color', 63 | name: 'Color' 64 | }, 65 | { 66 | key: 'label', 67 | name: 'Label' 68 | } 69 | ], 70 | geneCategories: [ 71 | { 72 | category: 'Color', 73 | genes: [ 74 | ['Hue Range', 'hueRange'], 75 | ['Hue Offset', 'hueOffset'], 76 | ['Saturation Offset', 'saturationOffset'], 77 | ['Saturation Factor', 'saturationValueFactor'], 78 | ['Lightness Offset', 'lightnessOffset'], 79 | ['Lightness Factor', 'lightnessValueFactor'] 80 | ] 81 | }, 82 | { 83 | category: 'Area', 84 | genes: [ 85 | ['Offset', 'areaOffset']//, 86 | // ['Rank Factor', 'areaRankFactor'], 87 | // ['Random Factor', 'areaRandomFactor'] 88 | ] 89 | }, 90 | { 91 | category: 'X Position', 92 | genes: [ 93 | ['Offset', 'xOffset'], 94 | ['Rank Factor', 'xRankFactor'], 95 | ['Random Factor', 'xRandomFactor'] 96 | ] 97 | }, 98 | { 99 | category: 'Y Position', 100 | genes: [ 101 | ['Offset', 'yOffset'], 102 | ['Rank Factor', 'yRankFactor'], 103 | ['Random Factor', 'yRandomFactor'] 104 | ] 105 | } 106 | ], 107 | genes: { 108 | areaOffset: -0.5, 109 | areaRankFactor: 0, 110 | areaRandomFactor: 0, 111 | 112 | xOffset: 0, 113 | xRankFactor: 0, 114 | xRandomFactor: 0, 115 | 116 | yOffset: 0, 117 | yRankFactor: 0, 118 | yRandomFactor: 0, 119 | 120 | ...defaultColorValues 121 | } 122 | }; -------------------------------------------------------------------------------- /src/drawing/PixiPath.js: -------------------------------------------------------------------------------- 1 | import PixiDrawable from './PixiDrawable'; 2 | import Path from './Path'; 3 | 4 | import ShapeShader from './shader'; 5 | import PathGeometry from './shapes/PathGeometry'; 6 | import setColorArray from '../util/setColorArray'; 7 | 8 | import { Container } from '@pixi/display'; 9 | import { Renderer } from '@pixi/core'; 10 | import { MeshRenderer, RawMesh } from '@pixi/mesh'; 11 | Renderer.registerPlugin('mesh', MeshRenderer); 12 | 13 | const geometries = new Map(); 14 | 15 | const GEOMETRY_LIFETIME = 10000; 16 | let cleanTimeout = 0; 17 | function pruneGeometries() { 18 | const now = Date.now(); 19 | let next = Infinity; 20 | geometries.forEach((geo, path) => { 21 | if (geo.count <= 0) { 22 | if (now - geo.latest >= GEOMETRY_LIFETIME) { 23 | geo.geometry.destroy(); 24 | geometries.delete(path); 25 | } else { 26 | next = Math.min(next, geo.latest); 27 | } 28 | } 29 | }); 30 | 31 | if (geometries.size && next < Infinity) { 32 | cleanTimeout = setTimeout(pruneGeometries, next - now); 33 | } else { 34 | cleanTimeout = 0; 35 | } 36 | } 37 | 38 | function getGeometry(path) { 39 | let geo; 40 | if (geometries.has(path)) { 41 | geo = geometries.get(path); 42 | } else { 43 | geo = { 44 | geometry: new PathGeometry(path), 45 | count: 0, 46 | latest: 0 47 | }; 48 | geometries.set(path, geo); 49 | } 50 | geo.latest = Date.now(); 51 | geo.count++; 52 | return geo.geometry; 53 | } 54 | 55 | function releaseGeometry(path) { 56 | const geo = geometries.get(path); 57 | if (geo) { 58 | geo.count--; 59 | if (!cleanTimeout && geo.count <= 0) { 60 | cleanTimeout = setTimeout(pruneGeometries, GEOMETRY_LIFETIME); 61 | } 62 | } 63 | } 64 | 65 | function PixiPath(parent, drawable) { 66 | PixiDrawable.call(this, parent, drawable); 67 | 68 | const path = drawable && drawable.path; 69 | 70 | const superUpdate = this.update; 71 | this.update = () => { 72 | // create mesh or container if haven't done it yet 73 | if (!this.pixiObject) { 74 | if (path) { 75 | const geometry = path && getGeometry(path); 76 | const shader = geometry && new ShapeShader(); 77 | this.pixiObject = new RawMesh(geometry, shader); 78 | } else { 79 | this.pixiObject = new Container(); 80 | } 81 | if (parent) { 82 | parent.addChild(this.pixiObject); 83 | } 84 | } 85 | 86 | const drawable = this.drawable; 87 | const pixiObject = this.pixiObject; 88 | if (drawable) { 89 | // set color uniform 90 | if (this.pixiObject.shader) { 91 | const { uColor: color } = pixiObject.shader.uniforms; 92 | setColorArray(color, drawable.color, drawable.opacity); 93 | } 94 | } 95 | superUpdate.call(this); 96 | }; 97 | 98 | const superDestroy = this.destroy; 99 | this.destroy = () => { 100 | if (path) { 101 | releaseGeometry(path); 102 | } 103 | 104 | superDestroy(); 105 | }; 106 | } 107 | 108 | PixiPath.prototype = Object.create(PixiDrawable.prototype); 109 | PixiDrawable.register(Path, PixiPath); 110 | 111 | export default PixiPath; -------------------------------------------------------------------------------- /src/components/ColorPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SketchPicker from 'react-color/lib/components/sketch/Sketch'; 3 | 4 | /* 5 | Material UI components 6 | */ 7 | import PropTypes from 'prop-types'; 8 | import withStyles from '@material-ui/core/styles/withStyles'; 9 | 10 | const styles = () => ({ 11 | color: { 12 | width: 36, 13 | height: 14, 14 | borderRadius: 2 15 | }, 16 | swatch: { 17 | padding: 5, 18 | background: '#fff', 19 | borderRadius: 1, 20 | boxShadow: '0 0 0 1px rgba(0,0,0,.1)', 21 | display: 'inline-block', 22 | cursor: 'pointer' 23 | }, 24 | popover: { 25 | position: 'absolute', 26 | zIndex: 2 27 | }, 28 | cover: { 29 | position: 'fixed', 30 | top: 0, 31 | right: 0, 32 | bottom: 0, 33 | left: 0 34 | } 35 | }); 36 | 37 | const Def = class ColorPicker extends React.Component { 38 | state = { 39 | displayColorPicker: false, 40 | color: { 41 | r: 255, 42 | g: 255, 43 | b: 255, 44 | a: '1' 45 | } 46 | } 47 | 48 | static propTypes = { 49 | classes: PropTypes.object.isRequired, 50 | className: PropTypes.string, 51 | color: PropTypes.object, 52 | onChange: PropTypes.func, 53 | disableAlpha: PropTypes.bool 54 | } 55 | 56 | static defaultProps = { 57 | forwardDisabled: false 58 | } 59 | 60 | // eslint-disable-next-line camelcase 61 | UNSAFE_componentWillMount() { 62 | if (this.props.color) { 63 | this.setState({ 64 | color: this.props.color 65 | }); 66 | } 67 | } 68 | 69 | // eslint-disable-next-line camelcase 70 | UNSAFE_componentWillReceiveProps(newProps) { 71 | if (newProps.color) { 72 | this.setState({ 73 | color: newProps.color 74 | }); 75 | } 76 | } 77 | 78 | handleClick = () => { 79 | this.setState({ displayColorPicker: !this.state.displayColorPicker }); 80 | } 81 | 82 | handleClose = () => { 83 | this.setState({ displayColorPicker: false }); 84 | } 85 | 86 | handleChange = color => { 87 | this.setState({ color: color.rgb }); 88 | if (this.props.onChange) { 89 | this.props.onChange(color.rgb); 90 | } 91 | } 92 | 93 | render() { 94 | const { 95 | classes, 96 | disableAlpha 97 | } = this.props; 98 | 99 | const { 100 | color, 101 | displayColorPicker 102 | } = this.state; 103 | 104 | const alpha = disableAlpha ? 1 : color.a; 105 | const backgroundColor = `rgba(${color.r}, ${color.g}, ${color.b}, ${alpha})`; 106 | 107 | return
108 |
109 |
110 |
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 | {{chart.name}/} 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 | About Morph 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 | 67 | 68 | 69 | 70 | 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 | 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 | 139 |
140 | 141 | 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 |

morph-header

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 ? : null} 245 | 254 |
255 | {controlsExpanded || !isCompact ? children : null} 256 |
257 |
258 | 269 |
270 |
271 |
272 | {navigation} 273 |
; 274 | } 275 | }; 276 | 277 | const NodeInspector = withStyles(styles, { withTheme: true })(Def); 278 | export default NodeInspector; --------------------------------------------------------------------------------