|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<head> |
|
<script src="https://fb.me/react-0.14.3.js"></script> |
|
<script src="https://fb.me/react-dom-0.14.3.js"></script> |
|
<script src="https://npmcdn.com/babel-core@5.8.34/browser.min.js"></script> |
|
<script src="http://d3js.org/d3.v3.min.js"></script> |
|
<script src="http://underscorejs.org/underscore-min.js"></script> |
|
<script src="generate_data.js"></script> |
|
|
|
<link rel="stylesheet" href="example.css" type="text/css" /> |
|
</head> |
|
|
|
<body> |
|
|
|
<div id="main" /> |
|
|
|
<script type="text/babel"> |
|
|
|
var width = 960; |
|
var height = 500; |
|
var force = d3.layout.force() |
|
.charge(-300) |
|
.linkDistance(50) |
|
.size([width, height]); |
|
|
|
// ***************************************************** |
|
// ** d3 functions to manipulate attributes |
|
// ***************************************************** |
|
|
|
var enterNode = (selection) => { |
|
selection.select('circle') |
|
.attr("r", (d) => d.size) |
|
.call(force.drag); |
|
|
|
selection.select('text') |
|
.attr("x", (d) => d.size + 5) |
|
.attr("dy", ".35em"); |
|
}; |
|
|
|
var updateNode = (selection) => { |
|
selection.attr("transform", (d) => "translate(" + d.x + "," + d.y + ")"); |
|
}; |
|
|
|
var enterLink = (selection) => { |
|
selection.attr("stroke-width", (d) => d.size); |
|
}; |
|
|
|
var updateLink = (selection) => { |
|
selection.attr("x1", (d) => d.source.x) |
|
.attr("y1", (d) => d.source.y) |
|
.attr("x2", (d) => d.target.x) |
|
.attr("y2", (d) => d.target.y); |
|
}; |
|
|
|
var updateGraph = (selection) => { |
|
selection.selectAll('.node') |
|
.call(updateNode); |
|
selection.selectAll('.link') |
|
.call(updateLink); |
|
}; |
|
|
|
// ***************************************************** |
|
// ** React classes to enter/exit elements |
|
// ***************************************************** |
|
|
|
var Node = React.createClass({ |
|
componentDidMount() { |
|
this.d3Node = d3.select(ReactDOM.findDOMNode(this)) |
|
.datum(this.props.data) |
|
.call(enterNode); |
|
}, |
|
|
|
componentDidUpdate() { |
|
this.d3Node.datum(this.props.data) |
|
.call(updateNode); |
|
}, |
|
|
|
render() { |
|
return ( |
|
<g className='node'> |
|
<circle/> |
|
<text>{this.props.data.key}</text> |
|
</g> |
|
); |
|
}, |
|
}); |
|
|
|
var Link = React.createClass({ |
|
componentDidMount() { |
|
this.d3Link = d3.select(ReactDOM.findDOMNode(this)) |
|
.datum(this.props.data) |
|
.call(enterLink); |
|
}, |
|
|
|
componentDidUpdate() { |
|
this.d3Link.datum(this.props.data) |
|
.call(updateLink); |
|
}, |
|
|
|
render() { |
|
return (<line className='link' />); |
|
}, |
|
}); |
|
|
|
// ***************************************************** |
|
// ** Graph and App components |
|
// ***************************************************** |
|
|
|
var Graph = React.createClass({ |
|
componentDidMount() { |
|
this.d3Graph = d3.select(ReactDOM.findDOMNode(this)); |
|
force.on('tick', () => { |
|
// after force calculation starts, call updateGraph |
|
// which uses d3 to manipulate the attributes, |
|
// and React doesn't have to go through lifecycle on each tick |
|
this.d3Graph.call(updateGraph); |
|
}); |
|
}, |
|
|
|
componentDidUpdate() { |
|
// we should actually clone the nodes and links |
|
// since we're not supposed to directly mutate |
|
// props passed in from parent, and d3's force function |
|
// mutates the nodes and links array directly |
|
// we're bypassing that here for sake of brevity in example |
|
force.nodes(this.props.nodes).links(this.props.links); |
|
|
|
// start force calculations after |
|
// React has taken care of enter/exit of elements |
|
force.start(); |
|
}, |
|
|
|
render() { |
|
// use React to draw all the nodes, d3 calculates the x and y |
|
var nodes = _.map(this.props.nodes, (node) => { |
|
return (<Node data={node} key={node.key} />); |
|
}); |
|
var links = _.map(this.props.links, (link) => { |
|
return (<Link key={link.key} data={link} />); |
|
}); |
|
|
|
return ( |
|
<svg width={width} height={height}> |
|
<g> |
|
{links} |
|
{nodes} |
|
</g> |
|
</svg> |
|
); |
|
} |
|
}); |
|
|
|
var App = React.createClass({ |
|
getInitialState() { |
|
return { |
|
nodes: [], |
|
links: [], |
|
}; |
|
}, |
|
|
|
componentDidMount() { |
|
this.updateData(); |
|
}, |
|
|
|
updateData() { |
|
// randomData is loaded in from external file generate_data.js |
|
// and returns an object with nodes and links |
|
var newState = randomData(this.state.nodes, width, height); |
|
this.setState(newState); |
|
}, |
|
|
|
render() { |
|
return ( |
|
<div> |
|
<div className="update" onClick={this.updateData}>update</div> |
|
<Graph nodes={this.state.nodes} links={this.state.links} /> |
|
</div> |
|
); |
|
}, |
|
}); |
|
|
|
ReactDOM.render( |
|
<App />, |
|
document.getElementById('main') |
|
); |
|
|
|
</script> |
|
|
|
</body> |
Argh! Why does d3 + react have to be weeeeird. I blame d3 for the weirdness.