Simple Line Graph with React and D3
May 25, 2021
Simple wasn’t a good word to use to describe this line graph. It was such a headache creating this line graph. So much so, that I almost abandoned it altogether a few times. In the end, though, I did end up with a line graph and a better understanding of D3.
D3 is usually described as a “low level” graphing library. The reason for that is because it doesn’t make any assumptions about what you’re trying to build. It gives you the ability to create whatever you want. D3 has a module called d3-shape
that will help you draw a line, but you won’t find anything in the library for creating a line graph.
It’s up to you to build the x-axis and y-axis, add the tick marks, create the scales, create the line, etc. Which makes D3 flexible and powerful, but also frustrating.
I think D3 is similar to Tailwind in this way. The Tailwindcss library itself isn’t going to give you a card element. Instead, it gives you all of the parts to create the border, create the spacing, define the size, etc.
Building the line graph
The goal here is to keep D3 and React as separate as possible. React should handle the rendering and D3 will tell it what to render. There isn’t much math involved in this graph, but D3 will handle any math that might be needed.
For the React component itself, I’ll be using the useRef
, useState
, and useEffect
hooks. The useRef
hook is used to create an element reference to pass to my D3 helper functions. The useState
hook is used to store the data that the line graph is built on. Lastly, the useEffect
hook, which is React’s lifecycle hook, is used to initialize the line graph.
The data stored in state will not be changing in this example, so it could in theory just be defined as a const
but I’ll keep it in state just so the concept and usage of the useState
hook stays fresh in my mind.
The struggles
Most of my struggles with this line graph came from the dates on the x-axis. It turns out that D3 wants actual Date
objects instead of strings. This seems obvious in hindsight, but not so obvious when I’m banging my head against the wall trying to figure out why my x-axis isn’t showing any data. In addition to that, to show dates in “correct” timezone I had to use D3’s utcFormat
instead of timeFormat
. It keep showing the days as 1/31 instead of 1/01 otherwise.
The other issue I had was with line itself. It was showing up as a weird shape instead of a line. It took me forever to figure this out, but finally a stumbled on a stackoverflow question related to this problem, and it turns out that path
elements have a black fill by default that makes it look like the first and last points are corrected, even though they aren’t.
The easiest way to get around this is to set the fill
attribute to none
. I can then change the color of the line by setting the stroke
attribute value, and I can set the width of the line by setting the stroke-width
attribute value.
The graph
The code
import React, { useRef, useState, useEffect } from 'react';
import { scaleLinear, scaleUtc } from 'd3-scale';
import { max, extent } from 'd3-array';
import { axisBottom, axisLeft } from 'd3-axis';
import { line } from 'd3-shape';
import { timeParse, utcFormat } from 'd3-time-format';
import { select } from 'd3-selection';
export const FinalGraph = () => {
// Init the element reference
const containerRef = useRef(null);
// Random data
const initialData = [
{ date: '2007-01-01', value: 100 },
{ date: '2007-02-01', value: 200 },
{ date: '2007-03-01', value: 300 },
{ date: '2007-04-01', value: 400 },
{ date: '2007-05-01', value: 500 },
{ date: '2007-06-01', value: 600 },
{ date: '2007-07-01', value: 500 },
{ date: '2007-08-01', value: 400 },
{ date: '2007-09-01', value: 300 },
{ date: '2007-10-01', value: 200 },
{ date: '2007-11-01', value: 100 },
{ date: '2007-12-01', value: 100 },
];
const [data] = useState(initialData);
const width = 600;
const height = 300;
// Init lifecycle hook
useEffect(() => {
const margin = { top: 20, right: 30, bottom: 30, left: 40 };
InitGraph(data, margin, width, height, containerRef);
}, [data]);
return (
<div ref={containerRef}></div>
);
}
const InitGraph = (data, margin, width, height, elementRef) => {
const parse = timeParse("%Y-%m-%d");
// Ensure that the dates are Date objects instead of strings
const formattedData = data.map(d => ({ date: parse(d.date), value: d.value }));
// Define the x-axis scale
const xScale = scaleUtc()
.domain(extent(formattedData, d => d.date))
.range([margin.left, width - margin.right]);
// Define the y-axis scale
const yScale = scaleLinear()
.domain([0, max(formattedData, d => d.value)]).nice()
.range([height - margin.bottom, margin.top])
// Build the x-axis
const xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(
axisBottom(xScale)
// Format the date shown on the x-axis
.tickFormat(utcFormat('%m/%d'))
);
// Build the y-axis
const yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(axisLeft(yScale))
// Define the line
const initLine = line()
// Ensure that the data is a number
.defined(d => !isNaN(d.value))
.x(d => xScale(d.date))
.y(d => yScale(d.value))
// Create the svg instead the ref passed from the React component
const svg = select(elementRef.current)
.append('svg')
.attr('width', width)
.attr('height', height);
// Create the axis'
svg.append('g').call(xAxis);
svg.append('g').call(yAxis);
// Create the line
svg.append('path')
.datum(formattedData)
.attr('d', initLine)
// Remove the fill!!
.attr("fill", "none")
// Change the line color
.attr("stroke", "steelblue")
// Change the line width
.attr("stroke-width", 1.5);
}
Conclusion
I wanted to go more in-depth on the code and why certain lines are written the way they are, but honestly, this unassuming line graph got the best of me. I want to just move on from it. It’s sort of rattled my confidence in my ability to use D3 in any sort of meaningful way.
I don’t mean to imply that I’m giving up on D3, but I think this has opened my eyes to how steep its learning curve is. I understand now why there are so many libraries written on top of D3 that abstract a lot of the complexity away. I don’t think D3 isn’t worth learning though, so I’ll keep on going with it for now.