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

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

·

6 min read

D3.js (Data-Driven Documents) is a powerful JavaScript library used for creating dynamic and interactive data visualizations on the web. With D3.js, creating an area chart is straightforward and customizable.

To create an area chart using D3.js, you would start by defining the dimensions of the chart and creating a scale for the X and Y axis. Then, you would use D3.js to create the area based on the data and the scales. You can customize the appearance of the chart by adjusting the color, opacity, and stroke of the area.

D3.js also provides many tools for adding interactivity to the chart. For example, you could use D3.js to add tooltips, highlight the area on hover, or animate the chart over time.

Overall, D3.js provides a flexible and powerful framework for creating customized area charts and other data visualizations on the web. In this blog, we will go over the steps to create an area chart using D3.js in a React.js project.

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-area-chart-using-d3js-reactjs

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

cd responsive-area-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 area chart component

Create a new file named AreaChart.tsx in the src/features directory of your project. This file will contain the code for the area 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 AreaChart: 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();
    svg.selectAll('.line').remove();
    svg.selectAll('.grid').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 the area
    const areaGenerator = d3
      .area<IData>()
      .x((d: any) => xScale(d.label))
      .y0(innerHeight)
      .y1((d: any) => yScale(d.value))
      .curve(d3.curveMonotoneX);

    svg
      .select('.area')
      .datum([...data])
      .attr('fill', 'steelblue')
      .attr('d', areaGenerator as any)
      .attr('opacity', 0.25);

    svg
      .selectAll('.dot')
      .data([...data])
      .enter()
      .append('circle')
      .attr('class', 'dot')
      .attr('cx', ({ label }) => xScale(label))
      .attr('cy', ({ value }) => yScale(value))
      .attr('r', 5)
      .style('cursor', 'pointer')
      .attr('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))
          .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());

    // Draw the line for area
    const lineGenerator = d3
      .line<IData>()
      .x(({ label }) => xScale(label))
      .y(({ value }) => yScale(value))
      .curve(d3.curveMonotoneX);

    svg
      .append('path')
      .data([data])
      .attr('stroke', 'steelblue')
      .attr('fill', 'none')
      .attr('class', 'line')
      .attr('d', lineGenerator);
  }, [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="area" />
      </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 AreaChart;

The above code is a React functional component, implementing an area chart using the D3.js library. The chart takes data as input, along with optional arguments to set the margins of the chart and whether the y-axis is on the right or left side. It uses the useResizeObserver hook to resize the chart when its container changes size. The chart is drawn in the useEffect hook, which is called whenever the data or dimensions change. The d3 library is used to create the x and y scales, axes, area, and line. Tooltips are added to show the exact value of each point on the chart when hovering over it with the mouse. The chart also shows a spinner while waiting for the dimensions to be calculated.

Let’s import the AreaChart 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 AreaChart from './AreaChart';
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">
              <AreaChart data={data} svgWrapperRef={svgLargeRef} margin={margin} isYaxisRight={false} />
            </div>
            <div ref={svgMediumRef} className="w-4/12 h-72 border border-gray-300 p-1">
              <AreaChart data={data} svgWrapperRef={svgMediumRef} margin={margin} isYaxisRight={false} />
            </div>
            <div ref={svgSmallRef} className="w-3/12 h-72 border border-gray-300 p-1">
              <AreaChart
                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 named Feature which imports the useEffect, useRef, and useState hooks from the react library. It also imports an AreaChart component and a randomNumber function from two other files.

Within the Feature component, it sets up three SVG elements using the useRef hook, each with a different width and height. It also sets up an empty array of data using the useState hook.

The useEffect hook is used to create a function called makeRandomData which is called once when the component mounts. The makeRandomData function generates new random data for the data array using the randomNumber function, and updates it every 10 seconds using setTimeout.

The return statement returns a JSX element with the three SVG elements wrapped in a parent div. Each SVG element is passed to the AreaChart component along with the data, margin, and isYaxisRight props. Finally, the Feature component is exported as the default export of this file.

Summary

Overall, the blog post provides a clear and detailed explanation of how to use d3.js to create a responsive area chart in a React.js application, making it a useful resource for anyone looking to visualize their data in this way.

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