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
{ | |
"nodes": [ | |
{ | |
"name": "Peter", | |
"label": "Person", | |
"id": 1 | |
}, | |
{ | |
"name": "Michael", | |
"label": "Person", | |
"id": 2 | |
}, | |
{ | |
"name": "Neo4j", | |
"label": "Database", | |
"id": 3 | |
}, | |
{ | |
"name": "Graph Database", | |
"label": "Database", | |
"id": 4 | |
} | |
], | |
"links": [ | |
{ | |
"source": 1, | |
"target": 2, | |
"type": "KNOWS", | |
"since": 2010, | |
"key": "1,2" | |
}, | |
{ | |
"source": 1, | |
"target": 3, | |
"type": "FOUNDED", | |
"key": "1,3" | |
}, | |
{ | |
"source": 2, | |
"target": 3, | |
"type": "WORKS_ON", | |
"key": "2,3" | |
}, | |
{ | |
"source": 3, | |
"target": 4, | |
"type": "IS_A", | |
"key": "3,4" | |
} | |
] | |
} |
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="http://d3js.org/d3.v4.min.js" type="text/javascript"></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: #999; stroke-opacity: .6; stroke-width: 1px; | |
} | |
</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> |