Tom MacWright

tom@macwright.com

Mixing d3 and React

tl;dr: Use JSX instead of the d3 selection API and keep everything else.

I have lots of love for both React and d3. Recently I was tasked with building a few data-visualization dashboards, and combined the two. From my understanding of the two technologies, the way I mushed them together was the simplest possible way, but after completing the project and reading prior art, it’s a bit different than most. So here’s my flavor of React+d3 intermingling.

This is one of the first times a Venn Diagram has seemed appropriate, and I have no intention of missing it.

DOMManipulationMathScalesLayoutsUtilitiesEtc.ComponentsServerRenderingReactd3

d3 includes everything necessary to create charts: it includes DOM manipulation, scales, algorithms, parsers, and much more. Especially in the world of d3 version 4, you can mix & match d3’s parts with other software, using only those you want to.

That’s the key to mixing d3 & React: use React for DOM manipulation. Sure, React has a technique for intermixing with other libraries and you can reference elements in the DOM, but in my experience these are last resorts.

That’s it: I think porting d3-based DOM manipulation code to React’s JSX code is the simplest and best way to mix the two libraries.

Other Approaches

There are other ways that other people have promoted - imitating d3’s API, wrapping around React’s lifecycle methods, or even wrapping d3 in components. I think these approaches fall into two traps: using d3’s selection logic where it isn’t necessary and having two ways to build a page, and over-abstracting d3’s extremely flexible visualization logic into less flexible components with hard assumptions.

Example

I’ll start with Mike Bostock’s pie chart update block.

Here’s the code, annotated with whether it needs to be changed or not:

/\*\*

- This code doesn't touch the DOM, and won't be changed
  \*/
  var width = 960,
  height = 500,
  radius = Math.min(width, height) / 2;

var color = d3.schemeCategory10;

var pie = d3.pie()
.value(function(d) { return d.apples; })
.sort(null);

var arc = d3.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 20);

/\*

- Here's where we start to interact with the DOM
  \*/
  var svg = d3.select("body").append("svg")
  .attr("width", width)
  .attr("height", height)
  .append("g")
  .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

d3.tsv("data.tsv", type, function(error, data) {
var path = svg.datum(data).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color[i]; })
.attr("d", arc);

d3.selectAll("input")
.on("change", change);

var timeout = setTimeout(function() {
d3.select("input[value=\"oranges\"]").property("checked", true).each(change);
}, 2000);

function change() {
var value = this.value;
clearTimeout(timeout);
pie.value(function(d) { return d[value]; }); // change the value function
path = path.data(pie); // compute the new angles
path.attr("d", arc); // redraw the arcs
}
});

function type(d) {
d.apples = +d.apples;
d.oranges = +d.oranges;
return d;
}

In terms of the value that we derive from d3 - the parts that would be hard to write from scratch without a library - the vast majority here is in the brilliantly-implemented scale and layout methods, and the methods that generate SVG paths. All of those methods work perfectly with React. They’d even work really well with jQuery or another tookit: they’re agnostic to the DOM implementation.

Here’s that example, in React:

Except for the part where we interact with the DOM, the code is the same.

Here’s the process I take to make this port:

  • Things like layouts, scales, and margins, I move into the render() lifecycle method. There’s a good chance that I’ll eventually make this chart resizable using react-measure, so calculating these parts on the fly makes sense, and outweighs the very tiny performance cost of recreating them for each render.
  • d3 examples often store state as a global variable or store it implicitly: this one, for instance, has a choice between apples and oranges that’s only applied when you click a button. I move this to the component state - or if you’re being fancy, you can move it to Redux or any other place you store state.
  • React makes you describe the DOM you want rather than the steps to create that DOM, like d3 does. Porting from one to the other means moving a lot of .append() calls into JSX, and instead of .attr('foo', bar), foo={bar} in JSX syntax.

Advantages

In my opinion, this approach has several major advantages over others:

  • It establishes one true way of putting elements on a page. You don’t add SVG elements with React and HTML elements with d3: you have one DOM-related API to think about.
  • Since the code you end up with is very similar to the d3 code you start with, it enables you to start from d3’s incredible library of examples and Mike Bostock’s invaluable gists.

Disadvantages

Most of d3’s interaction with the DOM is rooted in the selection API, but there are a few other substantive places where it builds DOMs for you. The one I’ve bumped into the most is the axis code, which draws ticks and axis lines. Luckily, it’s only around 100 lines ported to React - here’s what I’ve been working with.

Assumptions

The elephant in the room, for some, is animation. d3 is very concerned with making animation possible and its selection API reflects that fact. Personally I avoid animation whenever possible, and so I haven’t invested much time getting it to work with this approach: but it’s likely possible with Cheng Lou’s phenomenal react-motion project.