Popular Tags

D3.js Line Chart with React

In this article, I’ll explain how to create a line chart with tooltips using the D3.js library (v.6) and React.

D3.js Line Chart with React

Contents

  1. Prerequisites
  2. Creating Chart
  3. Adding Styles

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:

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:

→ D3.js Line Chart Tutorial

→ React Tutorial for Total Beginners