Skip to content

Charts

IDonutChart ​

A ring of slices, for the one question a bar chart answers badly: what a total is made of. Each slice is read by the length of its arc, so the parts stay comparable without a shared baseline.

  • Direct
  • Referral
  • Organic
  • Social
Traffic by source
Category Value Share
Direct4,82041.1%
Referral2,14018.2%
Organic3,60030.7%
Social1,18010.1%

A row with a null value is a missing reading and is left out of the ring — which is not the same as a zero. A negative value is dropped too, with a warning in development: a share of a whole has no way to be less than nothing, and IBarChart is the chart with a baseline to hang one off.

Use it for parts of one whole ​

A donut says "these add up to that". If the numbers are not parts of a total — three regions in different currencies, this year against last — the ring is claiming a relationship that is not there, and a bar chart is the honest shape.

Past about six slices the small arcs stop being separable and the legend turns into a reading exercise. Fold the tail into an "Other" row before that happens.

The hole ​

The center slot is given the total and its formatted form, so the figure the slices add up to sits where the reader is already looking.

  • Direct
  • Referral
  • Organic
  • Social
Traffic by source
Category Value Share
Direct4,82041.1%
Referral2,14018.2%
Organic3,60030.7%
Social1,18010.1%

The slot is sized to the hole, so long content wraps inside the ring rather than running out over the slices, and it never takes pointer events — the slice underneath still hovers at the edges.

Thickness ​

thickness is the width of the ring in pixels, defaulting to two fifths of the radius. A thinner ring reads as a gauge; a thicker one reads as a total that has been divided up.

Storage, thin
Category Value Share
Used6868%
Free3232%
Storage, default
Category Value Share
Used6868%
Free3232%
Storage, thick
Category Value Share
Used6868%
Free3232%

size is the rendered height in pixels and, once the container is wide enough, the ring's diameter — the circle is centred in whatever width it is given. The root fills the width it is in, so a row of charts wants a grid or an explicit width on each; dropped straight into a flex row they each claim the full line.

Pie ​

pie fills the middle in. The ring is the better default: a pie asks the reader to compare angles and areas rather than arc lengths, and it gives up the hole, which is the most valuable space on the chart.

  • Starter
  • Team
  • Enterprise
Accounts by plan
Category Value Share
Starter1,24053.7%
Team86037.2%
Enterprise2109.1%

The gap between slices ​

gap is the space between neighbours, in pixels — 0 closes it and lets them touch.

Plans, no gap
Category Value Share
Starter1,24053.7%
Team86037.2%
Enterprise2109.1%
Plans, default gap
Category Value Share
Starter1,24053.7%
Team86037.2%
Enterprise2109.1%
Plans, wide gap
Category Value Share
Starter1,24053.7%
Team86037.2%
Enterprise2109.1%

It is a width, not an angle, and it stays that width the whole way across. Each edge is pushed half a gap off its own radius rather than turned by a fixed angle, which is the difference between a divider that reads as a line and one that reads as a wedge widening towards the rim.

A pie is drawn the same way, which is why its slices stop a hair short of the middle: two parallel edges meet before they reach the centre, and that meeting point is the tip. Wide gaps make that blunt tip visible — another reason the ring is the better default.

Colour follows the entity ​

A slice takes its colour from its position in the array unless you pin it with slot. Pin every slice that can be filtered in and out, so its colour survives the ones around it disappearing.

  • Direct
  • Referral
  • Organic
  • Social
All sources
Category Value Share
Direct4,82041.1%
Referral2,14018.2%
Organic3,60030.7%
Social1,18010.1%
  • Referral
  • Organic
Two sources, colours pinned
Category Value Share
Referral2,14037.3%
Organic3,60062.7%

The palette defines eight categorical slots. Past the eighth the last one repeats and a warning is emitted — two identities sharing a colour is worse than being told about it.

Formatting ​

locale and format go to Intl.NumberFormat and cover every value the chart shows. The share is always a percent, whatever format says: it is a proportion, and currency options applied to 0.42 would render it as money.

  • Starter
  • Team
  • Enterprise
Revenue by plan
Category Value Share
Starter1.240 €53,7 %
Team860 €37,2 %
Enterprise210 €9,1 %

Accessibility ​

The slices are aria-hidden, and the data is exposed instead as a table with a column for the value and a column for the share. label names it — pass one, or the figure reaches assistive tech unnamed.

Animation ​

Every slice is on screen from the first frame, all of them stacked at twelve o'clock, and they unfurl together into place. Not a hand sweeping round the clock, which would reveal them in turn.

  • Direct
  • Referral
  • Organic
  • Social
Traffic by source
Category Value Share
Direct4,82041.1%
Referral2,14018.2%
Organic3,60030.7%
Social1,18010.1%

animate takes false to switch the reveal off, or an object to tune it: duration in milliseconds and easing, one of ease-out (the default), ease-in, ease-in-out or linear.

This is the one chart whose reveal is not a CSS transition. A path's d is not an animatable property, so the geometry is recomputed each frame against an eased progress value — which is why the easing is a named curve rather than any CSS timing function: the same control points have to be solvable in JavaScript.

It plays once, on the first paint with something to draw — not again when the data changes underneath it. A reader who has asked for reduced motion gets the finished chart with no animation at all.

Props ​

PropTypeDefaultDescription
dataDonutChartDatum[][]Rows of { label, value, slot? }
sizenumber240Rendered height in px, and the diameter once the box is wide enough
thicknessnumber—Ring width in px; defaults to two fifths of the radius
piebooleanfalseFill the middle in
gapnumber2Space between neighbouring slices in px; 0 closes it
animateboolean or { duration, easing }trueReveal on the first paint; false turns it off
legendbooleantrueShow the legend
localestring—Passed to Intl.NumberFormat
formatIntl.NumberFormatOptions—Passed to Intl.NumberFormat
labelstring—Accessible name for the figure and its data table
unstyledboolean—Skip built-in classes
classstring—Classes for the root
ui{ root?, svg?, slice?, center?, tooltip?, tooltipLabel?, tooltipValue?, tooltipShare?, table? }—Per-element class overrides

Slots ​

SlotPropsDescription
center{ total, formatted }Content for the hole; ignored when pie