Author avatar

Benney Au

Using D3.js Inside a React App

Benney Au

  • Sep 21, 2020
  • 9 Min read
  • 79 Views
  • Sep 21, 2020
  • 9 Min read
  • 79 Views
Web Development
Front End Web Development
Client-side Frameworks
React

Introduction

React and D3.js are two very popular JavaScript libraries. React is a library for building dynamic user interfaces, while D3.js is used for creating interactive, data-driven visualizations such as charts.

Although both libraries are widely used, integrating them presents some challenges. This is because: 1. Both libraries have changed and evolved substantially over time, and 2. Both libraries want direct access to the document object model (DOM)

In this guide, you will learn how to integrate these two libraries. You will learn how to encapsulate your D3.js logic in a chart component; then we will discuss how to make sure the chart is updated as new data arrives.

The guide will use a simplified version of Mike Bostock's Bar Line Chart as a sample D3.js chart to integrate into your app. We will focus our attention on the integration points rather than implementation details of React and D3.js.

Start a New React Project

Start by creating a new React project by running the following commands:

1
2
3
4
npx create-react-app my-app
cd my-app
yarn add d3
yarn start

These commands will set up a new React project with D3.js installed.

Create a Bar Chart Component

React uses components as the building blocks for composing user interfaces. As such, your D3.js chart should also be modeled as a component that can be added to different parts of your website.

To do this, create a new file named src/BarChart.js. Inside this file, add the following boilerplate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import * as d3 from 'd3';

function BarChart = () => {
  return (
    <svg
      style={{
        height: 500,
        width: "100%",
        marginRight: "0px",
        marginLeft: "0px",
      }}
    >
      <g className="plot-area" />
      <g className="x-axis" />
      <g className="y-axis" />
    </svg>
  );
}

export default BarChart;
js

This boilerplate sets up a basic scaffold for rendering an svg with a plot area and chart axis.

Create a Custom Hook to use D3js

React hooks are one way to add an imperative escape hatch to allow D3.js to interact directly with the DOM. In the previous step, you used standard JSX to render an svg element as a starting point. From here, you can take advantage of the useRef and useEffect hook to link D3.js with the svg element that has been created, and specify when your D3.js function should be executed.

By convention, hooks are prefixed with use, so you should call your new hook useD3. Create a new file in src/hooks/useD3.js and add the following function.

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import * as d3 from 'd3';

export const useD3 = (renderChartFn, dependencies) => {
    const ref = React.useRef();

    React.useEffect(() => {
        renderChartFn(d3.select(ref.current));
        return () => {};
      }, dependencies);
    return ref;
}
js

This function is a hook that accepts two arguments:

  • renderChartFn is a callback that contains your D3.js code to be executed
  • dependencies is a fixed-length array to tell React when to run the renderChartFn. This is useful for preventing unnecessary re-rendering and updating the chart correctly when new data arrives.

Using the Custom Hook in the BarChart Component

React hooks are just functions, so they can be called directly from your BarChart component. An updated version of the BarChart component using the hook is demonstrated below.

The code below uses the hook by supplying two arguments and linking the ref returned by the hook with the svg element.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import { useD3 } from './hooks/useD3';
import React from 'react';
import * as d3 from 'd3';

function BarChart({ data }) {
  const ref = useD3(
    (svg) => {
      const height = 500;
      const width = 500;
      const margin = { top: 20, right: 30, bottom: 30, left: 40 };

      const x = d3
        .scaleBand()
        .domain(data.map((d) => d.year))
        .rangeRound([margin.left, width - margin.right])
        .padding(0.1);

      const y1 = d3
        .scaleLinear()
        .domain([0, d3.max(data, (d) => d.sales)])
        .rangeRound([height - margin.bottom, margin.top]);

      const xAxis = (g) =>
        g.attr("transform", `translate(0,${height - margin.bottom})`).call(
          d3
            .axisBottom(x)
            .tickValues(
              d3
                .ticks(...d3.extent(x.domain()), width / 40)
                .filter((v) => x(v) !== undefined)
            )
            .tickSizeOuter(0)
        );

      const y1Axis = (g) =>
        g
          .attr("transform", `translate(${margin.left},0)`)
          .style("color", "steelblue")
          .call(d3.axisLeft(y1).ticks(null, "s"))
          .call((g) => g.select(".domain").remove())
          .call((g) =>
            g
              .append("text")
              .attr("x", -margin.left)
              .attr("y", 10)
              .attr("fill", "currentColor")
              .attr("text-anchor", "start")
              .text(data.y1)
          );

      svg.select(".x-axis").call(xAxis);
      svg.select(".y-axis").call(y1Axis);

      svg
        .select(".plot-area")
        .attr("fill", "steelblue")
        .selectAll(".bar")
        .data(data)
        .join("rect")
        .attr("class", "bar")
        .attr("x", (d) => x(d.year))
        .attr("width", x.bandwidth())
        .attr("y", (d) => y1(d.sales))
        .attr("height", (d) => y1(0) - y1(d.sales));
    },
    [data.length]
  );

  return (
    <svg
      ref={ref}
      style={{
        height: 500,
        width: "100%",
        marginRight: "0px",
        marginLeft: "0px",
      }}
    >
      <g className="plot-area" />
      <g className="x-axis" />
      <g className="y-axis" />
    </svg>
  );
}

export default BarChart;
js

The first argument supplied to the useD3 hook has been adapted from Mike Bostock's Bar Chart Sample. In the code snippet above, it joins the data variable from the component's props to chart elements. Note that the function does not directly use .append since the chart render function can be run many times and we want to avoid duplicate elements from being created. Instead, it uses selection joins to determine which elements should be created, removed, or modified.

The second argument is a dependency array that React inspects to check if the chartRenderFn needs to be re-executed. In this case we use [data.length]. This will cause React to update the chart if data items are added or removed. If you simply pass the entire data array [data] to the dependencies argument, you may find your function gets executed an excessive number of times. This is because React by default does a shallow comparison of all the elements in the dependencies array. In other scenarios where data items are being modified, you may need to change this argument to a content hash or last-updated time stamp for your chart to be updated correctly.

Using Your new BarChart Component

You have created a BarChart component. It takes data as props renders a BarChart. To see it in action, modify your src/App.js file to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React from 'react';
import BarChart from './BarChart';
import './App.css';

const data = [
  {year: 1980, efficiency: 24.3, sales: 8949000},
  {year: 1985, efficiency: 27.6, sales: 10979000},
  {year: 1990, efficiency: 28, sales: 9303000},
  {year: 1991, efficiency: 28.4, sales: 8185000},
  {year: 1992, efficiency: 27.9, sales: 8213000},
  {year: 1993, efficiency: 28.4, sales: 8518000},
  {year: 1994, efficiency: 28.3, sales: 8991000},
  {year: 1995, efficiency: 28.6, sales: 8620000},
  {year: 1996, efficiency: 28.5, sales: 8479000},
  {year: 1997, efficiency: 28.7, sales: 8217000},
  {year: 1998, efficiency: 28.8, sales: 8085000},
  {year: 1999, efficiency: 28.3, sales: 8638000},
  {year: 2000, efficiency: 28.5, sales: 8778000},
  {year: 2001, efficiency: 28.8, sales: 8352000},
  {year: 2002, efficiency: 29, sales: 8042000},
  {year: 2003, efficiency: 29.5, sales: 7556000},
  {year: 2004, efficiency: 29.5, sales: 7483000},
  {year: 2005, efficiency: 30.3, sales: 7660000},
  {year: 2006, efficiency: 30.1, sales: 7762000},
  {year: 2007, efficiency: 31.2, sales: 7562000},
  {year: 2008, efficiency: 31.5, sales: 6769000},
  {year: 2009, efficiency: 32.9, sales: 5402000},
  {year: 2010, efficiency: 33.9, sales: 5636000},
  {year: 2011, efficiency: 33.1, sales: 6093000},
  {year: 2012, efficiency: 35.3, sales: 7245000},
  {year: 2013, efficiency: 36.4, sales: 7586000},
  {year: 2014, efficiency: 36.5, sales: 7708000},
  {year: 2015, efficiency: 37.2, sales: 7517000},
  {year: 2016, efficiency: 37.7, sales: 6873000},
  {year: 2017, efficiency: 39.4, sales: 6081000},
]

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <BarChart data={data} />
      </header>
    </div>
  );
}

export default App;
js

This code snippet uses static data from a JavaScript variable to bind to your BarChart component.

Conclusion

This guide demonstrated how to integrate D3.js with React. You created a BarChart component to encapsulate your D3.js logic and used React Hook's dependency array to control when the chart gets re-rendered.

1