D3.js Pie & Doughnut Charts Tutorial

Maryam Alavi
Name
Maryam Alavi

Updated on

This tutorial covers how to compose pie and doughnut charts with D3.js in React, including labels and legends.

Pie Chart

import React, { useEffect, useRef } from 'react'
import * as d3 from 'd3'
 
function PieChart() {
  const containerRef = useRef(null)
  useEffect(() => {
    const data = [ { label: 'Apples', value: 30 }, { label: 'Bananas', value: 20 } ]
    const width = 640, height = 360
    const radius = Math.min(width, height) / 2 - 16
    const root = d3.select(containerRef.current)
    root.selectAll('*').remove()
    const svg = root.append('svg').attr('viewBox', `0 0 ${width} ${height}`).style('max-width', '100%').style('height', 'auto')
    const g = svg.append('g').attr('transform', `translate(${width / 2}, ${height / 2 - 12})`)
    const color = d3.scaleOrdinal().domain(data.map(d => d.label)).range(['#3b82f6', '#10b981'])
    const arcs = d3.pie().value(d => d.value).sort(null)(data)
    const arc = d3.arc().innerRadius(0).outerRadius(radius)
    g.selectAll('path').data(arcs).join('path').attr('d', arc).attr('fill', d => color(d.data.label))
  }, [])
  return <div ref={containerRef} />
}

Doughnut Chart

import React, { useEffect, useRef } from 'react'
import * as d3 from 'd3'
 
function DoughnutChart() {
  const containerRef = useRef(null)
  useEffect(() => {
    const data = [ { label: 'North', value: 35 }, { label: 'South', value: 20 } ]
    const width = 640, height = 360
    const radius = Math.min(width, height) / 2 - 16
    const root = d3.select(containerRef.current)
    root.selectAll('*').remove()
    const svg = root.append('svg').attr('viewBox', `0 0 ${width} ${height}`).style('max-width', '100%').style('height', 'auto')
    const g = svg.append('g').attr('transform', `translate(${width / 2}, ${height / 2})`)
    const color = d3.scaleOrdinal().domain(data.map(d => d.label)).range(['#3b82f6', '#10b981'])
    const arcs = d3.pie().value(d => d.value).sort(null)(data)
    const arc = d3.arc().innerRadius(radius * 0.6).outerRadius(radius)
    g.selectAll('path').data(arcs).join('path').attr('d', arc).attr('fill', d => color(d.data.label))
  }, [])
  return <div ref={containerRef} />
}

Tips

  • Use d3.pie to compute arc angles and d3.arc to render slices.
  • For doughnuts, set innerRadius to 50–70% of the outerRadius.
  • Add a center label to communicate totals or key metrics.

When to Use Pie vs. Doughnut Charts

Pie and doughnut charts represent parts of a whole at a single point in time. They shine when you have a small number of categories (ideally 3–6) and the goal is to communicate composition quickly. A doughnut chart is a pie chart with a hole; it provides space for a center label (such as the total) and often improves legibility by reducing clutter near the center. If your data contains many tiny categories or if exact comparisons are crucial, prefer a bar chart. Bars are easier to sort and compare precisely, while pies rely on angle and area, which are harder to judge visually.

Data Modeling and Percentages

Model data as { label, value }. To display percentages, compute the total and use value / total. Format with d3.format('.0%') or d3.format('.1%') to control rounding. D3’s pie layout returns arcs with start and end angles but does not round for you—only round for display to avoid mismatches between labels and slice geometry. If categories can overlap (e.g., respondents selecting multiple choices), a pie chart may mislead because it implies a complete partition of a whole. In those cases, pick a different visualization (like stacked bars or grouped bars) that better encodes overlaps.

Labeling Strategies

Labels are often the hardest part of pie/doughnut charts. Common approaches include:

  • Inside labels placed at arc centroids; best when slices are large enough.
  • Outside labels with leader lines; useful when slices are small.
  • Legends placed below or to the side; helpful for dense categories or responsive layouts.

If labels collide, reduce font size slightly, abbreviate category names, move labels outside with lines, or show labels on hover to keep the base chart clean while retaining a clear legend for reference.

Responsiveness and Layout

Use an SVG viewBox so the chart scales inside responsive containers. On small screens, slightly reduce outer radius and increase vertical padding for labels and legends. Consider changing the labeling approach at mobile breakpoints (e.g., legend-only labels). Keep minimum touch target sizes in mind if slices trigger interactions or navigation.

Interactivity and Tooltips

Thoughtful interactivity increases comprehension without distraction:

  • Highlight the hovered slice and show a tooltip with raw value and percentage.
  • Allow toggling categories via the legend to focus attention on specific segments.
  • For doughnuts, use the center label to display the hovered slice’s label/value for compact, glanceable feedback.

Limit animation to brief, purposeful transitions (on load or hover). Avoid exaggerated easing or “exploding” effects unless they communicate meaning.

Color and Accessibility

Choose a categorical palette with good contrast. Do not rely on color alone—pair with text labels or tooltips. Provide <title> elements for screen readers and ensure sufficient contrast ratios. If categories are critical and must be distinguishable in print or for users with color-vision deficiencies, consider adding patterns or textures. Limit the number of distinct colors; group minor categories under an “Other” slice when necessary and document the grouping rule.

Performance Notes

Pie and doughnut charts typically render a small number of arcs and are fast by default. If you update data frequently, update the pie layout and arc paths instead of recreating the entire SVG to minimize DOM churn. In dashboards with many charts, memoize color scales and arc generators to avoid repeated allocations on every render.

Common Pitfalls

  • Too many slices: readability drops beyond 6–8 categories.
  • 3D pies and heavy gradients: distort areas and harm accuracy.
  • Unsorted categories: consider sorting by value or a logical order (alphabetic, domain-specific) to support quick scanning.
  • Values that do not sum to a meaningful whole: use bars or another composition graphic to avoid implying completeness.

FAQs

How do I show percentage labels on slices? Compute value / total and format with d3.format('.0%'). Place inside with arc.centroid(d) when slices are large; otherwise, position labels outside with leader lines.

How can I group small categories? Aggregate them into an “Other” slice to reduce clutter. Document the grouping rule in the legend or a footnote.

Can I transition between pie and doughnut? Yes—animate innerRadius from 0 to a fraction of outerRadius. Keep duration short (300–500ms) and easing subtle.