3 ways to integrate React and D3

3 ways to integrate React and D3

by Peter Cook / 02 Aug 2018    

Prerequisites: Moderate experience with React and D3.

React is one of the most popular JavaScript libraries for building user interfaces. It’s strengths are its component system (allowing an application to be split into manageable parts) and its use of JSX which, in effect, allows you to specify your components using HTML.

D3 on the other hand is one of the most popular JavaScript libraries for visualising data. It’s strengths are manyfold: it has an efficient approach to updating the DOM (i.e. adding, removing and updating DOM elements), it has several functions for laying out data (line series, treemaps, sunburst, networks, geographic etc.) and it has some very useful functions for manipulating data.

Why might we want to use both libraries together? Each library has particular strengths which the other doesn’t provide. React has a simple component-based approach to building user interfaces and manipulating the DOM (D3 isn’t really geared towards building UI components) while D3 offers a multitude of data transformation and visualisation functions (React isn’t really geared towards data visualisation).

We’ll look at 3 different approaches to integrating D3 and React by creating a simple bubble chart:

Bubble chart

Our 3 approaches are:

  • D3 manages the chart (D3-oriented approach)
  • React manages the chart while D3 is used as a ‘utility’ library (React-oriented approach)
  • React handles element creation/removal while D3 manages style and attributes (hybrid approach)

D3-oriented approach: D3 manages the chart

This is one of the most common approaches to integrating React and D3. In short we:

  • create a container element in React for the bubble chart
  • add D3 code for creating and updating the bubble chart
  • call our D3 code on initial load (componentDidMount) and updates (componentDidUpdate)

Let’s start by creating our React component and add an SVG element that’ll act as our chart container:

class Chart extends React.Component {
  constructor(props) {
    super(props)

    this.state = {
      data: getData()
    }
  }

  render() {
    return <div>
             <svg
               width={this.props.width}
               height={this.props.height}
               ref={el => this.svgEl = el} >
             </svg>
           </div>
  }
}

ReactDOM.render(
  <Chart width={800} height={600} />,
  document.getElementById('app')
)

We’ll store our data on the component’s state object and initialise it using getData() (which is a function that returns a randomised array of data).

Note we’ve used React’s ref feature to allow us to reference the SVG element later on.

We’ll now add a function that manages the chart using D3:

updateChart() {
  let maxRadius = 40
  let xScale = d3.scaleLinear().domain([0, 1]).range([0, this.props.width])
  let yScale = d3.scaleLinear().domain([0, 1]).range([0, this.props.height])
  let rScale = d3.scaleLinear().domain([0, 1]).range([0, maxRadius])

  let u = d3.select(this.svgEl)
    .selectAll('circle')
    .data(this.state.data)

  u.enter()
    .append('circle')
    .merge(u)
    .attr('cx', d => xScale(d.x))
    .attr('cy', d => yScale(d.y))
    .attr('r', d => rScale(d.r))
    .style('fill', d => colours[d.colour])

  u.exit().remove()
}

For those experienced with D3 this code should be fairly familiar as it uses the general update pattern.

A key point of contact between React and D3 is the line:

let u = d3.select(this.svgEl)

which selects the SVG element.

Our final step is to call updateChart() when the component is created and updated:

componentDidMount() {
  this.updateChart()
}

componentDidUpdate() {
  this.updateChart()
}

Now any time the state changes, updateChart() is called and the chart updates.

One of the key benefits of managing the chart using D3 is that we can easily add transitions to the updates:

u.enter()
  .append('circle')
  .merge(u)
  .transition()
  .duration(1000)
  .attr('cx', d => xScale(d.x))
  .attr('cy', d => yScale(d.y))
  .attr('r', d => rScale(d.r))
  .style('fill', d => colours[d.colour])

Here’s the finished code:

See the Pen React+D3 (D3 manages DOM) by Frontend Charts (@frontendcharts) on CodePen.

React-oriented approach: React manages the chart

We’ll now look at an approach where React is responsible for creating and updating the chart, using D3 for ancillary functions such as scaleLinear.

This is the simplest of all the approaches as our chart code can be expressed largely in JSX:

render() {
  let maxRadius = 40
  let xScale = d3.scaleLinear().domain([0, 1]).range([0, this.props.width])
  let yScale = d3.scaleLinear().domain([0, 1]).range([0, this.props.height])
  let rScale = d3.scaleLinear().domain([0, 1]).range([0, maxRadius])

  let points = this.state.data.map(d => <circle
                                          cx={xScale(d.x)}
                                          cy={yScale(d.y)}
                                          r={rScale(d.r)}
                                          fill={colours[d.colour]} />)

  return <div>
           <svg width={this.props.width} height={this.props.height}>{points}</svg>
         </div>
}

This is probably the most straightforward way of intergrating React and D3, especially if we already have a React application as we can continue working within the React paradigm.

One thing to watch out for are transitions. Whereas in D3 transitions are very easy to add, React doesn’t have transitions built in so a transition plug-in will be required and there’s quite a few of them in existence.

See the finished code on Codepen:

See the Pen React+D3 (React manages chart) by Frontend Charts (@frontendcharts) on CodePen.

Hybrid approach: React for element creation, D3 for updates

The hybrid approach plays to each library’s strengths: React for specifying the DOM structure, and D3 for updating style (e.g. colour) and attributes (e.g. shape and position) meaning that we can harness D3’s transitions. (If we weren’t bothered about transitions this approach probably isn’t necessary.)

Our render() function now looks like:

render() {
  let circles = this.state.data.map(d => <circle />)

  return <div>
           <svg width={this.props.width} height={this.props.height} ref={el => this.svgEl = el}>{circles}</svg>
         </div>
}

Note that all render() does is add the circles to the DOM - it isn’t responsible for updating any of the circles’ style or attributes.

The responsibility for updating style and attributes is instead taken by D3. We’ll add a function that makes a selection of the circles, joins the data (this.state.data) and updates style and attributes:

updateStyleAndAttrs() {
  let maxRadius = 40
  let xScale = d3.scaleLinear().domain([0, 1]).range([0, this.props.width])
  let yScale = d3.scaleLinear().domain([0, 1]).range([0, this.props.height])
  let rScale = d3.scaleLinear().domain([0, 1]).range([0, maxRadius])

   d3.select(this.svgEl)
    .selectAll('circle')
    .data(this.state.data)
    .transition()
    .duration(1000)
    .attr('cx', d => xScale(d.x))
    .attr('cy', d => yScale(d.y))
    .attr('r', d => rScale(d.r))
    .style('fill', d => colours[d.colour])
}

Here we’ll using D3 just to update style and attributes. (We don’t need to use D3’s enter() and exit() functions because React is taking care of adding and removing elements.)

Finally we’ll call updateStyleAndAttrs() on component creation and update:

componentDidMount() {
  this.updateStyleAndAttrs()
}

componentDidUpdate() {
  this.updateStyleAndAttrs()
}

See the finished code on Codepen:

See the Pen React+D3 (D3 updates style and attrs w/transition) by Frontend Charts (@frontendcharts) on CodePen.

Summary

We’ve shown 3 approaches to integrating React and D3. The pros and cons of each approach can be quite subtle.

The React-oriented approach is on the surface the most attractive as it leaves the complex job of managing the DOM to React. (This is especially attractive when there’s a lot of element nesting, which is nearly always the case!) However, out-of-the-box React doesn’t support transitions so if we wanted transitions we’d have to use a plug-in such as React Move.

The D3-oriented approach is a common approach as it results in a clean break between the D3 and React code. It’s also an attractive approach if wanting to integrate existing D3 code into a React application.

The hybrid approach seems to be a less explored option, nonetheless it looks a compelling one. Using React to add/remove elements sidesteps D3’s enter/exit paradigm while using D3 to update attributes and style allows us to use D3’s powerful and efficient transitions.

Ultimately the choice of approach depends on the particular use case. Some of the questions you should ask are:

  • do you have an existing application build with React?
  • how important are transitionsin your application?
  • do you already have a visualisation built with D3?
  • how experienced with React/D3 are your developers?

I’m frequently working with both React and D3 so do get in touch if you’d like a hand with either!