SVG pie chart, donut chart and progress ring: the code

Two ways to draw a pie chart in SVG: a path with an arc for each slice, or circles with stroke-dasharray. Code for pies, donuts, progress rings and gauges.

Published

SVG has no pie chart element. There are two ways to build one. Draw each slice as a <path>: a line out from the centre, an arc round the rim and a line back. Or draw each slice as a <circle> whose thick, dashed stroke shows only part of the way round. The first gives real wedge shapes, and the second needs no trigonometry.

A pie with a quarter slice and a three-quarter slice, written by hand:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <path d="M 50 50 L 50 10 A 40 40 0 0 1 90 50 Z" fill="teal"/>
  <path d="M 50 50 L 90 50 A 40 40 0 1 1 50 10 Z" fill="gold"/>
</svg>

The teal quarter runs from 12 o’clock to 3 o’clock. The gold slice carries on from 3 o’clock all the way round to 12.

Which method

Path with an arcCircle with a dashed stroke
Sums neededSine and cosine for each slice endNone: values are percentages
Best forPies drawn from data in script, slices that pull out, exact wedge shapesDonuts, progress rings, gauges, charts written by hand or in a template
Each slice isA closed shape that can be filled, outlined and clicked exactlyA stroke: it cannot have its own outline
Animating a valueRewrite the pathChange one number, and CSS can do the transition

Method 1: a path for each slice

One slice is three commands. With the centre at cx,cy, radius r, and the slice running from point 1 to point 2 on the rim:

M cx cy  L x1 y1  A r r 0 large 1 x2 y2  Z

M goes to the centre. L draws out to the start of the slice. A draws the arc to the end of the slice: the two radii, a rotation of 0, the large-arc flag, the sweep flag and the end point. Z closes back to the centre.

The two flags matter:

  • large-arc is 1 when the slice is more than half the pie, over 180 degrees, and 0 otherwise. Get this wrong and a big slice is drawn as the small one on the other side.
  • sweep is 1 to go clockwise.

The points on the rim come from the angle. Measuring from 12 o’clock and going clockwise, the point at angle degrees is:

x = cx + r × sin(angle)
y = cy − r × cos(angle)

The y term is subtracted because y runs downwards in SVG. For the quarter slice above: at 0 degrees the point is 50, 10 and at 90 degrees it is 90, 50. The SVG path guide explains the arc command in full.

From data, in JavaScript

<svg id="chart" viewBox="0 0 100 100" width="300" role="img" aria-label="Materials: wood 40%, acrylic 30%, card 20%, leather 10%"></svg>
<script>
  function slicePath(cx, cy, r, startAngle, endAngle) {
    const point = (angle) => {
      const rad = (angle - 90) * Math.PI / 180;
      return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)];
    };
    const [x1, y1] = point(startAngle);
    const [x2, y2] = point(endAngle);
    const large = endAngle - startAngle > 180 ? 1 : 0;
    return `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} Z`;
  }

  const data = [
    { label: 'Wood', value: 40, colour: 'teal' },
    { label: 'Acrylic', value: 30, colour: 'gold' },
    { label: 'Card', value: 20, colour: 'crimson' },
    { label: 'Leather', value: 10, colour: 'slategray' },
  ];
  const total = data.reduce((sum, d) => sum + d.value, 0);
  const svg = document.querySelector('#chart');
  let angle = 0;
  for (const d of data) {
    const sweep = d.value / total * 360;
    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    path.setAttribute('d', slicePath(50, 50, 40, angle, angle + sweep));
    path.setAttribute('fill', d.colour);
    svg.append(path);
    angle += sweep;
  }
</script>

Four slices, starting at the top and going clockwise: teal for 40%, gold for 30%, red for 20% and grey for 10%. Subtracting 90 degrees inside point turns the usual maths convention, which starts at 3 o’clock, into one that starts at 12.

One case breaks it. A slice of 100% starts and ends at the same point, and an arc between two identical points draws nothing: in Chrome the whole pie vanished. When one value is the whole total, draw a <circle> instead.

Method 2: circles with a dashed stroke

A stroke can be dashed, and a dash can be made as long as a share of the circle. Give a circle a stroke as wide as the pie’s radius and the dash becomes a wedge.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <g fill="none" stroke-width="40" transform="rotate(-90 50 50)">
    <circle cx="50" cy="50" r="20" pathLength="100" stroke="teal" stroke-dasharray="40 100"/>
    <circle cx="50" cy="50" r="20" pathLength="100" stroke="gold" stroke-dasharray="30 100" stroke-dashoffset="-40"/>
    <circle cx="50" cy="50" r="20" pathLength="100" stroke="crimson" stroke-dasharray="20 100" stroke-dashoffset="-70"/>
    <circle cx="50" cy="50" r="20" pathLength="100" stroke="slategray" stroke-dasharray="10 100" stroke-dashoffset="-90"/>
  </g>
</svg>

The same four-slice pie as the script drew, with no script. How it works:

  • The radius is half the pie’s. A stroke is centred on the circle’s edge. With r="20" and stroke-width="40" it reaches from the centre out to 40.
  • pathLength="100" tells the browser to treat the circle’s outline as 100 units long, so that dash lengths can be written as percentages. Without it the dashes would have to be worked out from the circumference, 2 × π × r.
  • stroke-dasharray="40 100" is a dash of 40 followed by a gap of 100: one dash covering 40% of the way round, and a gap long enough that it never repeats.
  • stroke-dashoffset slides the dash along. Each slice is moved on by the sum of the slices before it, written as a negative number: 0, then -40, then -70, then -90.
  • rotate(-90 50 50) turns the lot a quarter turn back. The SVG specification starts a circle’s outline at 3 o’clock and runs it clockwise. The rotation puts the start at 12 o’clock.

Dashed lines covers stroke-dasharray and stroke-dashoffset.

A donut chart

Make the stroke thinner than the radius and the middle opens up. Only two numbers change from the pie: r and stroke-width.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <g fill="none" stroke-width="16" transform="rotate(-90 50 50)">
    <circle cx="50" cy="50" r="32" pathLength="100" stroke="teal" stroke-dasharray="40 100"/>
    <circle cx="50" cy="50" r="32" pathLength="100" stroke="gold" stroke-dasharray="30 100" stroke-dashoffset="-40"/>
    <circle cx="50" cy="50" r="32" pathLength="100" stroke="crimson" stroke-dasharray="20 100" stroke-dashoffset="-70"/>
    <circle cx="50" cy="50" r="32" pathLength="100" stroke="slategray" stroke-dasharray="10 100" stroke-dashoffset="-90"/>
  </g>
  <text x="50" y="50" text-anchor="middle" dominant-baseline="central" font-size="10">100</text>
</svg>

A ring from radius 24 to radius 40, with a number in the hole. The text sits outside the rotated group so that it stays upright.

A progress ring

A progress circle is a donut with one slice, drawn over a pale full ring:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="40" fill="none" stroke="gainsboro" stroke-width="10"/>
  <circle cx="50" cy="50" r="40" fill="none" stroke="teal" stroke-width="10"
    pathLength="100" stroke-dasharray="75 100" transform="rotate(-90 50 50)"/>
  <text x="50" y="50" text-anchor="middle" dominant-baseline="central" font-size="16">75%</text>
</svg>

To show another value, change the 75 in stroke-dasharray and in the text.

In a web page the change can be animated with one line of CSS, transition: stroke-dasharray 0.5s on the coloured circle. Setting the attribute to a new value then slides the bar there: read half way through the transition in Chrome, the computed dash was 63 on its way from 25 to 75.

stroke-linecap="round" gives the bar round ends, with two side effects. Each cap adds half the stroke width to the end of the bar, so it reads a little high. And at 0 the caps still show, as a dot. Leave the bar out, or hide it, when the value is 0.

A gauge

A gauge is the same idea on an open arc. pathLength works on any path:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 60">
  <path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="gainsboro" stroke-width="10"/>
  <path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="teal" stroke-width="10"
    pathLength="100" stroke-dasharray="60 100"/>
</svg>

A half ring from left to right over the top, filled to 60%. No rotation is needed, because the path already starts where the gauge should.

Labels on the slices

A label goes at the middle angle of its slice, part of the way out from the centre. For the quarter slice, the middle angle is 45 degrees. At 25 units from the centre, the formulas above give x = 50 + 25 × sin(45°) = 67.7 and y = 50 − 25 × cos(45°) = 32.3:

<text x="67.7" y="32.3" text-anchor="middle" dominant-baseline="central"
  font-size="6" fill="white">25%</text>

text-anchor="middle" and dominant-baseline="central" centre the text on that point both ways. Thin slices have no room for a label: put those in a legend beside the chart. SVG text covers text placement.

For people who cannot see the chart

A pie chart built from shapes says nothing to a screen reader. Give the <svg> role="img" and an aria-label that states the figures, as the script example does, or put the same numbers in a table near the chart. Accessible SVG has the patterns.

Common problems

What you seeCauseFix
A big slice is drawn as a small one on the wrong sideThe large-arc flag is 0 for a slice over 180 degreesSet it to 1
The slice bulges inwardsThe sweep flagUse 1 for clockwise
A 100% slice draws nothingThe arc starts and ends at the same pointDraw a <circle>
The pie starts at 3 o’clockThat is where a circle’s outline and the usual angle maths both startSubtract 90 degrees, or rotate by -90
The dashed-stroke pie has a hole or overlaps itselfr is not half of stroke-widthFor a full pie, stroke-width is twice r
Dash lengths do not match the percentagesNo pathLength, so dashes are in drawing unitspathLength="100"
The slices do not line upAn offset is positive, or is not the running totalEach stroke-dashoffset is minus the sum of the earlier slices
The label in the middle is on its sideIt is inside the rotated groupMove the <text> out of the group

Questions

Which method is better for a cutting machine? The path method. Each slice is a real closed outline that a cutter can follow. A dashed stroke is only a way of painting: software that follows the outline itself would cut the whole circle.

Can CSS draw a pie chart without SVG? A conic-gradient background on a round element does. SVG is the better choice when the slices need labels, links or hover effects, or when the chart must be saved as a file.

Do I need a chart library? Not for one pie. For many charts, axes and tooltips, a library such as D3 generates the same paths for you.

How do I check my arcs? Paste the SVG into the SVG viewer and change one number at a time.