Blog

Building D3 Components with React: Part 1

Introduction

D3 is a powerful library for creating visualizations with JavaScript. While it allows a high-level of customizations, it can be challenging to create isolated, declarative components. Thankfully, this is something React does really well. While there are libraries for integrating the two, I’ve found creating a custom integration to work best for my purposes. We’ll walk through a basic example of how this integration works as well as some lessons learned along the way.

Up & Running

NOTE: While we’ll be walking through the details of this implementation, the following assumes you have some basic knowledge of React (specifically the component lifecycle) and some knowledge of D3. It’s also important to note that we’re using D3 4.x, which is a little different than 3.x.

d3-components-with-react

We’ll be building basic progress arc. We’ll be using the create-react-app CLI, but I’ve added an example repo here. If you don’t have create-react-app already, you’ll need to install it with:

npm install -g create-react-app
create-react-app react-d3-example
cd react-d3-example
npm install --save d3

Now run npm start. If you see this in the browser, you’re good to go!

d3-components-with-react

Setting our Context

First let’s add our D3 component:

touch src/ProgressArc.js

This is what it will look like:

import React, { Component } from 'react';
import * as d3 from "d3";
class ProgressArc extends Component {
  render() {
    return (
 

    )
  }
}
export default ProgressArc;

Let’s empty the contents of src/App.js and call our ProgressArc.

import React, { Component } from 'react';
import ProgressArc from './ProgressArc';
class App extends Component {
  render() {
    return (
      
    );
  }
}
export default App;

Great! Now we’ll add our SVG context to src/ProgressArc.js.

...
class ProgressArc extends Component {
  componentDidMount() {
    this.setContext();
  }
  setContext() {
    return d3.select(this.refs.arc).append('svg')
      .attr('height', '300px')
      .attr('width', '300px')
      .attr('id', 'd3-arc')
      .append('g')
      .attr('transform', `translate(150, 150)`);
  }
...

The context here is the SVG canvas where we’ll be drawing our visualization. When the component mounts, we’re appending a SVG, setting the height and width, and centering the content. Eventually we’ll make height, width, and id dynamic props to be passed into the component.

Setting our Background

...
class ProgressArc extends Component {
  componentDidMount() {
    const context = this.setContext();
    this.setBackground(context);
  }
  ...
  setBackground(context) {
    return context.append('path')
    .datum({ endAngle: this.tau })
    .style('fill', '#e6e6e6')
    .attr('d', this.arc());
  }
  tau = Math.PI * 2;
  arc() {
    return d3.arc()
      .innerRadius(100)
      .outerRadius(110)
      .startAngle(0)
  }

  ...

There’s quite a bit going on here, so let’s break it down. After we create our context, we’re appending out background on top of it. Specifically, we’re appending a path which we can shape and style. The datum attribute is telling this path where to end, which we’re defining as tau.

NOTE: We’re defining tau as 2π here. D3 uses radians to measure arc length, which is fine, except most datasets don’t come in terms of radians, and to keep things simple, we’ll use tau as a way to quickly convert radians to percent. All you really need to know about this is that the circumference of any circle is equal to 2 x π. We can then multiply tau by a percentage and get a visualization that matches our expectations. (If I pass in 0.50, I should expect to see the path be a semi-circle.)

Since we want our background to be a full circle, we’ll set it to tau and fill it with a light gray. This path will follow the path we are defining inthis.arc().

this.arc() is returning a D3 arc that we are telling to start at 0 (the top, center of the circle) and has an inner-radius of 100px and an outer-radius or 110px. All of these properties will be things we can set as dynamic properties later when we call the component. If everything went well, you should see something like this when the view re-renders.
d3-components-with-react

Setting Our Foreground

Now that we’ve created our background, we can add our foreground, which will display the percentage of our progress. Similar tosetBackground(), we’ll call the function in componentDidMount() and write the function below.

...
componentDidMount() {
  const context = this.setContext();
  this.setBackground(context);
  this.setForeground(context);
}
...
setForeground(context) {
  return context.append('path')
    .datum({ endAngle: this.tau * 0.3 })
    .style('fill', '#00ff00')
    .attr('d', this.arc());
}

We’re setting the end angle for the foreground path to be 30% of the circumference and have a green fill. If everything went well, it should look like this:

d3-components-with-react

Adding Dynamic Props

Now that we have the basic setup, let’s refactor a bit to make these properties dynamic. First we’ll declare our props in src/App.js.

...
class App extends Component {
  render() {
    return (
      
    );
  }
}
...

Let’s update ProgressArc to use props. We’ll also add propTypes and displayName as a general best practice.

...
class ProgressArc extends Component {
  displayName: 'ProgressArc'; 
  propTypes: {
    id: PropTypes.string,
    height: PropTypes.number,
    width: PropTypes.number,
    innerRadius: PropTypes.number,
    outerRadius: PropTypes.number,
    backgroundColor: PropTypes.string,
    foregroundColor: PropTypes.string,
    percentComplete: PropTypes.number
  }
  componentDidMount() {
    const context = this.setContext();
    this.setBackground(context);
    this.setForeground(context);
  }
  setContext() {
    const { height, width, id} = this.props;
    return d3.select(this.refs.arc).append('svg')
      .attr('height', height)
      .attr('width', width)
      .attr('id', id)
      .append('g')
      .attr('transform', `translate(${height / 2}, ${width / 2})`);
  }
  setBackground(context) {
    return context.append('path')
    .datum({ endAngle: this.tau })
    .style('fill', this.props.backgroundColor)
    .attr('d', this.arc());
  }
  setForeground(context) {
    return context.append('path')
    .datum({ endAngle: this.tau * this.props.percentComplete })
    .style('fill', this.props.foregroundColor)
    .attr('d', this.arc());
  }
  tau = Math.PI * 2;
  arc() {
    return d3.arc()
      .innerRadius(this.props.innerRadius)
      .outerRadius(this.props.outerRadius)
      .startAngle(0)
  }
...

Great! We’ve created a declarative, isolated component for D3! We (or another developer) could easily call this component somewhere else, pass in the desired properties, and have confidence it would work consistently. But there is another aspect we should tie up first. We should also create a way for this component to respond to updates.

Adding Updates

React provides a powerful API for updating components to reflect state changes via the VirtualDOM. Let’s add a toggle button to update the ProgressArc percentComplete prop in src/App.js.

class App extends Component {
  constructor(props) {
     super(props);
     this.state = {percentComplete: 0.3};
     this.togglePercent = this.togglePercent.bind(this);
   }
  togglePercent() {
    const percentage = this.state.percentComplete === 0.3 ? 0.7 : 0.3;
    this.setState({percentComplete: percentage});
  }
  render() {
  console.log(this.state.percentComplete);  
  return (

    );
  }
}

There’s a bit going on here. We’ve added a toggle button that calls togglePercent() to toggle the state from 0.3 to 0.7 percent when clicked. We’ve also added an ES6 constructor to set the initial state for the App component and a console.log() in render() as a sanity check to ensure the state is updating. However, after the view re-renders, you may notice an issue when the button is clicked. While we can see the state update in the JS console, the ProgressArc does not. Why?

This is where we get into some lessons learned. While React is updating the state and ProgressArc component as we would expect, the SVG does not reflect that change. This is because SVG’s don’t respond to updates. So we have to remove the initial SVG and re-draw a new one. To do this, we’ll need to modify how the ProgressArc handles updates.

First we’re going to roll up all of our functionality in componentDidMount() into a function called drawArc().

class ProgressArc extends Component {
...
componentDidMount() {
  this.drawArc();
}
drawArc() {
  const context = this.setContext();
  this.setBackground(context);
  this.setForeground(context);
}
...

Now we’ll add the functions componentDidUpdate() and redrawArc() to handle updates to the percentComplete prop.

  ...
  componentDidUpdate() {
    this.redrawArc();
  }
  drawArc() {
    ...
  }
  redrawArc() {
    const context = d3.select(`#${this.props.id}`);
    context.remove();
    this.drawArc();
  }
  ...

We’re selecting the SVG context by the id prop we set earlier, removing it from the DOM, and calling drawArc() to redraw with the new data available. Thankfully, D3 does this so quickly, it will look like the same context is simply updating based on the new data. If all is well, we should see something like this:

d3-components-with-react

Cool! Now we have an isolated, declarative D3 component that responds to updates. We’ve abstracted the internals of how D3 works and created a visualization that can easily be used by anyone!

Part 2: Adding Animation

If you’d like to take this a step further and add animation to this transition, head over to Part 2!