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 by creating a new React project by running the following commands:
1npx create-react-app my-app
2cd my-app
3yarn add d3
4yarn start
These commands will set up a new React project with D3.js installed.
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.
1import React from 'react';
2import * as d3 from 'd3';
3
4function BarChart = () => {
5 return (
6 <svg
7 style={{
8 height: 500,
9 width: "100%",
10 marginRight: "0px",
11 marginLeft: "0px",
12 }}
13 >
14 <g className="plot-area" />
15 <g className="x-axis" />
16 <g className="y-axis" />
17 </svg>
18 );
19}
20
21export default BarChart;
This boilerplate sets up a basic scaffold for rendering an svg
with a plot area and chart axis.
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.
1import React from 'react';
2import * as d3 from 'd3';
3
4export const useD3 = (renderChartFn, dependencies) => {
5 const ref = React.useRef();
6
7 React.useEffect(() => {
8 renderChartFn(d3.select(ref.current));
9 return () => {};
10 }, dependencies);
11 return ref;
12}
This function is a hook that accepts two arguments:
renderChartFn
is a callback that contains your D3.js code to be executeddependencies
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.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.
1import { useD3 } from './hooks/useD3';
2import React from 'react';
3import * as d3 from 'd3';
4
5function BarChart({ data }) {
6 const ref = useD3(
7 (svg) => {
8 const height = 500;
9 const width = 500;
10 const margin = { top: 20, right: 30, bottom: 30, left: 40 };
11
12 const x = d3
13 .scaleBand()
14 .domain(data.map((d) => d.year))
15 .rangeRound([margin.left, width - margin.right])
16 .padding(0.1);
17
18 const y1 = d3
19 .scaleLinear()
20 .domain([0, d3.max(data, (d) => d.sales)])
21 .rangeRound([height - margin.bottom, margin.top]);
22
23 const xAxis = (g) =>
24 g.attr("transform", `translate(0,${height - margin.bottom})`).call(
25 d3
26 .axisBottom(x)
27 .tickValues(
28 d3
29 .ticks(...d3.extent(x.domain()), width / 40)
30 .filter((v) => x(v) !== undefined)
31 )
32 .tickSizeOuter(0)
33 );
34
35 const y1Axis = (g) =>
36 g
37 .attr("transform", `translate(${margin.left},0)`)
38 .style("color", "steelblue")
39 .call(d3.axisLeft(y1).ticks(null, "s"))
40 .call((g) => g.select(".domain").remove())
41 .call((g) =>
42 g
43 .append("text")
44 .attr("x", -margin.left)
45 .attr("y", 10)
46 .attr("fill", "currentColor")
47 .attr("text-anchor", "start")
48 .text(data.y1)
49 );
50
51 svg.select(".x-axis").call(xAxis);
52 svg.select(".y-axis").call(y1Axis);
53
54 svg
55 .select(".plot-area")
56 .attr("fill", "steelblue")
57 .selectAll(".bar")
58 .data(data)
59 .join("rect")
60 .attr("class", "bar")
61 .attr("x", (d) => x(d.year))
62 .attr("width", x.bandwidth())
63 .attr("y", (d) => y1(d.sales))
64 .attr("height", (d) => y1(0) - y1(d.sales));
65 },
66 [data.length]
67 );
68
69 return (
70 <svg
71 ref={ref}
72 style={{
73 height: 500,
74 width: "100%",
75 marginRight: "0px",
76 marginLeft: "0px",
77 }}
78 >
79 <g className="plot-area" />
80 <g className="x-axis" />
81 <g className="y-axis" />
82 </svg>
83 );
84}
85
86export default BarChart;
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.
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:
1import React from 'react';
2import BarChart from './BarChart';
3import './App.css';
4
5const data = [
6 {year: 1980, efficiency: 24.3, sales: 8949000},
7 {year: 1985, efficiency: 27.6, sales: 10979000},
8 {year: 1990, efficiency: 28, sales: 9303000},
9 {year: 1991, efficiency: 28.4, sales: 8185000},
10 {year: 1992, efficiency: 27.9, sales: 8213000},
11 {year: 1993, efficiency: 28.4, sales: 8518000},
12 {year: 1994, efficiency: 28.3, sales: 8991000},
13 {year: 1995, efficiency: 28.6, sales: 8620000},
14 {year: 1996, efficiency: 28.5, sales: 8479000},
15 {year: 1997, efficiency: 28.7, sales: 8217000},
16 {year: 1998, efficiency: 28.8, sales: 8085000},
17 {year: 1999, efficiency: 28.3, sales: 8638000},
18 {year: 2000, efficiency: 28.5, sales: 8778000},
19 {year: 2001, efficiency: 28.8, sales: 8352000},
20 {year: 2002, efficiency: 29, sales: 8042000},
21 {year: 2003, efficiency: 29.5, sales: 7556000},
22 {year: 2004, efficiency: 29.5, sales: 7483000},
23 {year: 2005, efficiency: 30.3, sales: 7660000},
24 {year: 2006, efficiency: 30.1, sales: 7762000},
25 {year: 2007, efficiency: 31.2, sales: 7562000},
26 {year: 2008, efficiency: 31.5, sales: 6769000},
27 {year: 2009, efficiency: 32.9, sales: 5402000},
28 {year: 2010, efficiency: 33.9, sales: 5636000},
29 {year: 2011, efficiency: 33.1, sales: 6093000},
30 {year: 2012, efficiency: 35.3, sales: 7245000},
31 {year: 2013, efficiency: 36.4, sales: 7586000},
32 {year: 2014, efficiency: 36.5, sales: 7708000},
33 {year: 2015, efficiency: 37.2, sales: 7517000},
34 {year: 2016, efficiency: 37.7, sales: 6873000},
35 {year: 2017, efficiency: 39.4, sales: 6081000},
36]
37
38function App() {
39 return (
40 <div className="App">
41 <header className="App-header">
42 <BarChart data={data} />
43 </header>
44 </div>
45 );
46}
47
48export default App;
This code snippet uses static data from a JavaScript variable to bind to your BarChart
component.
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.