Skip to content

Instantly share code, notes, and snippets.

@hkjpotato
Last active August 24, 2017 19:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hkjpotato/55a25dd75d7a0e8d3d2129a8326b61ca to your computer and use it in GitHub Desktop.
Save hkjpotato/55a25dd75d7a0e8d3d2129a8326b61ca to your computer and use it in GitHub Desktop.

This is a simple demo to show you how I integrate React and d3 force layout.

React + D3 exploration with the force layout:

  • React in charge of everything except location
  • D3 only care about location

Pro:

  • Able to do large scale of data.
  • React's data is immutable.
  • Force mutates the location data and update location directly (no React diff check required).
  • Dragging behavior is possible.

Reference ###Sally Wu's 3 approaches

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

###Uber's React Force Vis react-vis-force

###Formidable Force Layout formidable

//create force
function createForce(nodes) {
const force = d3.layout.force()
.size([960, 500]);
return applyNodes(force, nodes);
}
function applyNodes(force, nodes) {
const forceNodesSet = new Set(force.nodes().map(n => n.id));
const reactNodesSet = new Set(nodes.map(n => n.id));
//only when the key is different, we need to re-init the force
if (!setsEqual(forceNodesSet, reactNodesSet)) {
//a key to force node map to quickly access the force node by key(id)
const key2nodeMap = {};
const fnodes = nodes.map(node => {
//create a new object, dont mutate the react data
const fnode = {
id: node.id //only id is needed, you can copy init x and y as well
};
if (force.key2nodeMap && (node.id in force.key2nodeMap)) {
//copy the previous location if exits
Object.assign(fnode, force.key2nodeMap[node.id]);
}
key2nodeMap[node.id] = fnode;
return fnode;
});
//update force with new list of force nodes
force.nodes(fnodes);
//bind the key2nodeMap for quick search
force.key2nodeMap = key2nodeMap;
force.start();
}
return force;
}
<html>
<style type="text/css">
body {
font-family: Helvetica;
}
.btn {
color: #888888;
position:absolute;
top: 10px;
left: 10px;
padding: 5px 10px;
margin: 10px;
cursor: pointer;
border: 1px solid #999999;
border-radius: 3px;
background: #fff;
}
.btn.btn2 {
left: 120px;
}
</style>
<div id="root"></div>
<!-- babel -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.24/browser.js"></script>
<!-- react -->
<script src="https://unpkg.com/react@15/dist/react.js"></script>
<!-- react-dom -->
<script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="./setsEqual.js"></script>
<script src="./forceUtils.js"></script>
<script type="text/babel">
//react jsx code in es6
class ReactForce extends React.PureComponent {
static get defaultProps() {
return {
nodes: []
}
}
constructor(props) {
super(props);
const {
nodes
} = props;
//this is how force can access the actual dom when ticking
//notice this is very different from d3, which bind the data to dom directly
this.nodeDoms = {};
this.force = createForce(nodes);
this.force.on('tick', () => {
this.updatePostions();
})
}
componentWillReceiveProps(nextProps) {
if (nextProps.nodes !== this.props.nodes) {
//only update force when new nodes received
this.force = applyNodes(this.force, nextProps.nodes);
}
}
updatePostions() {
this.force.nodes().forEach(fnode => {
this.nodeDoms[fnode.id]
.setAttribute('transform', `translate(${fnode.x},${fnode.y})`);
});
}
render() {
const {
nodes,
width,
height
} = this.props;
//clean the key-instance map
this.nodeDoms = {};
const nodeElements = nodes.map(node => (
<circle
key={node.id}
ref={dom => this.nodeDoms[node.id] = dom}
r={node.radius}
cx={0}
cy={0}
fill={'#888'} />
));
return (
<svg width={width} height={height}>
<g>{nodeElements}</g>
</svg>
)
}
}
//--App--, owns nodes as state
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
nodes: [
{
id: 1,
radius: Math.floor(Math.random() * 10) + 1,
},
{
id: 2,
radius: Math.floor(Math.random() * 10) + 1,
}
],
width: 960,
height: 500
}
}
updateRadius() {
const nodes = this.state.nodes.map(node => ({
...node,
radius: Math.floor(Math.random() * 10) + 1
}));
this.setState({
nodes: nodes
});
}
addNode() {
this.setState({
nodes: [
...this.state.nodes,
{
id: this.state.nodes.length + 1,
radius: Math.floor(Math.random() * 10) + 1
}
]
});
}
render() {
return (
<div>
<div>
<ReactForce {...this.state} />
</div>
<button className="btn" onClick={() => this.updateRadius()}>Update Radius</button>
<button className="btn btn2" onClick={() => this.addNode()}>Add Node</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</script>
</html>
//from Uber
window.setsEqual = function(setA, setB) {
if (setA.size !== setB.size) {
return false;
}
for (const a of setA) {
if (!setB.has(a)) {
return false;
}
}
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment