v1.2.0
  1. ds
  2. Data display
  3. Donut and pie

Donut and pie

Parts of a whole, with the total in the middle.

A part of a whole, when there are few parts and the whole matters. The middle holds the total, which is the main reason to pick a donut over a bar. Past five segments, or when the parts are close in size, use horizontal bars: angles are hard to compare.

  • Compute$8,42068%
  • Storage$1,44012%
  • Network$1,21010%
  • Managed DB$9608%
  • Other$3103%
Show code
<div class="donut-row">
  <div class="donut" role="img" aria-label="Cloud spend for September, $12.3k: compute 68%, storage 12%, network 10%, managed databases 8%, other 3%">
  <svg viewBox="0 0 42 42" aria-hidden="true"><circle class="track" cx="21" cy="21" r="15.915"/><circle class="s1" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="67.63 32.37" stroke-dashoffset="0.00"><title>Compute: 68.2%</title></circle><circle class="s2" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="11.07 88.93" stroke-dashoffset="-68.23"><title>Storage: 11.7%</title></circle><circle class="s3" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="9.21 90.79" stroke-dashoffset="-79.90"><title>Network: 9.8%</title></circle><circle class="s4" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="7.18 92.82" stroke-dashoffset="-89.71"><title>Managed DB: 7.8%</title></circle><circle class="s6" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="1.91 98.09" stroke-dashoffset="-97.49"><title>Other: 2.5%</title></circle></svg>
  <div class="donut-c"><b>$12.3k</b><span>September</span></div>
</div>
  <ul class="ch-legend col">
    <li class="s1"><span>Compute</span><b>$8,420</b><small>68%</small></li>
    <li class="s2"><span>Storage</span><b>$1,440</b><small>12%</small></li>
    <li class="s3"><span>Network</span><b>$1,210</b><small>10%</small></li>
    <li class="s4"><span>Managed DB</span><b>$960</b><small>8%</small></li>
    <li class="s6"><span>Other</span><b>$310</b><small>3%</small></li>
  </ul>
</div>

Segments are circles with stroke-dasharray on a circumference of 100, so the dash is the share in percent and the offset is the running total. A 0.6 gap separates them. Hovering one thickens it and dims the rest; the <title> in each gives the exact share.

Status split and sizes

Code
<div class="donut thin" style="--size:7rem" role="img" aria-label="Responses by status: 97.6% 2xx, 2.1% 4xx, 0.3% 5xx">
  <svg viewBox="0 0 42 42" aria-hidden="true"><circle class="track" cx="21" cy="21" r="15.915"/><circle class="c-ok" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="97.00 3.00" stroke-dashoffset="0.00"><title>2xx: 97.6%</title></circle><circle class="c-warn" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="1.50 98.50" stroke-dashoffset="-97.60"><title>4xx: 2.1%</title></circle><circle class="c-down" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="0.10 100.30" stroke-dashoffset="-99.70"><title>5xx: 0.3%</title></circle></svg>
  <div class="donut-c"><b>97.6%</b><span>2xx</span></div>
</div>
<div class="donut" style="--size:5.5rem" role="img" aria-label="Storage quota 71% used">
  <svg viewBox="0 0 42 42" aria-hidden="true"><circle class="track" cx="21" cy="21" r="15.915"/><circle class="s1" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="70.40 29.60" stroke-dashoffset="0.00"><title>Used: 71.0%</title></circle><circle class="rest" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="28.40 71.60" stroke-dashoffset="-71.00"><title>Free: 29.0%</title></circle></svg>
  <div class="donut-c"><b>71%</b><span>quota</span></div>
</div>
<div class="donut pie" style="--size:4rem" role="img" aria-label="Spend split, pie">
  <svg viewBox="0 0 42 42" aria-hidden="true"><circle class="track" cx="21" cy="21" r="15.915"/><circle class="s1" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="69.39 30.61" stroke-dashoffset="0.00"><title>Compute: 70.0%</title></circle><circle class="s2" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="11.37 88.63" stroke-dashoffset="-69.99"><title>Storage: 12.0%</title></circle><circle class="s3" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="9.46 90.54" stroke-dashoffset="-81.96"><title>Network: 10.1%</title></circle><circle class="s4" cx="21" cy="21" r="15.915" pathLength="100" stroke-dasharray="7.38 92.62" stroke-dashoffset="-92.02"><title>Managed DB: 8.0%</title></circle></svg>
</div>

Status codes take status colours, because there the colour is the meaning. One value against a capacity is better as a ring; here the free share is a .rest segment with no stroke, so the track shows through. A pie (.pie) suits a 4rem thumbnail with no label; at larger sizes the hole is more useful than the extra ink.

ClassOnWhat it does
.donutdivThe square frame. --size (9rem), --sw the ring width (5 of 42). Modifiers: thin, pie.
circlesvgr="15.915" pathLength="100" on a 42 viewBox. stroke-dasharray="share 100-share", stroke-dashoffset="-start". Colour from a series or status class. .track for the empty ring, .rest for a remainder that shows the track.
.donut-cdivThe centre: a value in b, a label in span.
.donut-rowdivDonut and a .ch-legend.col beside it, wrapping on narrow screens. small in a legend item is the share.
donut()src/lib/viz.tsBuilds the SVG from name and value pairs.
Do
  • The total in the middle.
  • Largest segment first, clockwise from twelve o'clock, "other" last in the neutral.
  • Values and shares in the legend.
Don't
  • More than five segments.
  • Two donuts side by side to compare: use bars.
  • Labels around the ring with leader lines.

Accessibility

  • role="img" on the frame, with every share in the label. The legend repeats the numbers as text.