├── .prettierrc ├── legacy ├── .gitignore ├── src │ ├── shader │ │ ├── line-frag.glsl │ │ ├── line-vert.glsl │ │ ├── circle-frag.glsl │ │ └── circle-vert.glsl │ ├── testMolecule.json │ ├── infopanel.pug │ ├── index.pug │ └── DataProvider.js ├── README.md ├── data │ ├── rescale.py │ ├── layout.js │ ├── yearzones.py │ └── process.py ├── webpack.config.babel.js └── package.json ├── joss ├── fig1.png ├── paper.bib └── paper.md ├── src ├── components │ ├── graph-vis │ │ ├── lineup.css │ │ ├── sort.js │ │ ├── structure.js │ │ ├── RotatedPin.js │ │ ├── node-size-legend.js │ │ ├── node-color-legend.js │ │ ├── info-block.js │ │ ├── layouts.js │ │ ├── PinnedNode.js │ │ ├── table.js │ │ ├── filters.js │ │ ├── SplitContainer.js │ │ ├── info-panel.js │ │ ├── tooltip.js │ │ └── index.js │ ├── header │ │ ├── tri_logo.png │ │ ├── tri_logo_dark.png │ │ └── AboutPage.js │ ├── legend │ │ ├── index.js │ │ ├── LegendCircle.js │ │ ├── LegendGradient.js │ │ └── SizeLegend.js │ └── controls │ │ ├── grid.js │ │ ├── checkbox.js │ │ ├── number.js │ │ ├── slider.js │ │ ├── select.js │ │ ├── rangeslider.js │ │ └── reactselect.js ├── datasets │ ├── precise │ │ ├── templates │ │ │ ├── index.js │ │ │ ├── tooltip.js │ │ │ ├── minimal.js │ │ │ └── material.js │ │ ├── index.js │ │ ├── sizes.js │ │ └── colors.js │ ├── similarity │ │ ├── templates │ │ │ ├── index.js │ │ │ ├── tooltip.js │ │ │ ├── minimal.js │ │ │ └── material.js │ │ ├── colors.js │ │ ├── sizes.js │ │ └── index.js │ ├── index.js │ ├── cooccurence.js │ ├── default.js │ └── utils.js ├── data-provider │ ├── parse.js │ ├── graph.js │ └── index.js ├── store │ └── format.js ├── App.test.js ├── scene-manager │ └── shader │ │ ├── line-vert.glsl │ │ ├── line-frag.glsl │ │ ├── circle-highlight-frag.glsl │ │ ├── circle-frag.glsl │ │ ├── circle-vert.glsl │ │ └── circle-highlight-vert.glsl ├── rest │ └── index.js ├── index.css ├── App.css ├── index.js ├── utils │ └── webcomponents.js ├── logo.svg ├── worker │ └── index.js ├── layout.worker.js ├── serviceWorker.js └── App.js ├── public ├── favicon.ico ├── manifest.json └── index.html ├── images ├── example-figA.png ├── example-figB.png ├── example-fig1A.png └── example-fig1B.png ├── .prettierignore ├── scripts ├── sample_data.sh ├── deploy_s3.sh ├── test.js ├── start.js └── build.js ├── config ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── env.js └── webpackDevServer.config.js ├── data ├── json2gephi.py ├── graphml2json.py ├── edge-sample.py ├── co-occurence │ ├── convert.py │ └── convert.ipynb ├── convert_structures.ipynb └── similarity.py ├── .gitignore ├── CONTRIBUTING.md ├── .github └── workflows │ └── main.yml └── package.json /.prettierrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /legacy/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | src/edges.json 4 | -------------------------------------------------------------------------------- /joss/fig1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRI-AMDD/materialnet/HEAD/joss/fig1.png -------------------------------------------------------------------------------- /src/components/graph-vis/lineup.css: -------------------------------------------------------------------------------- 1 | .lu-wrapper { 2 | color: black; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRI-AMDD/materialnet/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /images/example-figA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRI-AMDD/materialnet/HEAD/images/example-figA.png -------------------------------------------------------------------------------- /images/example-figB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRI-AMDD/materialnet/HEAD/images/example-figB.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | /node_modules 3 | /build 4 | /dist 5 | /public 6 | /config 7 | /legacy 8 | /scripts 9 | -------------------------------------------------------------------------------- /images/example-fig1A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRI-AMDD/materialnet/HEAD/images/example-fig1A.png -------------------------------------------------------------------------------- /images/example-fig1B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRI-AMDD/materialnet/HEAD/images/example-fig1B.png -------------------------------------------------------------------------------- /src/components/header/tri_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRI-AMDD/materialnet/HEAD/src/components/header/tri_logo.png -------------------------------------------------------------------------------- /src/components/header/tri_logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TRI-AMDD/materialnet/HEAD/src/components/header/tri_logo_dark.png -------------------------------------------------------------------------------- /src/datasets/precise/templates/index.js: -------------------------------------------------------------------------------- 1 | import material from './material'; 2 | import minimal from './minimal'; 3 | 4 | export default [material, minimal]; 5 | -------------------------------------------------------------------------------- /src/datasets/similarity/templates/index.js: -------------------------------------------------------------------------------- 1 | import material from './material'; 2 | import minimal from './minimal'; 3 | 4 | export default [material, minimal]; 5 | -------------------------------------------------------------------------------- /src/components/legend/index.js: -------------------------------------------------------------------------------- 1 | export { default as LegendCircle } from './LegendCircle'; 2 | export { default as LegendGradient } from './LegendGradient'; 3 | export { default as SizeLegend } from './SizeLegend'; 4 | -------------------------------------------------------------------------------- /scripts/sample_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | girder-cli --host data.kitware.com download --parent-type item 5c99179c8d777f072bd8b6c1 public/ 3 | tar xfv public/sample-data.tar.gz -C public/ 4 | rm public/sample-data.tar.gz 5 | -------------------------------------------------------------------------------- /src/data-provider/parse.js: -------------------------------------------------------------------------------- 1 | export function extractElements(compound) { 2 | // for now use the following logic: 3 | // split by Captial letters, ignore numbers 4 | const r = /([A-Z][a-z]?)/g; 5 | return compound.match(r) || []; 6 | } 7 | -------------------------------------------------------------------------------- /src/datasets/precise/templates/tooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | 4 | export default function renderTooltip(node, _store) { 5 | return {node.name}; 6 | } 7 | -------------------------------------------------------------------------------- /src/datasets/similarity/templates/tooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | 4 | export default function renderTooltip(node, _store) { 5 | return {node.name}; 6 | } 7 | -------------------------------------------------------------------------------- /legacy/src/shader/line-frag.glsl: -------------------------------------------------------------------------------- 1 | uniform float opacity; 2 | varying float attenuate; 3 | varying float _hidden; 4 | 5 | void main() { 6 | if (_hidden > 0.0) { 7 | discard; 8 | } 9 | gl_FragColor = vec4(0.0, 0.0, 0.0, attenuate * opacity); 10 | } 11 | -------------------------------------------------------------------------------- /scripts/deploy_s3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$S3_BUCKET" ] 4 | then 5 | echo "Please set \$S3_BUCKET" 6 | exit 1 7 | fi 8 | 9 | S3_URL="s3://$S3_BUCKET/materialnet/" 10 | 11 | echo "Uploading to $S3_URL" 12 | 13 | aws s3 sync --delete build/ $S3_URL 14 | -------------------------------------------------------------------------------- /src/store/format.js: -------------------------------------------------------------------------------- 1 | import { format } from 'd3-format'; 2 | 3 | export function createFormatter(spec, prefix, suffix) { 4 | const base = format(spec); 5 | 6 | if (!prefix && !suffix) { 7 | return base; 8 | } 9 | return (v) => `${prefix || ''}${base(v)}${suffix || ''}`; 10 | } 11 | -------------------------------------------------------------------------------- /legacy/src/shader/line-vert.glsl: -------------------------------------------------------------------------------- 1 | attribute float focus; 2 | varying float attenuate; 3 | attribute float hidden; 4 | varying float _hidden; 5 | 6 | void main() { 7 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 8 | attenuate = focus; 9 | _hidden = hidden; 10 | } 11 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/scene-manager/shader/line-vert.glsl: -------------------------------------------------------------------------------- 1 | attribute float focus; 2 | varying float attenuate; 3 | attribute float hidden; 4 | varying float _hidden; 5 | 6 | void main() { 7 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 8 | attenuate = focus; 9 | _hidden = hidden; 10 | } 11 | -------------------------------------------------------------------------------- /src/scene-manager/shader/line-frag.glsl: -------------------------------------------------------------------------------- 1 | uniform float opacity; 2 | uniform float night; 3 | varying float attenuate; 4 | varying float _hidden; 5 | 6 | void main() { 7 | if (_hidden > 0.0) { 8 | discard; 9 | } 10 | gl_FragColor = night > 0.0 ? vec4(1.0, 1.0, 1.0, attenuate * opacity) : vec4(0.0, 0.0, 0.0, attenuate * opacity); 11 | } 12 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/scene-manager/shader/circle-highlight-frag.glsl: -------------------------------------------------------------------------------- 1 | varying vec4 vColor; 2 | varying vec4 edgeColor; 3 | varying float _hidden; 4 | 5 | void main() { 6 | float f = length(gl_PointCoord - vec2(0.5, 0.5)); 7 | if (_hidden > 0.0 || f > 0.5) { 8 | discard; 9 | } else if (f > 0.4) { 10 | gl_FragColor = edgeColor; 11 | } else { 12 | gl_FragColor = vColor; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/graph-vis/sort.js: -------------------------------------------------------------------------------- 1 | function sortStringsAlpha(a, b) { 2 | return a < b ? -1 : a > b ? 1 : 0; 3 | } 4 | 5 | function sortStringsLength(a, b) { 6 | if (a.length < b.length) { 7 | return -1; 8 | } else if (a.length > b.length) { 9 | return 1; 10 | } else { 11 | return sortStringsAlpha(a, b); 12 | } 13 | } 14 | 15 | export { sortStringsAlpha, sortStringsLength }; 16 | -------------------------------------------------------------------------------- /legacy/README.md: -------------------------------------------------------------------------------- 1 | # materialnet 2 | Prototype visualization for TRI materials network data 3 | 4 | ## Build Instructions 5 | 1. Install NPM dependencies: `npm install` 6 | 7 | 2. Build the data: `cd data; python process.py ../src/edges.json; cd ..`. 8 | 9 | 2. Build application: `npm run build` 10 | 11 | 3. Serve application: `npm start` 12 | 13 | 4. Go to http://localhost:8080 14 | -------------------------------------------------------------------------------- /src/components/graph-vis/structure.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { wc } from '../../utils/webcomponents'; 4 | 5 | const Structure = ({ cjson }) => ( 6 | 17 | ); 18 | 19 | export default Structure; 20 | -------------------------------------------------------------------------------- /src/datasets/similarity/colors.js: -------------------------------------------------------------------------------- 1 | import { propertyColorFactory } from '../utils'; 2 | import defaultTemplate from '../default'; 3 | 4 | export default [ 5 | ...defaultTemplate.colors, 6 | { 7 | label: 'Formation Energy', 8 | factory: propertyColorFactory('formation_energy'), 9 | }, 10 | { 11 | label: 'Band Gap', 12 | factory: propertyColorFactory('band_gap'), 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /src/rest/index.js: -------------------------------------------------------------------------------- 1 | export function fetchJSON(path) { 2 | return fetch(path).then(async (res) => { 3 | const text = await res.text(); 4 | let json; 5 | try { 6 | json = JSON.parse(text); 7 | } catch (e) { 8 | json = null; 9 | } 10 | 11 | return json; 12 | }); 13 | } 14 | 15 | export function fetchStructure(name) { 16 | return fetchJSON(`sample-data/structures/${name}.cjson`); 17 | } 18 | -------------------------------------------------------------------------------- /legacy/src/shader/circle-frag.glsl: -------------------------------------------------------------------------------- 1 | varying vec4 vColor; 2 | varying vec4 edgeColor; 3 | varying float _hidden; 4 | 5 | void main() { 6 | float f = length(gl_PointCoord - vec2(0.5, 0.5)); 7 | if (_hidden > 0.0 || f > 0.5) { 8 | discard; 9 | } else if (f > 0.4) { 10 | float factor = abs(0.5 - f) * 10.0; 11 | gl_FragColor = mix(edgeColor, vColor, factor); 12 | } else { 13 | gl_FragColor = vColor; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/scene-manager/shader/circle-frag.glsl: -------------------------------------------------------------------------------- 1 | varying vec4 vColor; 2 | varying vec4 edgeColor; 3 | varying float _hidden; 4 | 5 | void main() { 6 | float f = length(gl_PointCoord - vec2(0.5, 0.5)); 7 | if (_hidden > 0.0 || f > 0.5) { 8 | discard; 9 | } else if (f > 0.4) { 10 | float factor = abs(0.5 - f) * 10.0; 11 | gl_FragColor = mix(edgeColor, vColor, factor); 12 | } else { 13 | gl_FragColor = vColor; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /data/json2gephi.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | 5 | def main(): 6 | if len(sys.argv) < 2: 7 | print >>sys.stderr, 'usage: json2gephi.py ' 8 | 9 | jsonfile = sys.argv[1] 10 | 11 | with open(jsonfile) as f: 12 | data = json.loads(f.read()) 13 | 14 | for edge in data['edges']: 15 | print '{},{}'.format(*edge) 16 | 17 | return 0 18 | 19 | 20 | if __name__ == '__main__': 21 | sys.exit(main()) 22 | -------------------------------------------------------------------------------- /src/components/graph-vis/RotatedPin.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faThumbtack } from '@fortawesome/free-solid-svg-icons'; 4 | 5 | export default class RotatedPin extends React.Component { 6 | render() { 7 | return ( 8 | 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | public/sample-data/ 24 | 25 | /data/co-occurence/* 26 | !/data/co-occurence/*.py 27 | !/data/co-occurence/*.ipynb -------------------------------------------------------------------------------- /legacy/data/rescale.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | 5 | def main(): 6 | data = json.loads(sys.stdin.read()) 7 | 8 | # Remove the mean from the locations. 9 | mean_x = sum(map(lambda d: d['x'], data)) / len(data) 10 | mean_y = sum(map(lambda d: d['y'], data)) / len(data) 11 | 12 | for d in data: 13 | d['x'] -= mean_x 14 | d['y'] -= mean_y 15 | 16 | print json.dumps(data) 17 | 18 | return 0 19 | 20 | 21 | if __name__ == '__main__': 22 | sys.exit(main()) 23 | -------------------------------------------------------------------------------- /legacy/src/shader/circle-vert.glsl: -------------------------------------------------------------------------------- 1 | varying vec4 vColor; 2 | attribute float size; 3 | uniform float zoom; 4 | attribute float selected; 5 | varying vec4 edgeColor; 6 | attribute float focus; 7 | attribute float hidden; 8 | varying float _hidden; 9 | 10 | void main() { 11 | gl_PointSize = zoom * 0.5 * size; 12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 13 | vColor = vec4(color, focus); 14 | edgeColor = selected > 0.5 ? vec4(1.0, 1.0, 0.0, focus) : vec4(0.0, 0.0, 0.0, focus); 15 | _hidden = hidden; 16 | } 17 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | html { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | body { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | #root { 12 | width: 100%; 13 | height: 100%; 14 | } 15 | 16 | .content { 17 | position: relative; 18 | width: 100%; 19 | max-width: 100rem; 20 | left: 50%; 21 | transform: translateX(-50%); 22 | margin-top: 0.5rem; 23 | } 24 | 25 | .logo { 26 | height: 2.5rem; 27 | } 28 | 29 | .geojs-layer[renderer='webgl'] { 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | } 34 | -------------------------------------------------------------------------------- /src/scene-manager/shader/circle-vert.glsl: -------------------------------------------------------------------------------- 1 | varying vec4 vColor; 2 | attribute float size; 3 | uniform float zoom; 4 | attribute float selected; 5 | varying vec4 edgeColor; 6 | attribute float focus; 7 | attribute float hidden; 8 | varying float _hidden; 9 | 10 | void main() { 11 | gl_PointSize = zoom * 0.5 * size; 12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 13 | vColor = vec4(color, focus); 14 | edgeColor = selected > 0.5 ? vec4(1.0, 1.0, 0.0, focus) : vec4(0.0, 0.0, 0.0, focus); 15 | _hidden = hidden; 16 | } 17 | -------------------------------------------------------------------------------- /src/scene-manager/shader/circle-highlight-vert.glsl: -------------------------------------------------------------------------------- 1 | varying vec4 vColor; 2 | attribute float size; 3 | uniform float zoom; 4 | attribute float selected; 5 | varying vec4 edgeColor; 6 | attribute float focus; 7 | attribute float hidden; 8 | varying float _hidden; 9 | 10 | void main() { 11 | gl_PointSize = zoom * 0.5 * size; 12 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 13 | vColor = selected > 0.5 ? vec4(1.0, 1.0, 0.0, focus) : vec4(color, focus); 14 | edgeColor = vec4(0.0, 0.0, 0.0, focus); 15 | _hidden = hidden; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/controls/grid.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid } from '@material-ui/core'; 3 | 4 | export default (props) => { 5 | let { children } = props; 6 | 7 | children = React.Children.toArray(children); 8 | 9 | return ( 10 | 11 | {children.map((child, i) => { 12 | const gridsize = child.props.gridsize || { xs: 12 }; 13 | return ( 14 | 15 | {child} 16 | 17 | ); 18 | })} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/graph-vis/node-size-legend.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from '../../store'; 3 | import { observer } from 'mobx-react'; 4 | import { withStyles } from '@material-ui/core'; 5 | 6 | const styles = (theme) => ({ 7 | root: {}, 8 | }); 9 | 10 | @observer 11 | class NodeSizeLegend extends React.Component { 12 | static contextType = Store; 13 | 14 | render() { 15 | const store = this.context; 16 | 17 | return store.nodeSizer.legend(store.zoomNodeSizeFactor); 18 | } 19 | } 20 | 21 | export default withStyles(styles)(NodeSizeLegend); 22 | -------------------------------------------------------------------------------- /src/data-provider/graph.js: -------------------------------------------------------------------------------- 1 | export function neighborsOf(nodeNameOrSet, edges) { 2 | const lookup = (() => { 3 | if (typeof nodeNameOrSet === 'string') { 4 | nodeNameOrSet = [nodeNameOrSet]; 5 | } 6 | 7 | return Array.isArray(nodeNameOrSet) 8 | ? new Set(nodeNameOrSet) 9 | : nodeNameOrSet; 10 | })(); 11 | 12 | // Collect neighborhood of selected node. 13 | const nodes = new Set(lookup); 14 | for (const edge of edges) { 15 | if (lookup.has(edge[0]) || lookup.has(edge[1])) { 16 | nodes.add(edge[0]); 17 | nodes.add(edge[1]); 18 | } 19 | } 20 | return nodes; 21 | } 22 | -------------------------------------------------------------------------------- /legacy/src/testMolecule.json: -------------------------------------------------------------------------------- 1 | {"name": "d3(ClO)2", "chemical json": 0, "atoms": {"coords": {"3d": [0.0, 0.0, 0.0, 3.12964, 5.92438, 1.40671, 1.85627, 2.54018, 1.60468, -1.46547, 3.3842, 3.01139, 1.66416, 4.22823, 4.4181, 0.3908, 0.84403, 4.61607, 5.0327, 2.46545, 0.40472, -0.04679, 5.84966, 2.60667, 3.56722, 0.91875, 3.41611, -1.51225, 4.30296, 5.61805, 1.96206, 0.76814, 0.37514, 3.02386, 4.15235, 2.63625, 0.49658, 2.61606, 3.38653, 1.55839, 6.00026, 5.64764]}, "elements": {"number": [48, 48, 48, 48, 48, 48, 17, 17, 17, 17, 8, 8, 8, 8]}, "selected": [false, false, false, false, false, false, false, false, false, false, false, false, false, false]}} 2 | -------------------------------------------------------------------------------- /src/components/controls/checkbox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormControl, FormControlLabel, Checkbox } from '@material-ui/core'; 3 | 4 | class CheckboxControlComponent extends Component { 5 | render() { 6 | const { label, value, onChange, className } = this.props; 7 | 8 | return ( 9 | 10 | { 15 | onChange(val); 16 | }} 17 | /> 18 | } 19 | label={label} 20 | /> 21 | 22 | ); 23 | } 24 | } 25 | 26 | export default CheckboxControlComponent; 27 | -------------------------------------------------------------------------------- /src/datasets/index.js: -------------------------------------------------------------------------------- 1 | import precise from './precise'; 2 | import similarity from './similarity'; 3 | import cooccurence from './cooccurence'; 4 | 5 | export default [ 6 | { 7 | key: 'similarity', 8 | label: 'Materials Similarity Network', 9 | fileName: './sample-data/similarity.json', 10 | ...similarity, 11 | }, 12 | { 13 | key: 'stability', 14 | label: 'Materials Stability Network', 15 | fileName: './sample-data/stability.json', 16 | ...precise, 17 | }, 18 | { 19 | key: 'co1k', 20 | label: 'Materials Co-occurrence Network (~1000 edges)', 21 | fileName: './sample-data/cooccurrence-1k.json', 22 | ...cooccurence, 23 | }, 24 | { 25 | key: 'co500k', 26 | label: 'Materials Co-occurrence Network (~500000 edges)', 27 | fileName: './sample-data/cooccurrence-500k.json', 28 | ...cooccurence, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /src/components/graph-vis/node-color-legend.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from '../../store'; 3 | import { observer } from 'mobx-react'; 4 | import { withStyles } from '@material-ui/core'; 5 | 6 | const styles = (theme) => ({ 7 | root: { 8 | marginLeft: '1em', 9 | width: 300, 10 | paddingBottom: 20, 11 | display: 'flex', 12 | alignItems: 'center', 13 | fontSize: 'small', 14 | justifyContent: 'space-around', 15 | }, 16 | }); 17 | 18 | @observer 19 | class NodeColorLegend extends React.Component { 20 | static contextType = Store; 21 | 22 | render() { 23 | const store = this.context; 24 | const { classes } = this.props; 25 | 26 | const legend = store.nodeColorer.legend(); 27 | 28 | return legend &&
{legend}
; 29 | } 30 | } 31 | 32 | export default withStyles(styles)(NodeColorLegend); 33 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | const assetFilename = JSON.stringify(path.basename(filename)); 11 | 12 | if (filename.match(/\.svg$/)) { 13 | return `module.exports = { 14 | __esModule: true, 15 | default: ${assetFilename}, 16 | ReactComponent: (props) => ({ 17 | $$typeof: Symbol.for('react.element'), 18 | type: 'svg', 19 | ref: null, 20 | key: null, 21 | props: Object.assign({}, props, { 22 | children: ${assetFilename} 23 | }) 24 | }), 25 | };`; 26 | } 27 | 28 | return `module.exports = ${assetFilename};`; 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import Store, { ApplicationStore } from './store'; 7 | 8 | import { defineCustomElements as defineSplitMe } from 'split-me/loader'; 9 | import { defineCustomElements as defineMolecule } from '@openchemistry/molecule/loader'; 10 | defineSplitMe(window); 11 | defineMolecule(window); 12 | 13 | const store = new ApplicationStore(); 14 | 15 | ReactDOM.render( 16 | 17 | 18 | , 19 | document.getElementById('root') 20 | ); 21 | 22 | // If you want your app to work offline and load faster, you can change 23 | // unregister() to register() below. Note this comes with some pitfalls. 24 | // Learn more about service workers: http://bit.ly/CRA-PWA 25 | serviceWorker.unregister(); 26 | -------------------------------------------------------------------------------- /data/graphml2json.py: -------------------------------------------------------------------------------- 1 | import networkx 2 | import json 3 | import sys 4 | 5 | 6 | def main(): 7 | if len(sys.argv) < 3: 8 | print >>sys.stderr, 'usage: graphml2data.py ' 9 | return 1 10 | 11 | datafile = sys.argv[1] 12 | graphmlfile = sys.argv[2] 13 | 14 | # Read in the datafile. 15 | with open(datafile) as f: 16 | data = json.loads(f.read()) 17 | 18 | # Read in the GraphML file. 19 | g = networkx.read_graphml(graphmlfile) 20 | 21 | # Transplant the GraphML location data into the data file. 22 | for mat in data['nodes'].keys(): 23 | data['nodes'][mat]['x'] = g.node[mat]['x'] 24 | data['nodes'][mat]['y'] = g.node[mat]['y'] 25 | 26 | # Print the modified data out on stdout. 27 | print json.dumps(data, indent=2, separators=(',', ': ')) 28 | 29 | return 0 30 | 31 | 32 | if __name__ == '__main__': 33 | sys.exit(main()) 34 | -------------------------------------------------------------------------------- /legacy/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 3 | 4 | export default { 5 | entry: './src/index.js', 6 | output: { 7 | path: path.resolve('dist'), 8 | filename: 'index.js' 9 | }, 10 | plugins: [ 11 | new HtmlWebpackPlugin({ 12 | title: 'TRI/Kitware MaterialNet Prototype', 13 | chunksSortMode: 'none' 14 | }) 15 | ], 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | presets: [ 25 | '@babel/env' 26 | ] 27 | } 28 | } 29 | }, 30 | { 31 | test: /\.pug$/, 32 | use: ['pug-loader'] 33 | }, 34 | { 35 | test: /\.glsl$/, 36 | use: ['raw-loader'] 37 | } 38 | ] 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /legacy/src/infopanel.pug: -------------------------------------------------------------------------------- 1 | - const hypothetical = discovery === null; 2 | 3 | h1 #{name} (#{hypothetical ? "undiscovered" : discovery}) #[button#clear Clear selection] 4 | 5 | h2 Material Properties 6 | 7 | p #{degree} derived material#{degree !== 1 ? "s" : ""} 8 | 9 | if (formationEnergy !== undefined) 10 | p Formation energy: #{formationEnergy.toFixed(3)} eV/atom 11 | 12 | if (hypothetical) 13 | p Synthesis probability: #{(synthesisProbability * 100).toFixed(1)}% 14 | 15 | h2 Network Properties 16 | 17 | if (clusCoeff !== undefined) 18 | p Clustering coefficient: #{clusCoeff.toFixed(3)} 19 | 20 | if (eigenCent !== undefined) 21 | p Eigenvector centrality: #{eigenCent.toFixed(3)} 22 | 23 | if (degCent !== undefined) 24 | p Degree centrality: #{degCent.toFixed(3)} 25 | 26 | if (shortestPath !== undefined) 27 | p Shortest path: #{shortestPath.toFixed(3)} 28 | 29 | if (degNeigh !== undefined) 30 | p Degree neighborhood: #{degNeigh.toFixed(3)} 31 | -------------------------------------------------------------------------------- /src/components/controls/number.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { FormControl, TextField } from '@material-ui/core'; 4 | 5 | export default class NumberControlComponent extends Component { 6 | onChange = (value) => { 7 | if (Number.isFinite(Number.parseFloat(value))) { 8 | const { onChange } = this.props; 9 | onChange(value); 10 | } 11 | }; 12 | 13 | render() { 14 | const { label, value, min, max, step } = this.props; 15 | 16 | return ( 17 | 18 | { 29 | this.onChange(e.target.value); 30 | }} 31 | /> 32 | 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/legend/LegendCircle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from '@material-ui/core'; 3 | 4 | const height = 20; 5 | 6 | const styles = (theme) => ({ 7 | root: { 8 | borderRadius: '50%', 9 | position: 'relative', 10 | width: height, 11 | margin: '0 3em', 12 | height, 13 | '&::before': { 14 | content: 'attr(title)', 15 | position: 'absolute', 16 | top: '100%', 17 | left: '50%', 18 | whiteSpace: 'nowrap', 19 | padding: 2, 20 | textAlign: 'center', 21 | transform: 'translate(-50%,0)', 22 | }, 23 | }, 24 | }); 25 | 26 | class LegendCircle extends React.Component { 27 | render() { 28 | const { classes, label, color } = this.props; 29 | 30 | return ( 31 |
36 | ); 37 | } 38 | } 39 | 40 | export default withStyles(styles)(LegendCircle); 41 | -------------------------------------------------------------------------------- /src/components/graph-vis/info-block.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { format as d3Format } from 'd3-format'; 3 | import { Typography } from '@material-ui/core'; 4 | 5 | function defaultFormatter(v) { 6 | if (v == null) { 7 | return v; 8 | } 9 | if (typeof v === 'number') { 10 | return v.toFixed(3); 11 | } 12 | return v; 13 | } 14 | 15 | export default class InfoBlock extends React.Component { 16 | render() { 17 | const { label, value, format, children } = this.props; 18 | 19 | const formatValue = 20 | typeof format === 'function' 21 | ? format 22 | : format 23 | ? d3Format(format) 24 | : defaultFormatter; 25 | 26 | return ( 27 | value != null && ( 28 | <> 29 | 30 | {label} 31 | 32 | 33 | {formatValue(value)} 34 | {children} 35 | 36 | 37 | ) 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/datasets/cooccurence.js: -------------------------------------------------------------------------------- 1 | import defaultTemplate from './default'; 2 | import { propertyColorFactory, propertySizeFactory } from './utils'; 3 | 4 | const properties = { 5 | degree: { 6 | label: 'Derived materials', 7 | format: 'd', 8 | }, 9 | deg_cent: { 10 | label: 'Degree Centrality', 11 | filterable: true, 12 | }, 13 | eigen_cent: { 14 | label: 'Eigenvector Centrality', 15 | filterable: true, 16 | }, 17 | }; 18 | 19 | const colors = [ 20 | ...defaultTemplate.colors, 21 | ...Object.entries(properties).map(([prop, info]) => { 22 | return { 23 | label: info.label, 24 | factory: propertyColorFactory(prop), 25 | }; 26 | }), 27 | ]; 28 | 29 | const sizes = [ 30 | ...defaultTemplate.sizes, 31 | ...Object.entries(properties).map(([prop, info]) => { 32 | return { 33 | label: info.label, 34 | factory: propertySizeFactory(prop), 35 | }; 36 | }), 37 | ]; 38 | 39 | export default { 40 | ...defaultTemplate, 41 | 42 | properties, 43 | colors, 44 | sizes, 45 | 46 | defaults: { 47 | ...defaultTemplate.defaults, 48 | color: colors[1], 49 | size: sizes[1], 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/graph-vis/layouts.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Grid from '../controls/grid'; 4 | import Store from '../../store'; 5 | import { observer } from 'mobx-react'; 6 | import { Button } from '@material-ui/core'; 7 | 8 | @observer 9 | class Layouts extends React.Component { 10 | static contextType = Store; 11 | 12 | render() { 13 | const store = this.context; 14 | return ( 15 | 16 | {!store.subGraphLayouting && ( 17 | 23 | )} 24 | {store.subGraphLayouting && ( 25 | 26 | )} 27 | {!store.subGraphLayouting && ( 28 | 34 | )} 35 | 36 | ); 37 | } 38 | } 39 | 40 | export default Layouts; 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to MaterialNet 2 | 3 | MaterialNet is open source software, and we welcome questions, bug reports, and 4 | code contributions from the community. We want MaterialNet to solve not only our 5 | problems but yours too. Please use the following resources in contributing to 6 | the project. 7 | 8 | ## Bug/Issue Reports 9 | 10 | If you run into trouble with the MaterialNet software, you can file bugs and 11 | issues, or ask code-related questions by opening a report at the [GitHub issue 12 | tracker](https://github.com/ToyotaResearchInstitute/materialnet/issues). We will 13 | do our best to respond to questions and help resolve any issues. 14 | 15 | General support is available through this issue tracker as well. Just let us 16 | know what's wrong and we'll do our best to get you going again. 17 | 18 | ## Pull Requests 19 | 20 | We also welcome improvements to MaterialNet via pull requests. You can follow 21 | the usual GitHub-based procedure of 22 | [forking](https://github.com/ToyotaResearchInstitute/materialnet/network/members) 23 | the MaterialNet codebase, creating a branch with your contribution, then opening 24 | a [pull request](https://github.com/ToyotaResearchInstitute/materialnet/pulls) 25 | on GitHub. 26 | -------------------------------------------------------------------------------- /data/edge-sample.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import sys 4 | 5 | 6 | def main(): 7 | if len(sys.argv) < 4: 8 | print >>sys.stderr, 'usage: edge-sample.py ' 9 | return 1 10 | 11 | # Collect command line arguments. 12 | nodefile = sys.argv[1] 13 | edgefile = sys.argv[2] 14 | numedges = int(sys.argv[3]) 15 | seed = int(sys.argv[4]) 16 | 17 | # Read in data files. 18 | nodes = None 19 | with open(nodefile) as f: 20 | nodes = json.loads(f.read()) 21 | 22 | edges = None 23 | with open(edgefile) as f: 24 | edges = json.loads(f.read()) 25 | 26 | # Seed the RNG. 27 | random.seed(seed) 28 | 29 | # Grab a population sample. 30 | sample = random.sample(range(len(edges)), numedges) 31 | 32 | # Extract the edge sample. 33 | edge_sample = [edges[i] for i in sample] 34 | 35 | # Compute the node closure of the edge sample. 36 | node_sample = {} 37 | for e in edge_sample: 38 | node_sample[e[0]] = nodes[e[0]] 39 | node_sample[e[1]] = nodes[e[1]] 40 | 41 | # Dump the data. 42 | print json.dumps({'nodes': node_sample, 'edges': edge_sample}, indent=2, separators=(',', ': ')) 43 | 44 | return 0 45 | 46 | 47 | if __name__ == '__main__': 48 | sys.exit(main()) 49 | -------------------------------------------------------------------------------- /src/utils/webcomponents.js: -------------------------------------------------------------------------------- 1 | /*** 2 | Taken from: https://github.com/ionic-team/ionic-react-conference-app/blob/master/src/utils/stencil.js 3 | 4 | This function is meant to make it easier to use Props and Custom Events with Custom 5 | Elements in React. 6 | props.updateFavoriteFilter(e.target.value)} 9 | > 10 | 11 | <<< SHOULD BE WRITTEN AS >>> 12 | props.updateFavoriteFilter(e.target.value) 16 | })} 17 | > 18 | 19 | ***/ 20 | 21 | export function wc(customEvents = {}, props = {}) { 22 | let storedEl; 23 | 24 | return function (el) { 25 | Object.entries(customEvents).forEach(([name, value]) => { 26 | // If we have an element then add event listeners 27 | // otherwise remove the event listener 28 | const action = el ? el.addEventListener : storedEl.removeEventListener; 29 | if (typeof value === 'function') { 30 | action(name, value); 31 | return; 32 | } 33 | }); 34 | // If we have an element then set props 35 | if (el) { 36 | Object.entries(props).forEach(([name, value]) => { 37 | el[name] = value; 38 | }); 39 | } 40 | storedEl = el; 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/datasets/precise/index.js: -------------------------------------------------------------------------------- 1 | import templates from './templates'; 2 | import colors from './colors'; 3 | import sizes from './sizes'; 4 | import defaultTemplate from '../default'; 5 | 6 | export default { 7 | ...defaultTemplate, 8 | yearRange: [1920, 2016], 9 | templates, 10 | colors, 11 | sizes, 12 | 13 | properties: { 14 | degree: { 15 | label: 'Derived materials', 16 | format: 'd', 17 | }, 18 | formation_energy: { 19 | label: 'Formation Energy', 20 | filterable: true, 21 | format: '.3f', 22 | suffix: ` eV/atom`, 23 | }, 24 | synthesis_probability: { 25 | label: 'Synthesis Probability', 26 | filterable: true, 27 | format: '.1%', 28 | domain: [0, 1], 29 | }, 30 | eigen_cent: { 31 | label: 'Eigenvector Centrality', 32 | filterable: true, 33 | }, 34 | deg_cent: { 35 | label: 'Degree Centrality', 36 | filterable: true, 37 | }, 38 | shortest_path: { 39 | label: 'Shortest path', 40 | }, 41 | deg_neigh: { 42 | label: 'Degree neighborhood', 43 | }, 44 | }, 45 | 46 | defaults: { 47 | ...defaultTemplate.defaults, 48 | template: templates[0], 49 | color: colors[1], 50 | zoom: -2.3, 51 | size: sizes[1], 52 | colorYear: 2016, 53 | year: [1920, 2016], 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/controls/slider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormControl, Typography, Slider } from '@material-ui/core'; 3 | 4 | class SliderControlComponent extends Component { 5 | render() { 6 | const { 7 | label, 8 | value, 9 | range, 10 | step, 11 | onChange, 12 | digits, 13 | format, 14 | children, 15 | } = this.props; 16 | 17 | const child = children ? React.Children.only(children) : undefined; 18 | 19 | const paddingRight = child ? 16 : 0; 20 | 21 | return ( 22 | 23 | {label} 24 |
25 |
26 | {format 27 | ? format(value) 28 | : value.toFixed(Number.isInteger(digits) ? digits : 2)} 29 |
30 |
31 | { 37 | onChange(val); 38 | }} 39 | /> 40 |
41 | {child} 42 |
43 |
44 | ); 45 | } 46 | } 47 | 48 | export default SliderControlComponent; 49 | -------------------------------------------------------------------------------- /src/components/controls/select.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Select, MenuItem, FormControl, InputLabel } from '@material-ui/core'; 3 | 4 | import { isString } from 'lodash-es'; 5 | 6 | class SelectControlComponent extends Component { 7 | render() { 8 | const { label, value, options, onChange } = this.props; 9 | 10 | let selectOptions = []; 11 | for (let option of options) { 12 | if (isString(option)) { 13 | selectOptions.push( 14 | 15 | {option.replace('\\u002', '.')} 16 | 17 | ); 18 | } else if (Number.isFinite(option)) { 19 | selectOptions.push( 20 | 21 | {option} 22 | 23 | ); 24 | } else { 25 | const { label, value } = option; 26 | selectOptions.push( 27 | 28 | {label.replace('\\u002', '.')} 29 | 30 | ); 31 | } 32 | } 33 | 34 | return ( 35 | 36 | {label} 37 | 46 | 47 | ); 48 | } 49 | } 50 | 51 | export default SelectControlComponent; 52 | -------------------------------------------------------------------------------- /src/components/controls/rangeslider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormControl, Typography, Slider } from '@material-ui/core'; 3 | 4 | class RangeSliderControlComponent extends Component { 5 | render() { 6 | const { 7 | label, 8 | value, 9 | range, 10 | step, 11 | onChange, 12 | digits, 13 | format, 14 | children, 15 | } = this.props; 16 | 17 | const child = children ? React.Children.only(children) : undefined; 18 | 19 | return ( 20 | 21 | {label} 22 |
23 |
24 | {format 25 | ? format(value[0]) 26 | : value[0].toFixed(Number.isInteger(digits) ? digits : 2)} 27 |
28 |
29 | { 35 | onChange(val); 36 | }} 37 | /> 38 |
39 |
40 | {format 41 | ? format(value[1]) 42 | : value[1].toFixed(Number.isInteger(digits) ? digits : 2)} 43 |
44 | {child} 45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | export default RangeSliderControlComponent; 52 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI, in coverage mode, or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--coverage') === -1 && 45 | argv.indexOf('--watchAll') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /src/datasets/precise/templates/minimal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | import tooltip from './tooltip'; 4 | import InfoBlock from '../../../components/graph-vis/info-block'; 5 | import Structure from '../../../components/graph-vis/structure'; 6 | 7 | export default { 8 | label: 'Minimal', 9 | render: (node, store) => { 10 | const hypothetical = node.discovery == null; 11 | 12 | return ( 13 | <> 14 | {`${node.name} (${ 15 | hypothetical ? 'undiscovered' : node.discovery 16 | })`} 17 | 18 | 19 | Material Properties 20 | 21 | 22 | {node.degree != null && ( 23 | 27 | )} 28 | {node.formation_energy != null && ( 29 | 33 | )} 34 | {node.synthesis_probability != null && ( 35 | 39 | )} 40 | 41 |
42 | 43 |
44 | 45 | ); 46 | }, 47 | tooltip, 48 | }; 49 | -------------------------------------------------------------------------------- /src/datasets/similarity/templates/minimal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | import tooltip from './tooltip'; 4 | import InfoBlock from '../../../components/graph-vis/info-block'; 5 | import Structure from '../../../components/graph-vis/structure'; 6 | 7 | export default { 8 | label: 'Minimal', 9 | render: (node, store) => { 10 | const hypothetical = node.discovery == null; 11 | 12 | return ( 13 | <> 14 | {`${node.name} (${ 15 | hypothetical ? 'undiscovered' : node.discovery 16 | })`} 17 | 18 | 19 | Material Properties 20 | 21 | 22 | {node.degree != null && ( 23 | 27 | )} 28 | {node.formation_energy != null && ( 29 | 33 | )} 34 | {node.synthesis_probability != null && ( 35 | 39 | )} 40 | 41 |
42 | 43 |
44 | 45 | ); 46 | }, 47 | tooltip, 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/legend/LegendGradient.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from '@material-ui/core'; 3 | 4 | const height = 20; 5 | 6 | const styles = (theme) => ({ 7 | root: { 8 | flexGrow: 1, 9 | height: height / 2, 10 | position: 'relative', 11 | '&::before': { 12 | content: 'attr(data-from)', 13 | position: 'absolute', 14 | top: '100%', 15 | left: 0, 16 | whiteSpace: 'nowrap', 17 | paddingTop: height / 4 + 2, 18 | textAlign: 'left', 19 | }, 20 | '&::after': { 21 | content: 'attr(data-to)', 22 | position: 'absolute', 23 | top: '100%', 24 | right: 0, 25 | whiteSpace: 'nowrap', 26 | paddingTop: height / 4 + 2, 27 | textAlign: 'right', 28 | }, 29 | }, 30 | }); 31 | 32 | class LegendGradient extends React.Component { 33 | render() { 34 | const { classes, scale, format } = this.props; 35 | const gradientSamples = 10; 36 | const domain = scale.domain(); 37 | // sample the color scale and create a gradient defintion out of it 38 | const colors = scale.copy().domain([0, gradientSamples]); 39 | const samples = Array.from({ length: gradientSamples + 1 }) 40 | .map((_, i) => `${colors(i)} ${Math.round((i * 100) / gradientSamples)}%`) 41 | .join(','); 42 | return ( 43 |
49 | ); 50 | } 51 | } 52 | 53 | export default withStyles(styles)(LegendGradient); 54 | -------------------------------------------------------------------------------- /src/datasets/similarity/sizes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SizeLegend } from '../../components/legend'; 3 | import { scaleLinear, scaleLog, scaleSqrt } from 'd3-scale'; 4 | import defaultTemplate from '../default'; 5 | 6 | function degreeFunction(createScale) { 7 | return (store) => { 8 | if (!store.data) { 9 | return { 10 | legend: () => null, 11 | scale: () => store.sizeScaleRange[0], 12 | }; 13 | } 14 | 15 | const degrees = Object.values(store.data.nodes).map((node) => node.degree); 16 | const minMax = degrees.reduce( 17 | ([min, max], v) => { 18 | if (v == null) { 19 | return [min, max]; 20 | } 21 | return [Math.min(min, v), Math.max(max, v)]; 22 | }, 23 | [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] 24 | ); 25 | 26 | const scale = createScale(minMax).range(store.sizeScaleRange).clamp(true); 27 | 28 | return { 29 | legend: (factor) => , 30 | scale: (node) => { 31 | const degree = node.degree || 0; 32 | return scale(degree); 33 | }, 34 | }; 35 | }; 36 | } 37 | 38 | export default [ 39 | ...defaultTemplate.sizes, 40 | { 41 | label: 'Degree - Linear', 42 | factory: degreeFunction((domain) => scaleLinear().domain(domain)), 43 | }, 44 | { 45 | label: 'Degree - Sqrt', 46 | factory: degreeFunction((domain) => scaleSqrt().domain(domain)), 47 | }, 48 | { 49 | label: 'Degree - Log', 50 | factory: degreeFunction((domain) => 51 | scaleLog().domain([Math.max(1, domain[0]), domain[1]]) 52 | ), 53 | }, 54 | ]; 55 | -------------------------------------------------------------------------------- /legacy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "materialnet", 3 | "version": "0.1.0", 4 | "description": "Prototype visualization for TRI materials network dataEdit", 5 | "main": "index.js", 6 | "scripts": { 7 | "layout": "babel-node data/layout.js", 8 | "build": "webpack --mode development", 9 | "watch": "npm run build -- --watch", 10 | "build:prod": "webpack --mode production", 11 | "lint": "semistandard | snazzy", 12 | "start": "http-server dist/" 13 | }, 14 | "babel": { 15 | "presets": [ 16 | "@babel/env" 17 | ] 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/arclamp/materialnet.git" 22 | }, 23 | "author": "Kitware Inc.", 24 | "license": "Apache-2.0", 25 | "bugs": { 26 | "url": "https://github.com/arclamp/materialnet/issues" 27 | }, 28 | "homepage": "https://github.com/arclamp/materialnet#readme", 29 | "semistandard": { 30 | "ignore": [ 31 | "dist" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.0.0-beta.54", 36 | "@babel/node": "^7.0.0-beta.54", 37 | "@babel/preset-env": "^7.0.0-beta.54", 38 | "@babel/register": "^7.0.0-beta.54", 39 | "@openchemistry/molecule": "0.4.5", 40 | "babel-loader": "^8.0.0-beta.4", 41 | "d3-force": "^1.1.0", 42 | "d3-scale": "^2.1.0", 43 | "d3-selection": "^1.3.0", 44 | "html-webpack-plugin": "^3.2.0", 45 | "http-server": "^0.11.1", 46 | "pug": "^2.0.3", 47 | "pug-loader": "^2.4.0", 48 | "raw-loader": "^0.5.1", 49 | "semistandard": "^12.0.1", 50 | "snazzy": "^7.1.1", 51 | "three": "^0.94.0", 52 | "webcola": "^3.3.8", 53 | "webpack": "^4.16.1", 54 | "webpack-cli": "^3.1.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | MaterialNet 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/graph-vis/PinnedNode.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from '../../store'; 3 | import { observer } from 'mobx-react'; 4 | import { Chip } from '@material-ui/core'; 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | import RotatedPin from './RotatedPin'; 7 | import { faFlask, faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; 8 | 9 | @observer 10 | class PinnedNode extends React.Component { 11 | static contextType = Store; 12 | render() { 13 | const store = this.context; 14 | const { node, includeNeighbors, defineSubspace } = this.props; 15 | 16 | const label = node.formula || node.name; 17 | 18 | const neighbors = ( 19 | } 21 | label={label} 22 | onClick={() => (store.selected = node)} 23 | onDelete={() => store.toggleIncludeNeighbors(node)} 24 | /> 25 | ); 26 | const subspace = ( 27 | } 29 | label={label} 30 | onClick={() => (store.selected = node)} 31 | onDelete={() => store.toggleDefineSubspace(node)} 32 | /> 33 | ); 34 | 35 | if (includeNeighbors && defineSubspace) { 36 | return ( 37 | <> 38 | {neighbors} {subspace} 39 | 40 | ); 41 | } else if (includeNeighbors) { 42 | return neighbors; 43 | } else if (defineSubspace) { 44 | return subspace; 45 | } 46 | return ( 47 | } 49 | label={label} 50 | onClick={() => (store.selected = node)} 51 | onDelete={() => store.removePinned(node)} 52 | /> 53 | ); 54 | } 55 | } 56 | 57 | export default PinnedNode; 58 | -------------------------------------------------------------------------------- /src/datasets/similarity/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Typography } from '@material-ui/core'; 3 | import InfoBlock from '../../components/graph-vis/info-block'; 4 | import colors from './colors'; 5 | import sizes from './sizes'; 6 | import defaultTemplate from '../default'; 7 | 8 | const templates = [ 9 | { 10 | label: 'Similarity', 11 | render: (node, store) => { 12 | return ( 13 | <> 14 | 15 | {node.formula} ({node.name}) 16 | 17 | 18 | Properties 19 | 20 | 21 | {store.propertyList.map((prop) => { 22 | const value = node[prop.property]; 23 | if (value == null) { 24 | return null; 25 | } 26 | return ; 27 | })} 28 | 29 | ); 30 | }, 31 | tooltip: (node) => ( 32 | {node.formula} 33 | ), 34 | }, 35 | ]; 36 | 37 | export default { 38 | ...defaultTemplate, 39 | templates, 40 | colors, 41 | sizes, 42 | 43 | properties: { 44 | degree: { 45 | label: 'Derived materials', 46 | format: 'd', 47 | }, 48 | formation_energy: { 49 | label: 'Formation Energy', 50 | filterable: true, 51 | format: '.3f', 52 | suffix: ` eV/atom`, 53 | }, 54 | band_gap: { 55 | label: 'Band Gap', 56 | filterable: true, 57 | format: '.3f', 58 | suffix: ' eV/atom', 59 | }, 60 | }, 61 | 62 | defaults: { 63 | ...defaultTemplate.defaults, 64 | template: templates[0], 65 | color: colors[1], 66 | zoom: -2.3, 67 | size: sizes[2], 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /src/datasets/precise/sizes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SizeLegend } from '../../components/legend'; 3 | import { scalePow, scaleLinear, scaleLog, scaleSqrt } from 'd3-scale'; 4 | import defaultTemplate from '../default'; 5 | 6 | function degreeFunction(createScale) { 7 | return (store) => { 8 | if (!store.data) { 9 | return { 10 | legend: () => null, 11 | scale: () => store.sizeScaleRange[0], 12 | }; 13 | } 14 | // map lookup 15 | const degrees = store.data.nodeDegrees( 16 | (node) => node.discovery == null || node.discovery <= store.year[1] 17 | ); 18 | const minMax = Object.values(degrees).reduce( 19 | ([min, max], v) => { 20 | if (v == null) { 21 | return [min, max]; 22 | } 23 | return [Math.min(min, v), Math.max(max, v)]; 24 | }, 25 | [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY] 26 | ); 27 | 28 | const scale = createScale(minMax).range(store.sizeScaleRange).clamp(true); 29 | return { 30 | legend: (factor) => , 31 | scale: (node) => { 32 | const degree = degrees[node.name] || 0; 33 | return scale(degree); 34 | }, 35 | }; 36 | }; 37 | } 38 | 39 | export default [ 40 | ...defaultTemplate.sizes, 41 | { 42 | label: 'Degree - Linear', 43 | factory: degreeFunction((domain) => scaleLinear().domain(domain)), 44 | }, 45 | { 46 | label: 'Degree - Sqrt', 47 | factory: degreeFunction((domain) => scaleSqrt().domain(domain)), 48 | }, 49 | { 50 | label: 'Degree - Power 2', 51 | factory: degreeFunction((domain) => scalePow().domain(domain).exponent(2)), 52 | }, 53 | { 54 | label: 'Degree - Log', 55 | factory: degreeFunction((domain) => 56 | scaleLog().domain([Math.max(1, domain[0]), domain[1]]) 57 | ), 58 | }, 59 | ]; 60 | -------------------------------------------------------------------------------- /src/datasets/default.js: -------------------------------------------------------------------------------- 1 | import { ApplicationStore } from '../store'; 2 | import React from 'react'; 3 | import { Typography } from '@material-ui/core'; 4 | import InfoBlock from '../components/graph-vis/info-block'; 5 | import Structure from '../components/graph-vis/structure'; 6 | 7 | const templates = [ 8 | { 9 | label: 'Generic', 10 | render: (node, store) => { 11 | return ( 12 | <> 13 | 14 | {node.name} 15 | 16 | 17 | Properties 18 | 19 | 20 | {store.propertyList.map((prop) => { 21 | const value = node[prop.property]; 22 | if (value == null) { 23 | return null; 24 | } 25 | return ; 26 | })} 27 | 28 |
29 | 30 |
31 | 32 | ); 33 | }, 34 | tooltip: (node) => {node.name}, 35 | }, 36 | ]; 37 | 38 | const colors = [ 39 | { 40 | label: 'None', 41 | factory: () => ({ 42 | legend: () => null, 43 | scale: () => ApplicationStore.FIXED_COLOR, 44 | }), 45 | }, 46 | ]; 47 | 48 | const sizes = [ 49 | { 50 | label: 'None', 51 | factory: () => ({ 52 | legend: () => null, 53 | scale: () => 10, 54 | }), 55 | }, 56 | ]; 57 | 58 | export default { 59 | templates, 60 | 61 | colors, 62 | zoomRange: [-3.75, 3], 63 | sizes, 64 | yearRange: null, // disable 65 | 66 | properties: {}, 67 | 68 | defaults: { 69 | template: templates[0], 70 | color: colors[0], 71 | zoom: -2.3, 72 | size: sizes[0], 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/graph-vis/table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Store from '../../store'; 3 | import { observer } from 'mobx-react'; 4 | import LineUp, { 5 | LineUpStringColumnDesc, 6 | LineUpNumberColumnDesc, 7 | LineUpRanking, 8 | LineUpColumn, 9 | } from 'lineupjsx'; 10 | import 'lineupjsx/build/LineUpJSx.css'; 11 | import './lineup.css'; 12 | 13 | @observer 14 | class Table extends React.Component { 15 | static contextType = Store; 16 | 17 | onSelectionChanged = (selection) => { 18 | const store = this.context; 19 | store.selected = 20 | selection.length > 0 ? store.subGraphNodes[selection[0]] : null; 21 | }; 22 | 23 | render() { 24 | const store = this.context; 25 | const selected = store.selected 26 | ? store.subGraphNodes.findIndex((d) => d.name === store.selected.name) 27 | : -1; 28 | 29 | return ( 30 | = 0 ? [selected] : undefined} 41 | onSelectionChanged={this.onSelectionChanged} 42 | > 43 | 44 | {store.propertyList.map((prop) => ( 45 | 52 | ))} 53 | 54 | 55 | 56 | {store.propertyList.map((prop) => ( 57 | 58 | ))} 59 | 60 | 61 | ); 62 | } 63 | } 64 | 65 | export default Table; 66 | -------------------------------------------------------------------------------- /legacy/src/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | head 3 | meta(charset=utf-8) 4 | 5 | body 6 | table 7 | tr 8 | td 9 | div#vis(style=`width:${width}px; height:${height}px;`) 10 | td 11 | div(style="width:540px; height:540px") 12 | oc-molecule#structure 13 | 14 | input#search(type="text" list="materials" placeholder="Search (e.g. H2O)") 15 | datalist#materials 16 | 17 | div 18 | table(cellpadding="10") 19 | tr 20 | td 21 | span Node Color 22 | br 23 | select#color 24 | option(data-name="none") (none) 25 | option(selected data-name="discovery") Year of Discovery 26 | option(data-name="boolean") Discovered/Hypothetical 27 | option(data-name="undiscovered") Discovered/Undiscovered 28 | br 29 | input#coloryear(type="range" min="1945" max="2015" step="1" value="1945") 30 | span#coloryeardisplay 1945 31 | td 32 | span Node Size 33 | br 34 | select#size 35 | option(data-name="none") (none) 36 | option(selected data-name="degree/normal") Degree 37 | option(data-name="degree/large") Degree - Large 38 | option(data-name="degree/huge") Degree - Huge 39 | td 40 | input#links(type="checkbox" checked) 41 | label(for="links") Links 42 | 43 | td 44 | label(for="zoom") Zoom 45 | br 46 | input#zoom(type="range" min="0" max="113") 47 | 48 | td 49 | label(for="opacity") Link opacity 50 | br 51 | input#opacity(type="range" min="3" max="1000") 52 | 53 | td 54 | label(for="spacing") Node spacing 55 | br 56 | input#spacing(type="range" min="1" max="10") 57 | 58 | td 59 | label#filterlabel(for="filter") Show all materials 60 | br 61 | input#filter(type="range" min="1945" max="2016" value="2016") 62 | br 63 | button#autoplay Autoplay 64 | 65 | div#infopanel 66 | -------------------------------------------------------------------------------- /legacy/data/layout.js: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3-force'; 2 | 3 | import fs from 'fs'; 4 | import { performance } from 'perf_hooks'; 5 | 6 | // Grab the edgefile off the command line args. 7 | const edgefile = process.argv[2]; 8 | if (!edgefile) { 9 | console.error('usage: layout.js '); 10 | process.exit(1); 11 | } 12 | 13 | let iterations = process.argv[3]; 14 | if (iterations === undefined) { 15 | iterations = 300; 16 | } 17 | 18 | // Parse out the edge JSON. 19 | const text = fs.readFileSync(edgefile, 'utf8'); 20 | const edges = JSON.parse(text); 21 | 22 | // Process nodes and edges from the edge data. 23 | // 24 | // Begin by collecting unique nodes and recording the source/target indices for 25 | // each link. 26 | let index = {}; 27 | let reverseIndex = {}; 28 | let nodes = []; 29 | let links = []; 30 | let count = 0; 31 | edges.forEach(e => { 32 | e.forEach(n => { 33 | if (!index.hasOwnProperty(n)) { 34 | index[n] = count; 35 | reverseIndex[count] = n; 36 | count++; 37 | nodes.push({ 38 | name: n 39 | }); 40 | } 41 | }); 42 | 43 | links.push({ 44 | source: index[e[0]], 45 | target: index[e[1]] 46 | }); 47 | }); 48 | 49 | // Create a force simulation object. 50 | const layout = d3.forceSimulation() 51 | .nodes(nodes) 52 | .force('charge', d3.forceManyBody()) 53 | .force('link', d3.forceLink(links).distance(300).strength(1)) 54 | // .force('center', d3.forceCenter()) 55 | .stop(); 56 | 57 | let cycle = 0; 58 | const start = performance.now(); 59 | const tick = () => { 60 | ++cycle; 61 | 62 | const now = performance.now(); 63 | const elapsed = now - start; 64 | 65 | const est = Math.floor(0.01 * elapsed / cycle * (iterations - cycle)) / 10; 66 | 67 | process.stderr.write(`\r${cycle} / ${iterations} (${Math.floor(0.01 * elapsed) / 10}s elapsed, ~${est}s remaining)`); 68 | }; 69 | 70 | const end = () => { 71 | console.log(JSON.stringify(nodes.map(n => ({x: n.x, y: n.y, name: n.name})))); 72 | }; 73 | 74 | for (let i = 0; i < iterations; i++) { 75 | layout.tick(); 76 | tick(); 77 | } 78 | 79 | end(); 80 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | lint: 14 | name: Run lint tests 15 | runs-on: ubuntu-latest 16 | steps: 17 | # Check out the repo. 18 | - uses: actions/checkout@v2 19 | 20 | # Initiate caching for node_modules. 21 | - name: Cache node_modules 22 | uses: actions/cache@v2 23 | id: cache-node-modules 24 | with: 25 | path: node_modules 26 | key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }}-${{ hashFiles('package.json') }} 27 | 28 | # Install the Yarn dependencies. 29 | - uses: actions/setup-node@v1 30 | - name: Install dependencies 31 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 32 | run: yarn install 33 | 34 | # Run the linting tests. 35 | - run: yarn lint 36 | - run: yarn prettier:check-all 37 | 38 | deploy: 39 | name: Deploy application to github.io 40 | runs-on: ubuntu-latest 41 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 42 | steps: 43 | - uses: actions/checkout@v2 44 | 45 | - name: Cache node_modules 46 | uses: actions/cache@v2 47 | id: cache-node-modules 48 | with: 49 | path: node_modules 50 | key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }}-${{ hashFiles('package.json') }} 51 | 52 | - uses: actions/setup-node@v1 53 | 54 | - name: Install dependencies 55 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 56 | run: yarn install 57 | 58 | - run: yarn build 59 | 60 | - name: Download the sample data pack 61 | run: curl --output sample-data.tar.gz https://data.kitware.com/api/v1/file/5e5696b4af2e2eed35da2e44/download 62 | 63 | - name: Unpack the sample data 64 | run: tar xzvf sample-data.tar.gz -C build 65 | 66 | - name: Remove the tarball 67 | run: rm sample-data.tar.gz 68 | 69 | - uses: peaceiris/actions-gh-pages@v3 70 | with: 71 | github_token: ${{ secrets.GITHUB_TOKEN }} 72 | publish_dir: ./build 73 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right