React meets D3

Victor Mora
· 14 min read
By Rick Dikeman (Own work) [GFDL (http://www.gnu.org/copyleft/fdl.html) or CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0/)], via Wikimedia Commons

TLDR; We defined a set of classes that allow us to create beautiful and reusable charts in React using D3.

Last fall, our host team worked on the Host Hub, which provides owners some data about how they have been performing in form of different charts.

When one talks about rendering charts or any kind of data visualization in the web, D3.js is the de facto standard. So, if we want to build beautiful and reusable charts, we should definitely leverage the power of D3.

How do we integrate it?

But we have a problem, our web client is built in React and this library doesn’t get along well with D3: both use very different approaches to update the DOM. While React uses a virtual DOM and a well defined lifecycle to calculate and optimize DOM updates, D3 uses the browser DOM and data attributes.

If we want to build nice charts, with neat animations and don’t have our heads burst when dealing with SVG elements and their positioning, using D3 is a must, so we need to find a way so these two libraries can play well together.

What do we have to build?

We know we want to use D3 to build those data visualizations. But what are those charts that we have to build? And what are their requirements? How is the user going to interact with them?

  • A percentage / gauge chart. This chart can be in a circle or rectangle shape. Style changes depending on the value, but there is no user interaction with it.
  • A vertical stacked bar chart. When user hovers on a bar a tooltip appears. Clicking on the legend will hide / show the different series.
  • A horizontal bar chart. Technically this could be built using plain HTML and a little bit of CSS, but by using D3, we could leverage its animation power. Users can interact with the bars in order to filter the reviews shown below.
  • Future charts. There are plans to add more charts to the Host Hub in the future. Although this was outside of the scope of the current project, we should take it into account so our solution works in the future. We have longer term plans to potentially include new type of charts such as area charts.

What’s out there?

Once that we know the things we need to build, we can go out there and see if there is an existing library or approach that suits us and we can also see what other people are doing to solve this problem.

A simple google search of react d3 will show a bunch of already existing libraries and approaches on how to integrate both libraries.

To sum up, most of these libraries provide a set of plug and play chart components. They provide a lot more charts than we actually need and the customization options are very limited:

  • react-d3. The biggest react & d3 integration library is not maintained any longer (see here and here), so it is definitely not an option.
  • ReactD3. By looking at their landing page, they promote this library saying ‘you can build new d3 charts using React-d3 to create axis using xaxis component, yaxis component, label using label component, legend legend component… etc, in other words, it extends the flexibility of d3 library so you can assemble a whole new chart using core components’. It seems really promising, but after digging a little bit we realize that they don’t provide some of the things that we need (Gauge chart or enabling and disabling series in the bar chart), so we would have to build those components from scratch either way, as what they provide is a very very thin layer on top of D3 using [react-faux-dom](https://github.com/Olical/react-faux-dom). We would be adding tons of code that we wouldn’t even use and those custom components that we have to build would look pretty much the same whether we use the library or not.

Let’s roll our own charts

Using an existing library would limit our creativity to what they provide and wouldn’t save as much implementation time, so it seems that we have to build our own components. We are not the firsts that have had to do this and there is some literature talking about this topic.

There seems to be two main approaches when combining react and D3: D3 manipulates the DOM or react manipulates the DOM and D3 gives math support.

  • Using D3 for math and React render method for updating the work at first glance seems to be the best way to integrate both libraries, but we lose the ability to have transitions and animations. On the other hand, charts would be part of the React lifecycle and they could easily be reactive to props / state changes. If we are willing to leave transitions aside, you may think that this would be the way to go, but when charts get a little bit more complicated (nothing fancy here and just the stacked bar chart that our project demands), using D3 just for math and React for rendering isn’t that easy. Just for defining axis and grids D3 provides a plug an play API that we can no longer use, as we are the ones in charge of defining the SVG nodes and D3 only provides the math (converting data domains to SVG coordinates or creating paths *d* attributes). D3 is like a Ferrari and by removing its abilities to manipulate the DOM we are converting it into a Smart fortwo.
  • Use D3 for math and DOM updates. This approach is the one that let us use all the power that D3 has. We can use the D3 selection API to manipulate the DOM and and create React bindings so the charts look like react components for the people using them in other parts of the codebase. The main disadvantage of this approach is that we would create code that would be more complicated to maintain by people that haven’t worked that much with D3.

There are some blog posts comparing them both such as this and this one. At the end of the day, the decision is up to: leveraging the D3 power by building a less maintainable code for non D3 users or favor maintainability over more refined code.

I don’t believe in the maintainability argument. The one thing that we would get rid of by using D3 just for math is the `select` function, but we would still use all of the other math functions such as scales or curves. Most of the D3 tutorials start by showing how to use the `select` function and the importance of it

In practice, the only difference between both approaches is the lifecycle of the chart: rendered using the react render method and using the react lifecycle to perform updates via props or use D3 to render the chart and build a binding with the react lifecycle so it can still be updated via prop changes.

No matter what option we take, it would be abstracted from the final user as a React component. For instance, if we built the gauge chart, it doesn’t matter how it is built; its public API will be, for example, <GaugeChart percentage={0.92} desiredPercentage={0.9} />.

Now that we have decided how we are going to implement the charts, there is one more thing we need to decide on: how D3 is going to update the DOM? As I mentioned before, React uses a virtual DOM to minimize the updates in the browser DOM, which are really expensive. There is a way to have D3 update this virtual DOM and have react update the real DOM. This can be achieved using react-faux-dom), a library that provides a DOM like structure that can be passed to be rendered by react. This seems like the best approach, but we run into a problem: react-faux-dom API is limited and it would limit what D3 functionality we can use. If we are onboard with D3, let’s embrace it and not limit it.

While doing all this research, I came up with this article by Wealthfront’s engineering team, where they seem to have developed a similar approach to what we intend to do.

Testing

The one last thing we would need to consider before working on a prototype of these charts is testing. How do we ensure that the charts we build are correctly built and regression safe?

If we think about the final HTML rendered into the browser by one of these charts, it is going to be an SVG with a bunch of rects and paths with really weird and precise coordinates. Manually asserting that these values are correct could turn into something really hard.

This opens an opportunity to explore snapshot testing, which is something that we have been wanting to invest in for a while. This exploration is out of scope of this post, but it is worth mentioning.

A React managed vs a D3 component

While making these decisions that led to the final approach we are using to implement the charts, I also tried to build code examples of what things would look like using different approaches.

I started trying to build the same percentage chart using D3 just for math and also using D3 at its max. This is what that two charts ended up being:

The D3 version clearly looks way better than the React version. Both components have the same API, and they can be invoked like:

<PercentageChart
data={{
desiredPercetage: this.state.desiredPercentage,
percentage: this.state.percentage
}},
innerRadius={this.state.innerRadius}
outerRadius={this.state.outerRadius}
minAngle={deg2rad(this.state.minAngle)}
maxAngle={deg2rad(this.state.maxAngle)}
title="D3 Chart"
/>

But if we look at the implementation, they are completely different:

React version:

drawChart({ data, foregroundArc, angleScale, backgroundArc }) {
return (
<g className="arcs">
<path d={backgroundArc()} fill="#e6e6e6" />
<text
className="percentageChart-text"
fontSize="130"
fontFamily="Freight"
textAnchor="middle"
fill="#231f20"
>
{(data.percentage * 100).toFixed(0)}
</text>
<path
className="percentageChart-arc"
d={foregroundArc
.startAngle(angleScale(0))
.endAngle(angleScale(data.percentage))()}
/>
</g>
);
}

D3 version:

draw(chart, props) {
chart.attr(
"transform",
`translate(${this.props.width / 2}, ${this.props.height / 2})`
);
const arcs = chart.selectAll(".arcs").data(d => [d]);

const newArcs = arcs.enter().append("g").attr("class", "arcs");

newArcs
.append("path")
.attr("class", "percentageChart-backgroundArc")
.attr("d", props.backgroundArc)
.style("fill", "#e6e6e6");

const dataPath = newArcs
.append("path")
.attr("class", "percentageChart-arc")
.transition()
.duration(1000)
.delay(200)
.attr("d", props.foregroundArc)
.attrTween("d", this.tweenArc(props, true));

newArcs
.append("text")
.attr("class", "percentageChart-text")
.text(d => (d.percentage * 100).toFixed(0))
.attr("font-size", 130)
.attr("font-family", "Freight")
.attr("text-anchor", "middle")
.attr("fill", "#231f20");

arcs
.select(".percentageChart-text")
.text(d => (d.percentage * 100).toFixed(0));
arcs
.select(".percentageChart-arc")
.transition()
.duration(200)
.attrTween("d", this.tweenArc(props));

arcs
.select(".percentageChart-backgroundArc")
.attr("d", this.props.backgroundArc);
}

For this component, the react code looks simpler, cleaner and easier to understand if one doesn’t have much experience with D3. At this point, I was becoming convinced that getting rid of the animations for a simpler markup would be something worth considering. But, then the stacked bar chart came along ,and defining the SVG tags and attributes for that in React became something really complex, and I gave up in favor of just using D3.

Defining the grid or the axis would be something that would take a lot of time and code, while with D3 it would just be a few lines of code:

// Defining an axis
const generator = d3
.axisBottom(xScale)
.tickSizeOuter(0)
.tickSize(0)
.tickPadding(10)
.tickFormat(props.renderXAxisLabel);
// Rendering the axis
axis
.enter()
.append("g")
.attr("class", "axis")
.call(generator)

The chart component

At this point I was totally convinced that using pure D3 was a superior solution and I just started building the bar chart in a single React component. This component had a draw method that was getting called on initialization and after every prop change that happened in the component. This method receives three arguments:

  • The chart instance. This is the HTML node where the chart should be built (a g inside a svg).
  • The component props. Instead of having to access them via `this`, they get injected in the method (a cool feature I like from preact).
  • The component state. Same principle as with props.

Inside this draw method is where all the D3 code would act. Besides this method, the render method of this component would return an svg element which would be used to create the chart using the ref prop.

After some time experimenting with D3 I finally ended up having a chart that supported most of the needed features:

The chart looks really good, but there was a problem with the code: it was just one file with a lot of very tiny methods but that added up to 400 lines.

This is something that I didn’t like at all, and it goes against the principle of having small, single responsibility and reusable components that React promotes.

There were a lot of things that could be extracted into its own component. There is a legend, an x-axis, a y-axis, the grid, the lines and the bars. They could definitely be individual components, but the only thing that was there was the `draw` method and those bindings for the react lifecycle.

This is how the <Chart /> component was born. This component will render a svg tag and will create a chart object that will be passed down to its children and to its own draw method.

This draw function is also gonna be called every time the props or state of the chart component change. We can do that because of the way D3 works. The draw function will take care of creating new elements, updating the existing and deleting the ones that no longer exist.

The <Chart/> component will pass the chart instance to any of its children. These children have two common characteristics: their render method returns nothing (it is D3 handling the DOM and not React), and they must have a draw function that should get called whenever the component gets instantiated. This common behavior was moved into the <ChartComponent /> component.

Now, thanks to these two components, the 400 line file could be refactored into smaller chunks and we ended up having a really easy to understand and declarative chart:

render() {
const { data, height, width, margin, onLegendClick, series } = this.props;
return (
<Chart data={data} height={height} margin={margin} width={width}>
<Axis
animation={this.state.animation}
className="xAxis"
generator={this.state.xAxis}
orientation="BOTTOM"
/>
<Axis
animation={this.state.animation}
className="yAxis"
generator={this.state.yAxis}
orientation="RIGHT"
/>
<Axis
animation={this.state.animation}
className="grid"
generator={this.state.grid}
orientation="LEFT"
/>
<Bars
animation={this.state.animation}
stack={this.state.stack}
xScale={this.state.xScale}
yScale={this.state.yScale}
onMouseOver={this.handleBarMouseOver}
onMouseOut={this.handleBarMouseOut}
seriesColor={this.state.seriesColor}
/>
<Lines
animation={this.state.animation}
data={this.state.linesData}
line={this.state.line}
xScale={this.state.xScale}
/>
<Legend
onClick={onLegendClick}
series={series}
seriesColor={this.state.seriesColor}
/>
</Chart>
);
}

These individual components extend the <ChartComponent/> component. This is what the <Axis /> looks like:

export default class Axis extends ChartComponent {  
draw(chart, { className, generator, animation, width, height, orientation }) {
const axis = chart.selectAll(`.${className}`).data(d => [d]);
axis
.enter()
.append("g")
.attr("class", className)
.call(generator)
.attr(
"transform",
Axis.getAxisTransformation({ width, height, orientation })
);
axis
.transition()
.duration(animation.duration)
.delay(animation.start)
.call(generator);
}
}

If a chart is just one component, instead of declaring the chart, our component could extend the <Chart /> component and override the `draw` function:

class GaugeChart extends Chart {  
draw(chart, props) {
chart.attr(
"transform",
`translate(${this.props.width / 2}, ${this.props.height / 2})`
);
const arcs = chart.selectAll(".arcs").data(d => [d]);

const newArcs = arcs.enter().append("g").attr("class", "arcs");
...
}
}

Using D3 we can add event handlers to the elements that we create. That means that we have to be very cautious when deleting these elements from the DOM in order to not end up having zombie views or memory leaks when the component gets unmounted. The same way React has a componentWillUnmount method that gets called right before an element is gonna be destroyed, the chart components should have something similar.

Instead of defining the componentWillUnmount method in every chart component, I decided to define a new method. This allows two things: clarify that these components are not regular React components, and we can inject the chart, props and state to the destructor method. This method is called clear and it should take care of deleting any event handler that has been added to the DOM by D3. This is the clear method of <Legend/>:

clear(chart) {  chart.selectAll(".legend > .tag > rect").on("click", null);}

Now we have the toolset needed to create any type of chart using D3 but are still able to expose them as regular React components and allow people to expect them to behave just like any other component. In fact, even if we look at the react dev tools, they will appear as regular components:

We have two components that help us link react with D3 and ease the creation of the initial SVG tag: The <Chart/> and <ChartComponent /> components, and we have a well defined and differentiated from React lifecycle: draw and clear, which will get all the data that they may need injected.

Layers on top of the chart

Users can interact with the chart. For instance, in the stacked bar chart mentioned in this post, users can enable / disable the different series, but also hovering on any of the charts should show a pop up with more details about that specific bar. This popup is just HTML tags, and it should not be part of the SVG content: we can only define HTML tags inside a SVG tag using the foreign object and that is something that IE doesn’t handle very well.

We need to have the ability to have layers that position on top of the chart. The <Chart /> component now needs to accept elements that do not extend <ChartComponent /> and treat them differently. In order to position these elements, we need to have a way to convert SVG coordinates to the DOM top and left absolute positioning. This is not trivial, as the SVG tag the <Chart /> component creates uses the viewBox attribute that let us create responsive charts.

Thankfully, there is an easy way to do this. Since every SVG item is also a DOM element, we can call getBoundingClientRect on them and get its document top and left position, which can then be passed to the upper component so it can position the layers on top of the chart as desired.

This is how the coordinates of a hovered bar are calculated so the tooltip can be shown where it needs to be:

.on("mouseover", function(d) {
const boundingRect = this.getBoundingClientRect();
props.onMouseOver({
position: {
top: boundingRect.top + window.scrollY,
left: boundingRect.left + window.scrollX + boundingRect.width / 2
},
data: d
});
})

Then we can add any HTML inside the chart and absolutely position it the way we want on top of it:

...
<Legend
onClick={onLegendClick}
series={series}
seriesColor={this.state.seriesColor}
/>
{this.state.showTooltip &&
<div
style={{
...this.state.tooltipPosition,
width: 80,
height: 80,
marginTop: -(10 + 80),
marginLeft: -(80 / 2),
position: "absolute",
background: "green"
}}
/>}
</Chart>

Conclusion

In this post I’ve tried to describe the thought process towards designing an approach that can help us build reusable charts. This approach defines some base components that abstract us from binding the charts with React, and it also defines a simple lifecycle to create, update and destroy these charts.

This approach is born after analyzing what existing libraries and solutions are out there, what different approaches have been taken by other people and what the requirements for our next project are.

It embraces D3 instead of trying to limit it and transform it into something it is not supposed to be.

It definitely has some setbacks — a new component lifecycle and having to know D3 to actually build stuff — but I believe this approach can work, and I am really happy with the results of the final charts.

Resources

These are some resources that I found useful while researching the React and D3 integration:

Turo Engineering

Turo Engineering & Data Science

Victor Mora

Written by

Turo Engineering

Turo Engineering & Data Science

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade