Creating a responsive line chart using d3.js in React.js

Creating a responsive line chart using d3.js in React.js

·

6 min read

D3.js is a popular data visualization library that provides a lot of tools for creating interactive and dynamic visualizations. In this blog post, we'll cover how to create a line chart using D3.js in React.js.

Prerequisites

Before we get started, ensure that you have the following prerequisites:

  • Basic knowledge of HTML, CSS, and JavaScript

  • Node.js and npm installed on your system

  • A text editor like Visual Studio Code

Getting started

Let's create a new React.js project by running the following command in your terminal:

npx create-react-app responsive-line-chart-using-d3js-reactjs

This command creates a new React.js project with the name responsive-line-chart-using-d3js-reactjs. Navigate to the project directory by running the following command:

cd responsive-line-chart-using-d3js-reactjs

Open the project directory in your text editor. Next, install the d3 library by running the following command:

npm install d3

This command installs the latest version of the d3 library in your project.

Creating the line chart component

Create a new file named LineChart.tsx in the src/features directory of your project. This file will contain the code for the line chart component. Here's the code for the component:

import * as d3 from 'd3';
import { FC, useEffect, useRef } from 'react';

import Spinner from 'components/Spinner';
import { useResizeObserver } from 'hooks/useResizeObserver';

const LineChart: FC<IChart> = ({ data, svgWrapperRef, margin, isYaxisRight = false }) => {
  const dimensions: any = useResizeObserver(svgWrapperRef);
  const svgRef = useRef<SVGSVGElement>(null);

  useEffect(() => {
    if (!svgRef?.current || !dimensions) return;

    // Initialize the height, width and max value
    const innerWidth: number = dimensions?.width - margin?.right;
    const innerHeight: number = dimensions?.height - (margin?.top + margin?.bottom);
    const maxValue: number = Math.max(...data?.map(({ value }) => value));

    const svg = d3.select(svgRef?.current);
    svg.selectAll('.dot').remove();

    // Create x-axis scale
    const xScale: any = d3
      .scalePoint()
      .domain([...data?.map(({ label }) => label)])
      .range([margin?.left, innerWidth]);

    // Create y-axis scale
    const yScale: any = d3.scaleLinear().domain([0, maxValue]).rangeRound([innerHeight, margin?.top]);

    // Create x-axis
    const xAxis: any = d3.axisBottom(xScale).ticks(data?.length);
    svg
      .select('.x-axis')
      .style('transform', `translateY(${innerHeight}px)`)
      .style('color', 'steelblue')
      .style('font-size', 10)
      .call(xAxis)
      .selectAll('text')
      .style('text-anchor', 'end')
      .attr('dx', '-5px')
      .attr('dy', '4px')
      .attr('transform', 'rotate(-30)');

    // Create y-axis for left side
    const yAxis: any = d3.axisLeft(yScale).ticks(5);
    svg
      .select('.y-axis')
      .style('transform', `translateX(${margin?.left}px)`)
      .style('color', 'steelblue')
      .style('font-size', 10)
      .call(yAxis);

    // Create y-axis for right side
    const yAxisRight: any = d3.axisRight(yScale).ticks(5);
    svg
      .select('.y-axis-right')
      .style('transform', `translateX(${dimensions?.width - margin?.right}px)`)
      .style('color', 'steelblue')
      .style('font-size', 10)
      .call(yAxisRight);

    // Draw line
    const lineGenerator = d3
      .line<IData>()
      .x((d) => xScale(d?.label) + xScale.bandwidth() / 2)
      .y((d) => yScale(d?.value))
      .curve(d3.curveCardinal);

    svg
      .select('.line')
      .datum([...data])
      .attr('fill', 'none')
      .attr('stroke', 'steelblue')
      .attr('stroke-width', 1)
      .attr('d', lineGenerator);

    svg
      .selectAll('.dot')
      .data([...data])
      .enter()
      .append('circle')
      .attr('class', 'dot')
      .attr('cx', (d) => xScale(d?.label) + xScale.bandwidth() / 2)
      .attr('cy', (d) => yScale(d?.value))
      .attr('r', 5)
      .style('cursor', 'pointer')
      .style('fill', ({ fillColor }) => fillColor)
      .on('mouseenter', (event, item) => {
        svg
          .selectAll('.tooltip')
          .data([item?.value])
          .join((enter) => enter.append('text').attr('y', yScale(item?.value) - 4))
          .attr('class', 'tooltip')
          .text(`${item?.value}`)
          .attr('x', xScale(item?.label) + xScale.bandwidth() / 2)
          .style('font-size', '10px')
          .attr('text-anchor', 'middle')
          .transition()
          .duration(500)
          .attr('y', yScale(item?.value) - 8)
          .style('font-size', '14px')
          .style('fill', item?.fillColor)
          .style('opacity', 1);
      })
      .on('mouseleave', () => svg.select('.tooltip').remove());
  }, [data, dimensions]);

  if (!dimensions) {
    return (
      <div className="flex w-full justify-center items-center py-2">
        <Spinner className="text-gray-300 h-8 w-8" />
      </div>
    );
  }

  return (
    <div className="d3js">
      <svg ref={svgRef} width={`${dimensions?.width}`} height={dimensions?.height}>
        <g className="x-axis" />
        {isYaxisRight ? <g className="y-axis-right" /> : <g className="y-axis" />}
        <path className="line" />
      </svg>
    </div>
  );
};

interface IData {
  label: string;
  value: number;
  fillColor: string;
}

interface IChart {
  data: IData[];
  svgWrapperRef: any;
  margin: {
    top: number;
    right: number;
    bottom: number;
    left: number;
  };
  isYaxisRight?: boolean;
}

export default LineChart;

The above code creates a reusable line chart component for displaying data points, built with d3.js and React.js. The component expects to receive data in the form of an array of objects, where each object has three properties: label (a string), value (a number), and fillColor (a string). It also expects to receive a svgWrapperRef (a reference to the wrapper element for the chart), a margin object (with properties top, right, bottom, and left for the margins of the chart), and a boolean isYaxisRight (to indicate whether the y-axis is on the right side of the chart).

The useResizeObserver hook is used to observe the size of the svgWrapperRef element and update the chart when the size changes.

The useEffect hook is used to render the chart when the data or dimensions change. The code initializes the height, width, and max value for the chart, selects the SVG element, and creates the x and y scales, axes, and line using d3 methods. It also creates a dot for each data point, with hover functionality to display a tooltip. Finally, if the dimensions are not yet available, the component displays a spinner until they are.

Let’s import the LineChart component in the app.tsx or other components and pass the props. Here's the code for the app.tsx

import { useEffect, useRef, useState } from 'react';

import LineChart from './LineChart';
import { randomNumber } from 'utils/helpers';

const Feature = () => {
  const margin = { top: 20, right: 10, bottom: 20, left: 30 };
  const [data, setData] = useState<any[]>([]);
  const svgLargeRef: any = useRef<SVGSVGElement>();
  const svgMediumRef: any = useRef<SVGSVGElement>();
  const svgSmallRef: any = useRef<SVGSVGElement>();

  useEffect(() => {
    const makeRandomData = () => {
      setData([]);
      ['JS', 'Python', '.Net', 'Java', 'GoLang', 'UI/UX']?.map((label) => {
        setData((prevData) => [...prevData, { label, value: randomNumber(10, 99), fillColor: `#${randomNumber()}` }]);
      });

      setTimeout(() => {
        makeRandomData();
      }, 1000 * 10);
    };

    makeRandomData();
  }, []);

  return (
    <div className="relative px-5 py-2">
      <div className="flex justify-center">
        <div className="w-[90vw] h-full">
          <h2 className="flex justify-between items-center font-semibold text-black mb-2">D3.js Chart</h2>
          <div className="flex w-full h-[75vh] justify-center items-start border border-dashed border-black text-white p-4 space-x-2">
            <div ref={svgLargeRef} className="w-5/12 h-72 border border-gray-300 p-1">
              <LineChart data={data} svgWrapperRef={svgLargeRef} margin={margin} isYaxisRight={false} />
            </div>
            <div ref={svgMediumRef} className="w-4/12 h-72 border border-gray-300 p-1">
              <LineChart data={data} svgWrapperRef={svgMediumRef} margin={margin} isYaxisRight={false} />
            </div>
            <div ref={svgSmallRef} className="w-3/12 h-72 border border-gray-300 p-1">
              <LineChart
                data={data}
                svgWrapperRef={svgSmallRef}
                margin={{ ...margin, left: 10, right: 30 }}
                isYaxisRight={true}
              />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Feature;

The above code defines a React functional component called Feature, which renders a set of three line charts using the D3.js library.

The component imports useEffect, useRef, and useState from the react library, and imports a randomNumber function from a utils/helpers module.

The Feature component initializes state using the useState hook with an empty array for the data variable. It also initializes three refs using the useRef hook, which are used to reference the SVG elements that will be rendered for each line chart.

The useEffect hook is used to generate new random data for the charts and update the data state every 10 seconds. The makeRandomData function updates the data state by generating an array of objects containing a label, a value, and a fillColor for each chart. The randomNumber function is used to generate random values for the charts.

The component renders a div element that contains three child div elements, each representing a line chart. Each child div element references a different SVG element using the ref attribute and passes the data and other chart properties to the LineChart component.

The LineChart component is not defined in the code provided, but based on its usage, it appears to be a custom component that renders a line chart using D3.js. It receives the data, a reference to the SVG wrapper element, a margin object, and a boolean value indicating whether the y-axis should be on the right or left side of the chart.

Overall, the Feature component generates and renders three line charts using D3.js, which update with new random data every 10 seconds.

Summary

This blog post is a useful and informative tutorial for anyone looking to create interactive data visualizations using D3.js and React.js.

I hope you found this tutorial helpful! If you have any questions or feedback, feel free to let me know.

Download full source code from GitHub