Playing with D3 Version 4, React, React-Motion
Note — D3 Version 4 officially released!
This text has been published in Dec 2015, before d3.v4 has officially been released:
So, some details might have changed — thank you for understanding.
TL;DR
Here we present 3 examples, combining the power of the new d3 Version 4 modules and React (and React-Motion, and Redux).
All three examples provide different findings, see summary at the bottom.
And there is a challenge in Example 2.
Links
- Source Code on Github
- Live Example (sorry, direct links to the examples are currently broken)
- d3 modules
- react-motion
Motivation
Based on the new d3 modules by Mike Bostock, we wanted to find out a bit about the implications that new setup has in a webapp in React.
What has changed with d3 version 4? Amongst many other things, we would like to highlight two points:
- D3 has been split up into smaller modules. This is very helpful if you just require e.g. scales or shapes. Especially the introduction of the latter has motivated this article.
- The API has changed slightly, see e.g. here for d3-shape.
Before we check out the examples, we think it’s important to keep in mind: There are many ways to combine D3 and React, as we are listing here or as Shirley Wu is discussing in this talk. It depends on the project and your skills!
So this playground is to give the user an idea on how the new d3 modules *can* be used in React, hopefully it will trigger interesting discussions.
Set up of the Examples
We are providing 3 simple examples here:
- A very plain example, just consisting of a svg container and some shapes
- An example using d3 Version 3 layouts. Why version 3? Well, to our knowledge, these layouts haven’t been ported to d3 Version 4 modules yet. Still we wanted to check performance when rendered by React, etc.
- An animated example using Cheng Lou’s react-motion library.
Example 1: d3-shape
In this example, we just set up a component with an svg container. In this container, we then add a line, a pie chart and a radial line. Let’s for example look at the line component:
import React from 'react';
import { line, basisClosed } from 'd3-shape';
export const Line = () => {
const lineGenerator = line()
.curve(basisClosed)
.x(d => d.x)
.y(d => d.y);
const data = [ /* some data */ ];
return (<path stroke={'red'} fill={'none'}
d={lineGenerator(data)}/>);
};
And that’s it.
For the pie chart, notice how we map over the arcs to plot these:
return (<g>
{arcs.map((a, i) => {
return (<path
key={'arc' + i}
fill={'none'}
stroke={'steelblue'}
d={arcGen(a)}/>);
})}
</g>);
Simple as that.
Example 2: Using layouts & performance
Here we have a challenge for the reader:
Here is the original example by Mike Bostock. Notice how he, on mouseEvent, attaches classes to the respective links and nodes, and thus the performance/user experience is really good (in Chrome, that is; let’s forget about the other browsers).
In React, if you want to use React to manage the DOM, the situation is a bit different. How so? Well, to our understanding, you can only change lines if you re-render them. So, if we want to be as close to the original example as we can (adding/deleting classes on mouseover), we re-render quite a lot of things.
In order to speed things up, we thus moved the lines and nodes in separate components, using shouldComponentUpdate to only re-render components that have changed.
So e.g. for the lines we have:
export default class LinkPath extends Component {
shouldComponentUpdate(nextProps) {
return (nextProps.classy !== 'link' ||
this.props.classy !== 'link'
/* some other conditions */);
}
render() {
const { l, lineGenerator, classy } = this.props;
return (<path
className={classy}
d={lineGenerator(l)}/>);
}
}
where we add it in the layout as follows:
<g>
{lines.map((l, i) => {
let classy = /* set up the classes based on data */
return (<LinkPath key={'path' + i}
classy={classy}
l={l}
lineGenerator={lineGenerator}/>);
})}
</g>
This is it, with some additional props for rotation, tension, respectively.
So the challenge? How can we make this any faster? (without bending the rules of React “too much”…). Any feedback is much appreciated!
Example 3: An animated path using react-motion
One thing we love a lot about D3 are the transitions. So we wanted to see: can we do the same/similar effects with react-motion? Note that this example has room for improvement, as we are new to this library. But still, the first results are promising. Yes, it takes a bit of time to set things up to work as expected, but it’s definitely worth it!
We squeezed everything in one file to give a better overview, for the sake of the example.
So what are the important bits?
Note that we are using TransitionMotion (instead of Motion), as we want to be able to add/remove points in the graph (such as Enter/Exit in D3). Setting up an example in Motion (for a fixed number of points) works too, we didn’t include that here.
So for TransitionMotion, we set up the “styles” (the moving parts) as follows:
getStyles(fakeData) {
const { springParamsStd } = this.props.app;
const { width } = this.state;
const xScale = linear()
.domain([0, 100])
.range([0.1 * width, 0.9 * width]);
const yScale = linear()
.domain([0, 100])
.range([0.9 * width, 0.1 * width]);
const configs = {};
fakeData.map((d, i) => {
configs['point' + i] = {
x: spring(xScale(d.x), springParamsStd),
y: spring(yScale(d.y), springParamsStd),
r: spring(50 * Math.random(), springParamsStd),
};
});
return configs;
}
(using scales from d3-scale).
Next, for the set up, we need
willEnter(key, style) {
const { springParamsStd } = this.props;
return Object.assign({}, style, { r: spring(0, springParamsStd) });
}
willLeave(key, style) {
const { springParamsStd } = this.props;
return Object.assign({}, style, { r: spring(-20, springParamsStd) });
}
and we can move to the render function, or better, the important (moving) part:
<TransitionMotion
styles={this.getStyles(fakeData)}
willEnter={this.willEnter.bind(this)}
willLeave={this.willLeave.bind(this)}
>
{inter => {
const myPath = [];
const pts = fakeData.length;
Object.keys(inter).map((p, i) => {
const point = inter[p];
if (i < pts) {
myPath.push({
x: point.x,
y: point.y,
});
}
});
return (<div style={{ border: '2px solid lightgray' }}>
<svg height={width} width={width}
stroke={ 'lightgray' }
strokeWidth={'5px'} fill="none">
<path d={lineGenerator(myPath)}/>
{
Object.keys(inter).map((key, i) => {
const { ...style } = inter[key];
return (
<circle key={'circ' + i}
cx={style.x}
cy={style.y}
r={Math.max(0, style.r)}
stroke={col(i / (nFakeData - 1))}
/>
);
})
}
</svg>
</div>);
}}
</TransitionMotion>
We advise to read the react-motion documentation, it’s probably not so clear at first sight…
Summary
Example 1
Using these new d3 modules, we can build some very light-weight components to include in all React apps (example 1). These little helpers can be reused very easily and styled as in the good old d3 times (using JSX, if you like).
See also Victory.JS by formidablelabs for a similar approach, that provides Components you can combine (and animate!).
Example 2
If you want to build a complex graph (such as the Hierarchical Edge Bundling (it even has a complex name)), then you must think about the tooling thoroughly, i.e. how much React, D3, … should I add to the mix? How can I achieve a good performance?
There is not a perfect way and that’s why this business is so interesting.
Oh, and there is a challenge, see above.
Example 3
This example provides a way to animate the graphs in React. This is meant to be POC. We hope to find some time soon to package these things up neatly so that it could be packed into a module, as the lines, pies, etc. in example 1.
Summary of the summary
Do you have any feedback? Ideas? Please do let us (Chris Roth) know, thanks!