Skip to content

Instantly share code, notes, and snippets.

@sxywu
Last active May 26, 2019 16:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sxywu/1db896c1a38d89ae71b4 to your computer and use it in GitHub Desktop.
Save sxywu/1db896c1a38d89ae71b4 to your computer and use it in GitHub Desktop.
The Force with React + D3, Approach #2

React + D3 exploration with the force layout:

  • React for structure
  • D3 for data calculation
  • D3 AND React for rendering
    • React for enter/exit
    • D3 for update

Pro:

  • Takes advantage of respective strengths

Con:

  • Wraps React component around all the nodes -> will not scale
  • Makes people uncomfortable

Use case: React app with a small but highly interactive visualization


The original using only D3: Enter-Update-Exit in Force Layout

The Force with React + D3, Approach #1

The Force with React + D3, Approach #2

The Force with React + D3, Approach #3

body {
font-family: Helvetica;
}
.update {
color: #888888;
position:absolute;
top: 10px;
left: 10px;
padding: 5px 10px;
margin: 10px;
cursor: pointer;
border: 1px solid #999999;
border-radius: 3px;
}
.node circle {
fill: #888888;
stroke: #fff;
stroke-width: 2px;
}
.node text {
fill: #888888;
stroke: none;
font-size: .6em;
}
.link {
stroke: #cccccc;
stroke-opacity: .6;
}
function randomData(nodes, width, height) {
var oldNodes = nodes;
// generate some data randomly
nodes = _.chain(_.range(_.random(10, 30)))
.map(function() {
var node = {};
node.key = _.random(0, 30);
node.size = _.random(4, 10);
return node;
}).uniq(function(node) {
return node.key;
}).value();
if (oldNodes) {
var add = _.initial(oldNodes, _.random(0, oldNodes.length));
add = _.rest(add, _.random(0, add.length));
nodes = _.chain(nodes)
.union(add).uniq(function(node) {
return node.key;
}).value();
}
links = _.chain(_.range(_.random(15, 35)))
.map(function() {
var link = {};
link.source = _.random(0, nodes.length - 1);
link.target = _.random(0, nodes.length - 1);
link.key = link.source + ',' + link.target;
link.size = _.random(1, 3);
return link;
}).uniq((link) => link.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(-150, 150);
d.y = height / 2 + _.random(-25, 25);
}
});
}
<!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>
@worldsayshi
Copy link

Argh! Why does d3 + react have to be weeeeird. I blame d3 for the weirdness.

@worldsayshi
Copy link

I added a bunch of scaffolding and turned it into a server-client app: https://github.com/worldsayshi/graphql-react-force-d3-example :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment