Drawing Charts in React with D3
Apr 24, 2020 • 13 Minute Read
Introduction
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.
A Simple D3 Bar Chart
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:
- First, it appends an svg to the document, which will later be used as the canvas for drawing the charts.
- Then it resizes the svg element and draws a border.
- The code following it uses the enter-update-exit pattern of D3. If you are not familiar with these concepts, please check out this visualization.
- Using the pattern, D3 will first draw bars for any new data points in the dataset. If this is the first dataset loaded, then all data points will be considered new.
- Then it will update any existing bars to match the new data points.
- Finally, it will remove any additional bars from the chart.
// chart.html
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<div>
<button id="btn" onclick="changeData()">Change Data</button>
</div>
<script>
var width = 600;
var height = 400;
var datasets = [
[10, 30, 40, 20],
[10, 40, 30, 20, 50, 10],
[60, 30, 40, 20, 30]
]
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
function drawChart(data) {
var selection = svg.selectAll("rect").data(data);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([0, height-100]);
selection
.transition().duration(300)
.attr("height", (d) => yScale(d))
.attr("y", (d) => height - yScale(d))
selection
.enter()
.append("rect")
.attr("x", (d, i) => i * 45)
.attr("y", (d) => height)
.attr("width", 40)
.attr("height", 0)
.attr("fill", "orange")
.transition().duration(300)
.attr("height", (d) => yScale(d))
.attr("y", (d) => height - yScale(d))
selection
.exit()
.transition().duration(300)
.attr("y", (d) => height)
.attr("height", 0)
.remove()
}
var i = 0;
function changeData(){
drawChart(datasets[i++]);
if(i == datasets.length) i = 0;
}
window.addEventListener('load', function() {
changeData();
});
</script>
</body>
</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:
- Draw multiple charts on the same page without affecting one another.
- Easily use existing D3 code.
- Refresh the dataset.
- Smoothly update the chart with animations when the dataset changes.
- Reuse a chart component with minimal changes to the parent component.
React and D3
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:
// charts/BasicD3.js
import * as d3 from 'd3';
export function drawChart(height, width){
d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
.append("text")
.attr("fill", "green")
.attr("x", 50)
.attr("y", 50)
.text("Hello D3")
}
// App.js
import React, { useEffect, useState } from 'react';
import './App.css';
import { drawChart } from './charts/BasicD3';
function App() {
const [data, setData] = useState([]);
useEffect(() => {
drawChart(400, 600);
}, []);
return (
<div className="App">
<h2>Graphs with React</h2>
<div id="chart">
</div>
</div>
);
}
export default App;
If the integration is successful, you will see "Hello D3" in green. Here you have:
- Created a div to draw the D3 chart using React.
- Used a pure D3 code to draw the chart and text.
- Used the 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.
// BasicD3.js
import * as d3 from 'd3';
export function drawChart(height, width, data){
const svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
var selection = svg.selectAll("rect").data(data);
// ....
// rest of the d3 code from chart.html
// .....
}
// App.js
import React, { useEffect, useState } from 'react';
import './App.css';
import { drawChart } from './charts/BasicD3';
const dataset = [
[10, 30, 40, 20],
[10, 40, 30, 20, 50, 10],
[60, 30, 40, 20, 30]
]
var i = 0;
function App() {
const [data, setData] = useState([]);
useEffect(() => {
changeChart();
}, []);
const changeChart = () => {
drawChart(400, 600, dataset[i++]);
if(i === dataset.length) i = 0;
}
return (
<div className="App">
<h2>Graphs with React</h2>
<button onClick={changeChart}>Change Data</button>
<div id="chart">
</div>
</div>
);
}
export 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.
// BasicD3.js
import * as d3 from 'd3';
export function initChart(height, width){
d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
}
export function drawChart(height, width, data){
const svg = d3.select("#chart svg");
// ....
}
// App.js
// ...
function App() {
const [data, setData] = useState([]);
useEffect(() => {
initChart(400, 600);
changeChart();
}, []);
// ...
}
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:
- You can only draw one chart per page since the 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.
- This doesn't accommodate well with React's reusability concept. Ideally, a 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.
A Better React/D3 Intergration
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:
// BarChart.js
import * as d3 from 'd3';
import React, { useRef, useEffect } from 'react';
function BarChart({ width, height, data }){
const ref = useRef();
useEffect(() => {
const svg = d3.select(ref.current)
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
}, []);
useEffect(() => {
draw();
}, [data]);
const draw = () => {
const svg = d3.select(ref.current);
var selection = svg.selectAll("rect").data(data);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([0, height-100]);
selection
.transition().duration(300)
.attr("height", (d) => yScale(d))
.attr("y", (d) => height - yScale(d))
selection
.enter()
.append("rect")
.attr("x", (d, i) => i * 45)
.attr("y", (d) => height)
.attr("width", 40)
.attr("height", 0)
.attr("fill", "orange")
.transition().duration(300)
.attr("height", (d) => yScale(d))
.attr("y", (d) => height - yScale(d))
selection
.exit()
.transition().duration(300)
.attr("y", (d) => height)
.attr("height", 0)
.remove()
}
return (
<div className="chart">
<svg ref={ref}>
</svg>
</div>
)
}
export default BarChart;
// App.js
import React, { useEffect, useState } from 'react';
import './App.css';
import BarChart from './charts/BarChart';
const datas = [
[10, 30, 40, 20],
[10, 40, 30, 20, 50, 10],
[60, 30, 40, 20, 30]
]
var i = 0;
function App() {
const [data, setData] = useState([]);
useEffect(() => {
changeData();
}, []);
const changeData = () => {
setData(datas[i++]);
if(i === datas.length) i = 0;
}
return (
<div className="App">
<h2>Graphs with React</h2>
<button onClick={changeData}>Change Data</button>
<BarChart width={600} height={400} data={data} />
</div>
);
}
export default App;
Several changes are done in the above code to make it more React-compatible:
- The chart is now a React component. As a result, the lifecycle of the chart is managed by itself without being part of the parent component.
- Better reusability
- The reference to the 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.
Conclusion
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.