D3 is one of the most-used visualization frameworks available for Javascript. For a developer coming from a vanilla JS background, D3 might be useful beyond the everyday visualizations. But for a React developer, the benefits of D3 are not immediately approachable because of the way React and D3 handle the DOM. This guide explores how we could integrate and maximize the benefit of D3 with React without interrupting the mechanisms of React. All examples discussed are available at the Github repo for your reference.
In practice, when you need a D3 visualization you would probably modify an existing code sample from D3 documentation or a blog. Following the same approach, you will first go through a sample for a simple bar chart in pure D3. Then you will explore the methods of integrating the same code sample into React.
The D3 code below performs the following actions:
svg
to the document, which will later be used as the canvas for drawing the charts. svg
element and draws a border. enter-update-exit
pattern of D3. If you are not familiar with these concepts, please check out this visualization. 1// chart.html
2
3<!DOCTYPE html>
4<head>
5 <meta charset="utf-8">
6 <script src="https://d3js.org/d3.v4.min.js"></script>
7</head>
8
9<body>
10 <div>
11 <button id="btn" onclick="changeData()">Change Data</button>
12 </div>
13
14 <script>
15
16 var width = 600;
17 var height = 400;
18 var datasets = [
19 [10, 30, 40, 20],
20 [10, 40, 30, 20, 50, 10],
21 [60, 30, 40, 20, 30]
22 ]
23
24 var svg = d3.select("body").append("svg")
25 .attr("width", width)
26 .attr("height", height)
27 .style("border", "1px solid black")
28
29 function drawChart(data) {
30 var selection = svg.selectAll("rect").data(data);
31 var yScale = d3.scaleLinear()
32 .domain([0, d3.max(data)])
33 .range([0, height-100]);
34
35 selection
36 .transition().duration(300)
37 .attr("height", (d) => yScale(d))
38 .attr("y", (d) => height - yScale(d))
39
40 selection
41 .enter()
42 .append("rect")
43 .attr("x", (d, i) => i * 45)
44 .attr("y", (d) => height)
45 .attr("width", 40)
46 .attr("height", 0)
47 .attr("fill", "orange")
48 .transition().duration(300)
49 .attr("height", (d) => yScale(d))
50 .attr("y", (d) => height - yScale(d))
51
52 selection
53 .exit()
54 .transition().duration(300)
55 .attr("y", (d) => height)
56 .attr("height", 0)
57 .remove()
58 }
59
60 var i = 0;
61 function changeData(){
62 drawChart(datasets[i++]);
63 if(i == datasets.length) i = 0;
64 }
65
66 window.addEventListener('load', function() {
67 changeData();
68 });
69 </script>
70</body>
71</html>
You can run the above code by directly opening chart.html
in the browser. When you click the Change Data
button, the chart updates by adding and removing bars and changing the heights of existing bars smoothly. D3 transitions provide these smooth animations.
Considering the above chart, you can expect the following features from a React/D3 integration:
The key barrier in integrating D3 with React is the conflict in the way each library handles the DOM. React uses the concept of a virtual DOM without touching the actual DOM directly. On the other hand, D3 provides its own set of features by directly interacting with the DOM. Thus, integrating D3 with a React component can cause errors in the functioning of the component.
To prevent this, make sure that React and D3 will work in their own spaces. For example, if you are creating an admin dashboard, make sure that React manages every front-end aspect except whatever is inside the charts, including navigations, buttons, tables, etc. But when it comes to the chart, the control of the component and its aspects should be handed over to D3. This includes transitions, data updates, rendering, mouse interactions, etc.
You can try to get React and D3 to work together using a basic D3 example. Before that, create a new React app.
Install D3 using npm install --save d3
. Create a charts
directory under the src
to store all D3-related files to organize and separate the code. Use the following code to find if D3 can work alongside React:
1// charts/BasicD3.js
2import * as d3 from 'd3';
3
4export function drawChart(height, width){
5 d3.select("#chart")
6 .append("svg")
7 .attr("width", width)
8 .attr("height", height)
9 .style("border", "1px solid black")
10 .append("text")
11 .attr("fill", "green")
12 .attr("x", 50)
13 .attr("y", 50)
14 .text("Hello D3")
15}
16
17// App.js
18import React, { useEffect, useState } from 'react';
19import './App.css';
20import { drawChart } from './charts/BasicD3';
21
22function App() {
23 const [data, setData] = useState([]);
24
25 useEffect(() => {
26 drawChart(400, 600);
27 }, []);
28
29
30 return (
31 <div className="App">
32 <h2>Graphs with React</h2>
33 <div id="chart">
34 </div>
35 </div>
36 );
37}
38
39export default App;
If the integration is successful, you will see "Hello D3" in green. Here you have:
div
to draw the D3 chart using React. useEffect()
hook to call the drawChart()
method on React app load. Now you can try the bar chart sample above to see if this method works well.
1// BasicD3.js
2import * as d3 from 'd3';
3
4export function drawChart(height, width, data){
5 const svg = d3.select("#chart")
6 .append("svg")
7 .attr("width", width)
8 .attr("height", height)
9 .style("border", "1px solid black")
10
11 var selection = svg.selectAll("rect").data(data);
12 // ....
13 // rest of the d3 code from chart.html
14 // .....
15}
16
17// App.js
18import React, { useEffect, useState } from 'react';
19import './App.css';
20import { drawChart } from './charts/BasicD3';
21
22const dataset = [
23 [10, 30, 40, 20],
24 [10, 40, 30, 20, 50, 10],
25 [60, 30, 40, 20, 30]
26]
27var i = 0;
28
29function App() {
30 const [data, setData] = useState([]);
31
32 useEffect(() => {
33 changeChart();
34 }, []);
35
36 const changeChart = () => {
37 drawChart(400, 600, dataset[i++]);
38 if(i === dataset.length) i = 0;
39 }
40
41
42 return (
43 <div className="App">
44 <h2>Graphs with React</h2>
45 <button onClick={changeChart}>Change Data</button>
46 <div id="chart">
47 </div>
48 </div>
49 );
50}
51
52export default App;
In the App
component, you have specified three data arrays in the dataset
variable. When clicked, the Change Data
button will call the drawChart
method with the new data array. This function is a way to demonstrate a dynamic dataset in a real-world application. When you run the above sample, on each click of the button a new chart gets added to the DOM rather than updates being done to the existing chart. This happens due to the appending of a new svg
element at the beginning of the drawChart()
method. You can fix it by splitting the chart initialization and drawing it into two parts.
1// BasicD3.js
2import * as d3 from 'd3';
3
4export function initChart(height, width){
5 d3.select("#chart")
6 .append("svg")
7 .attr("width", width)
8 .attr("height", height)
9 .style("border", "1px solid black")
10}
11
12export function drawChart(height, width, data){
13 const svg = d3.select("#chart svg");
14 // ....
15}
16
17
18// App.js
19// ...
20function App() {
21 const [data, setData] = useState([]);
22
23 useEffect(() => {
24 initChart(400, 600);
25 changeChart();
26 }, []);
27
28 // ...
29}
Finally, you have the chart integration working smoothly and updating as required. This integration seems quite trivial and simple. But there are a few issues with it when compared against our expectations:
div
is fixed using a unique ID. To overcome this, create multiple IDs (one per chart) and pass this in the drawChart()
method. However, this will not scale in the long run.BarChart
component should work just by having data, height, width, and other chart options as parameters as opposed to being initiated in the parent component's lifecycle.You can improve on the idea from the previous section by adding React patterns when possible. This ensures that you'll get the best of both worlds. The following code shows better integration between the two libraries:
1// BarChart.js
2import * as d3 from 'd3';
3import React, { useRef, useEffect } from 'react';
4
5function BarChart({ width, height, data }){
6 const ref = useRef();
7
8 useEffect(() => {
9 const svg = d3.select(ref.current)
10 .attr("width", width)
11 .attr("height", height)
12 .style("border", "1px solid black")
13 }, []);
14
15 useEffect(() => {
16 draw();
17 }, [data]);
18
19 const draw = () => {
20
21 const svg = d3.select(ref.current);
22 var selection = svg.selectAll("rect").data(data);
23 var yScale = d3.scaleLinear()
24 .domain([0, d3.max(data)])
25 .range([0, height-100]);
26
27 selection
28 .transition().duration(300)
29 .attr("height", (d) => yScale(d))
30 .attr("y", (d) => height - yScale(d))
31
32 selection
33 .enter()
34 .append("rect")
35 .attr("x", (d, i) => i * 45)
36 .attr("y", (d) => height)
37 .attr("width", 40)
38 .attr("height", 0)
39 .attr("fill", "orange")
40 .transition().duration(300)
41 .attr("height", (d) => yScale(d))
42 .attr("y", (d) => height - yScale(d))
43
44 selection
45 .exit()
46 .transition().duration(300)
47 .attr("y", (d) => height)
48 .attr("height", 0)
49 .remove()
50 }
51
52
53 return (
54 <div className="chart">
55 <svg ref={ref}>
56 </svg>
57 </div>
58
59 )
60
61}
62
63export default BarChart;
1// App.js
2import React, { useEffect, useState } from 'react';
3import './App.css';
4import BarChart from './charts/BarChart';
5
6const datas = [
7 [10, 30, 40, 20],
8 [10, 40, 30, 20, 50, 10],
9 [60, 30, 40, 20, 30]
10]
11var i = 0;
12
13function App() {
14 const [data, setData] = useState([]);
15
16 useEffect(() => {
17 changeData();
18 }, []);
19
20 const changeData = () => {
21 setData(datas[i++]);
22 if(i === datas.length) i = 0;
23 }
24
25
26 return (
27 <div className="App">
28 <h2>Graphs with React</h2>
29 <button onClick={changeData}>Change Data</button>
30 <BarChart width={600} height={400} data={data} />
31 </div>
32 );
33}
34
35export default App;
Several changes are done in the above code to make it more React-compatible:
svg
element is now passed using React useRef
hook, eliminating the need to individually specify the element ID/class for D3 to draw.With that, your expectation checklist is now complete.
For React developers, D3 is not a quickly available option for charting and visualization due to the off-the-shelf incompatibility of the two libraries. In this guide, we went through an approach that would enable a harmonious integration between the two. To expand it further, with this approach we are now able to use the D3 code examples available in the D3 documentation and blogs directly in our code, with only minimal modifications.