Skip to content

Instantly share code, notes, and snippets.

@ZoeLeBlanc
Last active February 20, 2018 18:11
Show Gist options
  • Save ZoeLeBlanc/3043ed56ba7e90e97c681c7692795d74 to your computer and use it in GitHub Desktop.
Save ZoeLeBlanc/3043ed56ba7e90e97c681c7692795d74 to your computer and use it in GitHub Desktop.
React D3 Force With Labels
license: mit
{
"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
},
{
"source": 1,
"target": 3,
"type": "FOUNDED"
},
{
"source": 2,
"target": 3,
"type": "WORKS_ON"
},
{
"source": 3,
"target": 4,
"type": "IS_A"
}
]
}
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<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://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
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 linkDistance=200;
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() {
d3.json("graph.json", function (error, graph) {
if (error) throw error;
var links = graph.links;
var nodes = graph.nodes;
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment