Built with blockbuilder.org
forked from sxywu's block: Updated React+D3, Approach #1
forked from sxywu's block: Updated React+D3, Approach #2
| license: mit |
Built with blockbuilder.org
forked from sxywu's block: Updated React+D3, Approach #1
forked from sxywu's block: Updated React+D3, Approach #2
| function randomData(nodes, width, height) { | |
| var oldNodes = nodes; | |
| // generate some data randomly | |
| nodes = _.chain(_.range(_.random(10, 20))) | |
| .map(() => { | |
| return { | |
| key: _.random(30), | |
| size: _.random(8, 16), | |
| }; | |
| }).uniqBy('key').value(); | |
| if (oldNodes) { | |
| var end = _.random(oldNodes.length); | |
| var start = _.random(end); | |
| var add = _.slice(oldNodes, start, end + 1); | |
| nodes = _.chain(nodes) | |
| .union(add).uniqBy('key').value(); | |
| } | |
| var nodeKeys = _.map(nodes, 'key'); | |
| links = _.chain(_.range(_.random(15, 25))) | |
| .map(function() { | |
| var source = nodeKeys[_.random(nodes.length - 1)]; | |
| var target = nodeKeys[_.random(nodes.length - 1)]; | |
| if (source === target) return; | |
| return { | |
| source, | |
| target, | |
| key: source + ',' + target, | |
| size: _.random(2, 4) | |
| }; | |
| }).filter().uniqBy('key').value(); | |
| maintainNodePositions(oldNodes, nodes, width, height); | |
| return {nodes, links}; | |
| } | |
| function maintainNodePositions(oldNodes, nodes, width, height) { | |
| var kv = {}; | |
| _.each(oldNodes, function(d) { | |
| kv[d.key] = d; | |
| }); | |
| _.each(nodes, function(d) { | |
| if (kv[d.key]) { | |
| // if the node already exists, maintain current position | |
| d.x = kv[d.key].x; | |
| d.y = kv[d.key].y; | |
| } else { | |
| // else assign it a random position near the center | |
| d.x = width / 2 + _.random(-25, 25); | |
| d.y = height / 2 + _.random(-25, 25); | |
| } | |
| }); | |
| } |
| <meta charset='utf-8'> | |
| <head> | |
| <script src="//npmcdn.com/[email protected]/dist/react.min.js"></script> | |
| <script src="//npmcdn.com/[email protected]/dist/react-dom.min.js"></script> | |
| <script src="//npmcdn.com/[email protected]/browser.min.js"></script> | |
| <script src='https://unpkg.com/[email protected]'></script> | |
| <script src='https://unpkg.com/[email protected]'></script> | |
| <script src='generateData.js'></script> | |
| <style> | |
| svg { | |
| width: 400px; | |
| height: 300px; | |
| } | |
| #root { | |
| width: 400px; | |
| text-align: center; | |
| color: #333; | |
| } | |
| .update { | |
| padding: 5px 10px; | |
| margin: 10px; | |
| cursor: pointer; | |
| border: 1px solid #333; | |
| display: inline-block; | |
| } | |
| .node { | |
| fill: #ee8365; | |
| stroke: #fff; | |
| cursor: pointer; | |
| } | |
| .link { | |
| stroke: #ee8365; | |
| stroke-opacity: .5; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id='root' /> | |
| <script type="text/babel"> | |
| var width = 400; | |
| var height = 300; | |
| var simulation = d3.forceSimulation() | |
| .force('collide', d3.forceCollide(d => 2 * d.size)) | |
| .force('charge', d3.forceManyBody(-100)) | |
| .force('center', d3.forceCenter(width / 2, height / 2)) | |
| .stop(); | |
| class Graph extends React.Component { | |
| constructor(props) { | |
| super(props); | |
| this.state = {selected: null}; | |
| this.selectNode = this.selectNode.bind(this); | |
| } | |
| shouldComponentUpdate(nextProps, nextState) { | |
| if (nextProps.version === this.props.version) { | |
| // if version is the same, no updates to data | |
| // so it must be interaction to select+highlight a node | |
| this.calculateHighlights(nextState.selected); | |
| this.circles.attr('opacity', d => | |
| !nextState.selected || this.highlightedNodes[d.key] ? 1 : 0.2) | |
| this.lines.attr('opacity', d => | |
| !nextState.selected || this.highlightedLinks[d.key] ? 0.5 : 0.1) | |
| return false; | |
| } | |
| return true; | |
| } | |
| componentDidMount() { | |
| this.container = d3.select(this.refs.container); | |
| this.calculateData(); | |
| this.calculateHighlights(this.state.selected); | |
| this.renderLinks(); | |
| this.renderNodes(); | |
| } | |
| componentDidUpdate() { | |
| this.calculateData(); | |
| this.calculateHighlights(this.state.selected); | |
| this.renderLinks(); | |
| this.renderNodes(); | |
| } | |
| calculateData() { | |
| var {nodes, links} = this.props; | |
| simulation.nodes(nodes) | |
| .force('link', d3.forceLink(links).id(d => d.key).distance(100)); | |
| _.times(2000, () => simulation.tick()); | |
| this.nodes = nodes; | |
| this.links = links; | |
| } | |
| calculateHighlights(selected) { | |
| this.highlightedNodes = {}; | |
| this.highlightedLinks = {}; | |
| if (selected) { | |
| this.highlightedNodes[selected] = 1; | |
| _.each(this.links, link => { | |
| if (link.source.key === selected) { | |
| this.highlightedNodes[link.target.key] = 1; | |
| this.highlightedLinks[link.key] = 1; | |
| } | |
| if (link.target.key === selected) { | |
| this.highlightedNodes[link.source.key] = 1; | |
| this.highlightedLinks[link.key] = 1; | |
| } | |
| }); | |
| } | |
| } | |
| renderNodes() { | |
| this.circles = this.container.selectAll('circle') | |
| .data(this.nodes, d => d.key); | |
| // exit | |
| this.circles.exit().remove(); | |
| // enter + update | |
| this.circles = this.circles.enter().append('circle') | |
| .classed('node', true) | |
| .merge(this.circles) | |
| .attr('cx', d => d.x) | |
| .attr('cy', d => d.y) | |
| .attr('r', d => d.size) | |
| .attr('opacity', d => | |
| !this.state.selected || this.highlightedNodes[d.key] ? 1 : 0.2) | |
| .on('click', this.selectNode); | |
| } | |
| renderLinks() { | |
| this.lines = this.container.selectAll('line') | |
| .data(this.links, d => d.key); | |
| // exit | |
| this.lines.exit().remove(); | |
| // enter + update | |
| this.lines = this.lines.enter().insert('line', 'circle') | |
| .classed('link', true) | |
| .merge(this.lines) | |
| .attr('stroke-width', d => d.size) | |
| .attr('x1', d => d.source.x) | |
| .attr('x2', d => d.target.x) | |
| .attr('y1', d => d.source.y) | |
| .attr('y2', d => d.target.y) | |
| .attr('opacity', d => | |
| !this.state.selected || this.highlightedLinks[d.key] ? 0.5 : 0.1); | |
| } | |
| selectNode(node) { | |
| if (node.key === this.state.selected) { | |
| this.setState({selected: null}); | |
| } else { | |
| this.setState({selected: node.key}); | |
| } | |
| } | |
| render() { | |
| return ( | |
| <svg ref='container' /> | |
| ) | |
| } | |
| } | |
| class App extends React.Component { | |
| constructor(props) { | |
| super(props); | |
| this.updateData = this.updateData.bind(this); | |
| this.state = {nodes: [], links: [], version: 0}; | |
| } | |
| componentWillMount() { | |
| this.updateData(); | |
| } | |
| updateData() { | |
| var {nodes, links} = randomData(this.state.nodes, width, height); | |
| this.setState({nodes, links, version: this.state.version + 1}); | |
| } | |
| render() { | |
| return ( | |
| <div> | |
| <Graph {...this.state} /> | |
| <div className="update" onClick={this.updateData}>update</div> | |
| </div> | |
| ); | |
| } | |
| } | |
| ReactDOM.render( | |
| <App />, | |
| document.getElementById('root') | |
| ); | |
| </script> | |
| </body> |