DEV Community

Cover image for React D3 Donut Chart
Vineeth.TR
Vineeth.TR

Posted on

React D3 Donut Chart

D3.js is one of the best JavaScript library for data visualizations,
let's combine D3 and React create a simple Donut Chart.
I am not going to explain how we can create react app 😉
We can start from d3 onboarding.

Add D3 on you project

npm i --save d3
Enter fullscreen mode Exit fullscreen mode

Create DonutChart component

After installing D3 we will create a new react component called DonutChart. here I am giving file name 'DonutChart.js'.

import React , { Component} from 'react';

class DonutChart extends Component {

    constructor(props) {
        super(props);
    }

   componentDidMount() {

  }

render() {
        return <>
            <div></div> 
          </>
    }
}

export default DonutChart;
Enter fullscreen mode Exit fullscreen mode

Use D3 in React component

We will import D3 in to our component

import * as d3 from 'd3';
Enter fullscreen mode Exit fullscreen mode

Create D3 selectors with React refs

class DonutChart extends Component {

  constructor(props) {
        super(props);
        this.chRef = React.createRef();
    }

  componentDidMount() {
    const divEl = d3.select(this.chRef.current)
  }

  render() {
        return <>
            <div ref={this.chRef}></div>
          </>
    }
}
Enter fullscreen mode Exit fullscreen mode

Donut Painting

Create a colors Array for the donut partition painting

const colors = [ '#8ce8ad', '#57e188', '#34c768', '#2db757', '#27acaa', '#42c9c2', '#60e6e1', '#93f0e6', '#87d3f2', '#4ebeeb', '#35a4e8', '#188ce5', '#542ea5', '#724bc3', '#9c82d4', '#c981b2', '#b14891', '#ff6d00', '#ff810a', '#ff9831', '#ffb46a', '#ff9a91', '#ff736a', '#f95d54', '#ff4136', '#c4c4cd' ];
Enter fullscreen mode Exit fullscreen mode

Or else we can create random color generator. in this option you need to also consider text colors for readability and better user experiences

const colors = ()=> {  '#' +  Math.floor(Math.random()*16777215).toString(16)}
Enter fullscreen mode Exit fullscreen mode

Draw donut chart

let's deep in to the show 🏊🏻, We will create a drawChart() method for portability.

    // DrawChart 
    drawChart(){
        const {data } = this.props;
        const svgContainer = d3.select(this.chRef.current).node();
        const width  = svgContainer.getBoundingClientRect().width;
        const height = width;
        const margin = 15;
        let radius = Math.min(width, height) / 2  - margin;

        // Create SVG
        const svg  = d3.select(this.chRef.current)
        .append('svg')
        .attr("width", '100%')
        .attr("height", '100%')
            .attr('viewBox', '0 0 ' + width + ' ' + width )
        //.attr('preserveAspectRatio','xMinYMin')
        .append("g")
        .attr("transform", "translate(" + Math.min(width,height) / 2 + "," + Math.min(width,height) / 2 + ")");

        let pie = d3.pie()
            .value( d => d.value )
        let data_ready = pie(data)

        // Donut partition  
        svg
        .selectAll('whatever')
        .data(data_ready)
        .enter()
        .append('path')
        .attr('d', d3.arc()
            .innerRadius(radius/ 1.75)  // This is the size of the donut hole
            .outerRadius(radius)
        )
        .attr('fill',  (d) =>  colors[d.index] )
        .attr("stroke", "#fff")
        .style("stroke-width", "2")
        .style("opacity", "0.8")


     }


Enter fullscreen mode Exit fullscreen mode

Show partition name and value

We can add labels also for identifying the donut partition.

// legend Position
let legendPosition = d3.arc().innerRadius(radius/1.75).outerRadius(radius);

// Legend group and legend name 
svg
.selectAll('mySlices')
.data(data_ready)
.enter()
.append('g')
.attr("transform", d => `translate(${legendPosition.centroid(d)})`)
.attr("class", 'legend-g')
.style("user-select", "none")
.append('text')
.text(d => d.data.name)
.style("text-anchor", "middle")
.style("font-weight", 700)
.style("fill", '#222')
.style("font-size", 14);

//Label for value
svg
.selectAll('.legend-g')
.append('text')
.text((d) => { return d.data.value })
.style("fill", '#444')
.style("font-size", 12)
.style("text-anchor", "middle")
.attr("y", 16);

Enter fullscreen mode Exit fullscreen mode

View DonutChart.js

let's see full picture of DonutChart.js

import React , { Component} from 'react';
import * as d3 from 'd3';
const colors = [ '#8ce8ad', '#57e188', '#34c768', '#2db757', '#27acaa', '#42c9c2', '#60e6e1', '#93f0e6', '#87d3f2', '#4ebeeb', '#35a4e8', '#188ce5', '#542ea5', '#724bc3', '#9c82d4', '#c981b2', '#b14891', '#ff6d00', '#ff810a', '#ff9831', '#ffb46a', '#ff9a91', '#ff736a', '#f95d54', '#ff4136', '#c4c4cd' ];

class DonutChart extends Component {

    constructor(props) {
        super(props);
        this.chRef = React.createRef();
    }


    // Chart load after component Mount
    componentDidMount() {
        this.drawChart()
    }


    // DrawChart 
    drawChart(){
        const {data } = this.props;
        const svgContainer = d3.select(this.chRef.current).node();
        const width  = svgContainer.getBoundingClientRect().width;
        const height = width;
        const margin = 15;
        let radius = Math.min(width, height) / 2  - margin;
        // legend Position
        let legendPosition = d3.arc().innerRadius(radius/1.75).outerRadius(radius);

        // Create SVG
        const svg  = d3.select(this.chRef.current)
        .append('svg')
        .attr("width", '100%')
        .attr("height", '100%')
            .attr('viewBox', '0 0 ' + width + ' ' + width )
        //.attr('preserveAspectRatio','xMinYMin')
        .append("g")
        .attr("transform", "translate(" + Math.min(width,height) / 2 + "," + Math.min(width,height) / 2 + ")");

        let pie = d3.pie()
            .value( d => d.value )
        let data_ready = pie(data)

        // Donut partition  
        svg
        .selectAll('whatever')
        .data(data_ready)
        .enter()
        .append('path')
        .attr('d', d3.arc()
            .innerRadius(radius/ 1.75)  // This is the size of the donut hole
            .outerRadius(radius)
        )
        .attr('fill',  (d) =>  colors[d.index] )
        .attr("stroke", "#fff")
        .style("stroke-width", "2")
        .style("opacity", "0.8")


      // Legend group and legend name 
       svg
        .selectAll('mySlices')
        .data(data_ready)
        .enter()
        .append('g')
        .attr("transform", d => `translate(${legendPosition.centroid(d)})`)
        .attr("class", 'legend-g')
        .style("user-select", "none")
        .append('text')
        .text(d =>  d.data.name)
        .style("text-anchor", "middle")
        .style("font-weight", 700)
        .style("fill", '#222')
        .style("font-size", 14);

       //Label for value
        svg
        .selectAll('.legend-g')
        .append('text')
        .text((d)=>{ return  d.data.value})
        .style("fill", '#444')
        .style("font-size", 12)
        .style("text-anchor", "middle")
        .attr("y", 16 );
    }



    render() {
        return <>
            <div ref={this.chRef}></div> </>
    }


}

export default DonutChart;
Enter fullscreen mode Exit fullscreen mode

Import and Use

Let's Import and use DonutChart in application

import './App.css';
import DonutChart from './charts/DonutChart.js';
const donutData = [
 {name: "<5", value: 19},
 {name: "5-9", value: 20},
 {name: "10-14", value: 19},
 {name: "15-19", value: 24},
 {name: "20-24", value: 22},
 {name: "25-29", value: 29},
 {name: "30-34", value: 22},
 {name: "35-39", value: 18},
 {name: "40-44", value: 23},
 {name: "45-49", value: 19},
 {name: "50-54", value: 16},
 {name: "55-59", value: 19},
 {name: "60-64", value: 28},
 {name: "65-69", value: 17},
 {name: "70-74", value: 20},
 {name: "75-79", value: 17},
 {name: "80-84", value: 18},
 {name: "≥85", value: 21}
]

function App() {
  return (
    <div className="App">
       <DonutChart data={donutData}  />
    </div>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Result

image

Top comments (2)

Collapse
 
dannyengelman profile image
Danny Engelman • Edited

Apologies;
couldn't resist doing it with a 999 (Gzip) Bytes W3C standard Web Component
and only HTML:

Collapse
 
vineethtrv profile image
Vineeth.TR

Great 👏