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

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

·

5 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 a donut chart is straightforward and customizable. In this blog post, we will guide you through the process of building a responsive donut chart using d3.js in React.js, step-by-step.

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

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

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

Create a new file named DonutChart.tsx in the src/features directory of your project. This file will contain the code for the donut 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 DonutChart: FC<IChart> = ({ data, svgWrapperRef }) => {
  const dimensions: any = useResizeObserver(svgWrapperRef);
  const svgRef = useRef<SVGSVGElement>(null);

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

    const innerWidth = dimensions?.width;
    const innerHeight = dimensions?.height;
    const radius = Math.min(innerWidth, innerHeight) / 2;

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

    const pieGenerator = d3
      .pie<IData>()
      .value(({ value }) => value)
      .sort(null);

    const arcGenerator: any = d3
      .arc<d3.PieArcDatum<IData>>()
      .innerRadius(radius * 0.35)
      .outerRadius(radius * 0.65);

    const arcGeneratorForLabel = d3
      .arc<d3.PieArcDatum<IData>>()
      .innerRadius(radius)
      .outerRadius(radius * 0.85);

    const slices = pieGenerator([...data]);

    const g = svg
      .attr('width', innerWidth)
      .attr('height', innerHeight)
      .append('g')
      .attr('transform', `translate(${innerWidth / 2}, ${innerHeight / 2})`);

    g.selectAll('path')
      .data(slices)
      .enter()
      .append('path')
      .attr('fill', (d, i) => d?.data?.fillColor)
      .attr('d', arcGenerator);

    g.selectAll('polyline')
      .data(slices)
      .enter()
      .append('polyline')
      .style('fill', 'none')
      .style('stroke', 'steelblue')
      .attr('points', (d) => {
        const pos = arcGeneratorForLabel.centroid(d);
        pos[0] = radius * 0.65 * (midAngle(d) < Math.PI ? 1 : -1);
        return [arcGenerator.centroid(d), arcGeneratorForLabel.centroid(d), pos];
      });

    g.selectAll('text')
      .data(slices)
      .enter()
      .append('text')
      .transition()
      .duration(500)
      .attr('dy', '.35em')
      .text((d) => d?.data?.label)
      .attr('transform', (d) => {
        const pos = arcGeneratorForLabel.centroid(d);
        pos[0] = radius * 0.95 * (midAngle(d) < Math.PI ? 1 : -1);
        return `translate(${pos})`;
      })
      .style('font-size', '12px')
      .style('fill', (d) => d?.data?.fillColor);
  }, [data, dimensions]);

  const midAngle = (d: any) => d.startAngle + (d.endAngle - d.startAngle) / 2;

  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} />
    </div>
  );
};

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

interface IChart {
  data: IData[];
  svgWrapperRef: any;
}

export default DonutChart;

The above code is a functional component in React that renders a donut chart using D3.js library. The component takes in data as props and the svgWrapperRef, which is a reference to the wrapper element that contains the SVG element. The component makes use of the useResizeObserver hook to observe the dimensions of the SVG element's wrapper and sets the dimensions to the dimensions of the SVG element.

Inside the useEffect hook, the component generates a pie chart using D3.js library with the provided data. It creates an arc generator, slices the data into pie slices, and appends them as SVG path elements. It also adds text and polylines to label the slices.

The midAngle function is a helper function used to calculate the mid-angle of the slice, which is used to position the label of the slice.

The component conditionally renders a Spinner if the dimensions of the SVG element are not yet available. Overall, the code creates a responsive donut chart using D3.js in React.js.

Let’s import the DonutChart 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 DonutChart from './DonutChart';
import { randomNumber } from 'utils/helpers';

const Feature = () => {
  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-4/6 h-72 border border-gray-300 p-1">
              <DonutChart data={data} svgWrapperRef={svgLargeRef} />
            </div>
            <div ref={svgMediumRef} className="w-2/6 h-72 border border-gray-300 p-1">
              <DonutChart data={data} svgWrapperRef={svgMediumRef} />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Feature;

This is a React functional component that renders a D3.js donut chart. It imports the useEffect, useRef, and useState hooks from React, the DonutChart component from the local ./DonutChart file, and the randomNumber function from a utils/helpers module.

The component defines three SVG references using the useRef hook, one for each of the three donut charts that are being rendered. It also initializes the data state as an empty array using the useState hook.

The useEffect hook is used to generate random data for the donut charts and update the data state every 10 seconds. The makeRandomData function generates new data using the randomNumber function and updates the data state. It is called initially when the component mounts, and then repeatedly every 10 seconds using the setTimeout function.

The return statement renders the donut charts using the DonutChart component, passing in the data state and the corresponding SVG reference for each chart. The component also renders some HTML elements with classes and styles.

Summary

This blog post explains how to use the D3.js library to build a responsive and interactive donut chart in a React application. The author walks through the steps required to create a data visualization component, and explains key concepts such as creating and updating SVG elements, data binding, and color schemes. The final result is a highly customizable and visually appealing donut chart that can be integrated into any React project.

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