React and d3

While React and d3 are designed with different goals in mind they do share a common theme. This is that data should define what you see.

The following musings are supported by a simple mortgage calculator example you might want to play around with first.

That they have different goals can be seen in their approaches to changing the DOM. I prefer to think of the algorithms behind these two pieces of technology as implementation details for the most part. At the end of the day what you get out of them is satisfactory performance for a particular use-case. For React is it effectively letting you re-render your interactive UI on any data change using a shadow DOM. For d3, it doesn't feel quite as declarative, but it will mutate the DOM directly to enable you to update and animate thousands of elements as part of your data visualization.

It's easy to think of applications which would benefit from both approaches. For example, an analytics dashboard could use React for the UI and d3 to render the charts.

However it turns out to be not straight forward to integrate the two technologies. One approach I've used before is to treat the d3 component as a black-box as far as React is concerned. The extent of the integration is to hook it into the React lifecycle events so it knows when to re-render itself.

This blog post explores a possible deeper integration. The basic idea is to use React, rather than d3, to render the SVG elements. However we still make use of d3 to generate the necessary attributes from the data. This way we treat d3 as a utility library and make use of its excellent layout, shape, and geographic capabilities.

So what might this look like? Let's consider rendering a line chart. First up we need an svg element to draw into. We can do this declaratively in JSX.

return <svg width="600px" height="200px"></svg>

Next up we need to draw a line. Here we can use a path element.

<path d="M10 10 H 90 V 90 H 10 Z" fill="transparent" stroke="black" />

The fill and stroke properties specify the appearance of the line. The d attribute is much more interesting to us. It is a string containing a series of commands and parameters for drawing lines and curves. This is where d3 comes in. We can use the d3-shape module to generate this string for us based on our input data. The example below assumes the data has two array properties: time and value.

const x = d3.scaleLinear()
  .domain(d3.extent(data.time))
  .range([0, width]);

const y = d3.scaleLinear()
  .domain([0, d3.max(data.value)]
  .range([height, 0]);

const line = d3.line()
  .x(d => x(d.time))
  .y(d => y(d.value));

return (
  <svg width="600px" height="200px">
    <path d={line(data)} fill="transparent" stroke="black" />
  </svg>
);

Looking good! A chart is not a chart without axes though. So let's give that a go.

The d3-axis module is the place to start. Reading through the README we see an example of the output.

<g fill="none" font-size="10" font-family="sans-serif" text-anchor="middle">
  <path class="domain" stroke="#000" d="M0.5,6V0.5H880.5V6"></path>
  <g class="tick" opacity="1" transform="translate(0,0)">
    <line stroke="#000" y2="6" x1="0.5" x2="0.5"></line>
    <text fill="#000" y="9" x="0.5" dy="0.71em">
      0.0
    </text>
  </g>
  <g class="tick" opacity="1" transform="translate(176,0)">
    <line stroke="#000" y2="6" x1="0.5" x2="0.5"></line>
    <text fill="#000" y="9" x="0.5" dy="0.71em">
      0.2
    </text>
  </g>
  <g class="tick" opacity="1" transform="translate(352,0)">
    <line stroke="#000" y2="6" x1="0.5" x2="0.5"></line>
    <text fill="#000" y="9" x="0.5" dy="0.71em">
      0.4
    </text>
  </g>
  <g class="tick" opacity="1" transform="translate(528,0)">
    <line stroke="#000" y2="6" x1="0.5" x2="0.5"></line>
    <text fill="#000" y="9" x="0.5" dy="0.71em">
      0.6
    </text>
  </g>
  <g class="tick" opacity="1" transform="translate(704,0)">
    <line stroke="#000" y2="6" x1="0.5" x2="0.5"></line>
    <text fill="#000" y="9" x="0.5" dy="0.71em">
      0.8
    </text>
  </g>
  <g class="tick" opacity="1" transform="translate(880,0)">
    <line stroke="#000" y2="6" x1="0.5" x2="0.5"></line>
    <text fill="#000" y="9" x="0.5" dy="0.71em">
      1.0
    </text>
  </g>
</g>

As you can see this is a little more complicated than the simple path we considered previously. The axis is built up from a collection of path, line and text elements. If you're not familiar with svg, the g element is used to group several other elements together. It's not unlike a div element in html. In particular it let's us transform a group of elements together which is very useful for positioning the ticks on our axis.

Can we do as we did before and declare the elements in JSX and use d3 to generate the necessary attributes? Turns out the answer is no. If we look at an example of how the d3 axis object is used we can get a feel for why this is the case.

d3.select('body')
  .append('svg')
  .attr('class', 'axis')
  .attr('width', 1440)
  .attr('height', 30)
  .append('g')
  .attr('transform', 'translate(0,30)')
  .call(axis)

The axis object is passed a d3 selection to operate on. It will then append all the necessary elements with the correct attributes for us. If we dig into the source code we can see that manipulating the DOM is mixed up with the generation of the attributes. Don't get me wrong, this is not a criticism of the implementation, it works very well. It's simply that it brings up a roadblock for our approach of using d3 purely as a way to generate the necessary information to declaratively render our axis.

What are we to do? Two solutions jump to mind.

  • Use d3-axis as is but pass it a non-visible element to render to (or use react-faux-dom) and convert the output to a string
  • Re-implement d3-axis ourselves with an api suitable for our purposes

The first approach appears the saner of the two. On the other hand, the d3-axis implementation is short and this is just a blog post so let's see where this takes us.

In the spirit of separating concerns I will break the axis out into its own React component. The intention is for me to be able to write something like this.

<svg width="600px" height="200px">
  <path d={line(data)} fill="transparent" stroke="black" />
  <Axis orient="bottom" scale={x} label="Time" />
  <Axis orient="left" scale={y} label="Value" />
</svg>

You can see an actual implementation here: https://github.com/johnwalley/mortgage-calculator/blob/master/components/Axis.js.

It's not pretty but it gives you a feel for where this approach might go.

The people behind React D3 have obviously had similar questions around React/d3 integration but have taken this much, much further. Take a peek at their axis component: https://github.com/react-d3/react-d3-core/blob/master/src/axis/axis.jsx. In fact you'll see that they opted for the react-faux-dom approach.

You can find my proof of concept, using the above ideas, here: https://johnwalley.github.io/mortgage-calculator/. And source code here: https://github.com/johnwalley/mortgage-calculator.