D3 (or D3.js) is a JavaScript library for visualizing data using Scalable Vector Graphics (SVG) and HTML. D3 stands for “data-driven documents”, which are interactive dashboards and all sorts of dynamically driven web applications.
This is not just a library for building chart layouts. It’s useful when you need to work with the Document Object Model (DOM), dynamically update data, use animation techniques, and operate with different states of data entities.
Prerequisites
We’ll need the following:
- Node.js
- React — see my React Tutorial for Total Beginners
- D3.js
To install D3.js, download the latest version d3.zip on GitHub. Then install D3 via npm:
npm install d3
Next, follow instructions in React Tutorial for Total Beginners to create a React project. Then open your project folder. We’ll work with three files:
- public/index.html — will contain HTML
- src/index.js — will contain our JS/D3 code
- src/index.css — will contain CSS styles
For plotting, we’ll also be using data from the usd-2020.csv file. We put it into the /public folder. The usd-2020.csv file has the following structure:
date,price
12/30/2020,74.6769
12/29/2020,73.7651
12/28/2020,73.7895
12/25/2020,74.1945
…
In our D3 code, we’ll refer to values as .date or .price.
Now open the public/index.html file. Its <body> tag should contain the following:
<body>
<div id="root"></div>
<script src="https://d3js.org/d3.v6.min.js"></script>
</body>
The #root element will contain all the code that will be generated in src/index.js later. The D3.js library code is embedded in the <script> element.
Here’s the full content of the public/index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script src="https://d3js.org/d3.v6.min.js"></script>
</body>
</html>
Creating Chart
Now open the src/index.js file.
First, we need to import libraries:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import * as d3 from "d3";
Here’s the general structure of the src/index.js file:
// Basic chart variables
const margin, width, height, color...
const Chart = () => {
// This will generate the chart
}
ReactDOM.render(
// This will render the DOM
);
We’ll create our line chart in several steps.
1. Set basic chart variables
const margin = { top: 40, right: 80, bottom: 60, left: 50 },
width = 960 - margin.left - margin.right,
height = 280 - margin.top - margin.bottom,
color = "OrangeRed";
960 and 280 are not the chart’s size in pixels but its aspect ratio. Although our chart will be responsive, its aspect ratio will stay the same. Margins will be used to create space for labels and titles.
2. Create the Chart() function
const Chart = () => {
}
Inside this function, set the states that will help handle the mousemove event and append the data:
const [activeIndex, setActiveIndex] = React.useState(null),
[data, setData] = React.useState([]);
All the code on steps 3 to 8 goes inside this Chart() function.
3. Import data
React.useEffect(() => {
d3.csv("/usd-2020.csv").then((d) => {
d = d.reverse();
const parseDate = d3.timeParse("%m/%d/%Y");
d.forEach((i) => {
i.date = parseDate(i.date);
i.price = Number(i.price);
});
setData(d);
});
return () => undefined;
}, []);
Here we’re importing the usd-2020.csv file that is located in the /public folder. As our dataset has dates in “m/d/y” format (e.g. 01/02/2020), we need to parse dates with "%m/%d/%Y".It’s also important to reverse the data to be able to create tooltip boxes. (Although, some datasets don't require reversing).
4. Define min and max values
const yMinValue = d3.min(data, (d) => d.price),
yMaxValue = d3.max(data, (d) => d.price);
This will be used to set limitations on the y-axis.
5. Create x- and y-axes
const getX = d3
.scaleTime()
.domain(d3.extent(data, (d) => d.date))
.range([0, width]);
const getY = d3
.scaleLinear()
.domain([yMinValue - 1, yMaxValue + 2])
.range([height, 0]);
const getXAxis = (ref) => {
const xAxis = d3.axisBottom(getX);
d3.select(ref).call(xAxis.tickFormat(d3.timeFormat("%b")));
};
const getYAxis = (ref) => {
const yAxis = d3.axisLeft(getY).tickSize(-width).tickPadding(7);
d3.select(ref).call(yAxis);
};
getY returns the y-coordinate, and getX returns the x-coordinate.
The d3.scaleTime() function is used to create and return a new time scale on the x-axis. And the d3.scaleLinear() function is used to create scale points on the y-axis. These scales will help us find the positions/coordinates on the graph for each data item.
The d3.axisBottom() function in D3.js is used to create a bottom horizontal axis (X), and the d3.axisLeft() function in D3.js creates a left vertical axis (Y).
We also want to format months on the x-axis with "%b" so that February would be represented as Feb etc.
6. Define line and area paths
const linePath = d3
.line()
.x((d) => getX(d.date))
.y((d) => getY(d.price))
.curve(d3.curveMonotoneX)(data);
const areaPath = d3
.area()
.x((d) => getX(d.date))
.y0((d) => getY(d.price))
.y1(() => getY(yMinValue - 1))
.curve(d3.curveMonotoneX)(data);
area() and line() are D3 helper functions. The area function transforms each data point into information that describes the shape, and the line function draws a line according to data values. curveMonotoneX is the type of line/area curve (check D3 curve explorer for more).
7. Define event handlers
const handleMouseMove = (e) => {
const bisect = d3.bisector((d) => d.date).left,
x0 = getX.invert(d3.pointer(e, this)[0]),
index = bisect(data, x0, 1);
setActiveIndex(index);
};
const handleMouseLeave = () => {
setActiveIndex(null);
};
When we move the mouse over the chart, the handleMouseMove() function is responsible for finding out the position of the cursor, figuring out the nearest plot point, and translating the tooltip as well as the circle marker to the nearest point.
We need to recover the closest x-coordinate in the dataset. We can do this thanks to the d3.bisector() function. Once we have this position, we need to use it to update the circle and text position on the chart. getX.invert takes a number from the scale’s range (i.e., the width of the chart) and maps it to the scale’s domain (i.e., a number between the values on the x-axis). bisect helps us in finding the nearest point to the left of this invert point.
Our task is to make the effect of an increasing circle when hovering over the area. Since we don’t have a specific rectangle in the DOM, to which we can append the event directly, we will append the event to the entire SVG and then calculate the position.
8. Return SVG
Here we’re creating the SVG element and its child elements. We’re also creating a circle marker for the point we’re hovering over and a tooltip box (an info tip or a hovering tip). And we add the value (price) and the date at the intersection.
return (
<div className="wrapper">
<svg
viewBox={`0 0 ${width + margin.left + margin.right}
${height + margin.top + margin.bottom}`}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
// x- and y-axes
<g className="axis" ref={getYAxis} />
<g
className="axis xAxis"
ref={getXAxis}
transform={`translate(0,${height})`}
/>
// area and line paths
<path fill={color} d={areaPath} opacity={0.3} />
<path strokeWidth={3} fill="none" stroke={color} d={linePath} />
// y-axis label
<text
transform={"rotate(-90)"}
x={0 - height / 2} y={0 - margin.left} dy="1em">
{"USD"}
</text>
// chart title
<text
x={width / 2} y={0 - margin.top / 2} text-anchor="middle" >
{"USD to RUB Exchange Rates, 2020"}
</text>
// chart subtitle
<a
className="subtitle"
href="https://www.moex.com/ru/index/rtsusdcur.aspx?tid=2552"
target="_blank">
<text x="0" y={height + 50}>
{"Source: Moscow Exchange"}
</text>
</a>
{data.map((item, index) => {
return (
<g key={index}>
// hovering text
<text
fill="#666"
x={getX(item.date)}
y={getY(item.price) - 20}
textAnchor="middle"
>
{index === activeIndex ? item.price : ""}
</text>
// hovering circle
<circle
cx={getX(item.date)}
cy={getY(item.price)}
r={index === activeIndex ? 6 : 4}
fill={color}
strokeWidth={index === activeIndex ? 2 : 0}
stroke="#fff"
style={{ transition: "ease-out .1s" }}
/>
</g>
);
})}
</svg>
</div>
);
Note that the <svg> element can be dynamically resized using the viewBox attribute — this makes our chart responsive as it can stretch or shrink with the browser window. The value of the viewBox attribute is a list of four numbers: min-x, min-y, width, and height.
9. Render the DOM
ReactDOM.render(
<Chart />,
document.getElementById("root")
);
ReactDOM.render renders the DOM with the help of the public/index.html file where #root — the <div id="root"></div> element — is located.
Adding Styles
Finally, we can add some styles in the src/index.css file:
body {
padding: 80px 40px;
font-family: Helvetica;
background: darkslategray;
}
.wrapper {
font-size: 16px;
color: #666;
padding: 25px;
}
svg {
overflow: visible;
}
text {
fill: gainsboro;
color: gainsboro;
font-size: 16px;
}
.subtitle text {
text-decoration: underline;
font-size: 12px;
}
.axis .domain,
.axis.xAxis line {
display: none;
}
That’s it, our D3.js line chart is ready. You can get the full code on GitHub.
Read also: