# Donut Chart

> An animated donut or pie chart for React and shadcn, with outside labels on leader lines.

A donut or pie whose slices morph between datasets by key: new slices grow out of their seam and retired ones close into it. The active slice lifts out while the centre rolls to its value; outside labels ride the morph on leader lines.

- Tier: Eased Pro
- Interaction: Hover
- Page: https://eased.dev/components/donut-chart
- Installs: motion

## Install

Donut Chart ships with Eased Pro (https://eased.dev/#pricing). Members add the `@eased-pro` registry with their token (https://eased.dev/account), then run:

```bash
npx shadcn add @eased-pro/donut-chart
```

A donut or pie of parts of a whole whose slices morph between datasets by key, built on the free chart core.

## When to use

- Shares of one total across a handful of categories: revenue by plan, traffic by source, spend by team.
- A breakdown that changes with a range control, where seeing a share grow or shrink is the point.
- A card that needs the total and any one part in the same place: the centre reads the total and rolls to the slice you point at.

## When not to use

- More than six categories: fold the tail into "Other", or use a [Bar Chart](/components/bar-chart), where lengths compare more precisely than angles.
- Values over time: use a [Line Chart](/components/line-chart) or [Area Chart](/components/area-chart).
- Several totals side by side: one stacked [Bar Chart](/components/bar-chart) compares them; a row of donuts does not.

## Usage

```tsx example.tsx
import { DonutChart } from "@/components/eased/donut-chart/donut-chart";

const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });

export function Example() {
  return (
    <DonutChart
      data={[
        { key: "starter", label: "Starter", value: 8420 },
        { key: "team", label: "Team", value: 21760 },
        { key: "business", label: "Business", value: 34980 },
        { key: "enterprise", label: "Enterprise", value: 52300 },
      ]}
      label="Revenue by plan, last 30 days"
      format={(v) => usd.format(v)}
      labels="percent"
    />
  );
}
```

## Examples

### Morphing with a range

Give each slice a stable `key` and keep the order fixed. Under a `ChartRangeProvider`, every chart on the page morphs on the same beat when the range changes.

```tsx
import { ChartRangeProvider, useChartRange } from "@/components/eased/_primitives/chart-core/range";

function Plans() {
  const range = useChartRange<"30d" | "12m">()?.range ?? "30d";
  return <DonutChart data={revenue[range]} label={`Revenue by plan, ${range}`} format={usd.format} />;
}

<ChartRangeProvider defaultValue="30d">
  <Plans />
</ChartRangeProvider>
```

### Pie

A solid pie for small spaces. With no centre (a pie, or a ring too small to hold the total), the legend's trailing edge reads the total and the active slice.

```tsx
<DonutChart variant="pie" data={sources} label="Traffic by source" height={180} />
```

## API reference

### DonutChart

| Prop | Type | Default | Description |
|---|---|---|---|
| `data` | `{ key, label, value, color? }[]` | | Clockwise from 12 o'clock in the order given, never sorted. Values ≥ 0. |
| `label` | `string` | | Names the figure, the slider and the hidden table. |
| `variant` | `"donut" \| "pie"` | `"donut"` | A ring with the total in its centre, or a solid pie. |
| `height` | `number` | `240` | The plot; the legend sits above. |
| `format` | `(value: number) => string` | | Values in the centre, labels and screen reader text. |
| `totalLabel` | `string` | `"Total"` | What the centre reads when no slice is active. |
| `labels` | `"percent" \| "value" \| false` | `false` | Names each slice outside the ring on a leader line. They drop out when the chart is too narrow to hold them beside the ring. |
| `legend` | `boolean` | `true` | Keys toggle slices; hovering or focusing one lifts its slice. |
| `defaultHidden` | `string[]` | | Slice keys hidden at first. At least one slice stays on. |
| `onActiveChange` | `(slice \| null) => void` | | The hovered, focused or tapped slice. |
| `className` | `string` | `""` | Appended to the root's classes. |

Each slice keeps the colour it first appeared in, `chartColor(1…6)` by position, while slices come and go around it. Pass `color` to choose one.

## Keyboard

| Key | Action |
|---|---|
| `Tab` | Focuses the ring and activates the first slice. |
| `→` / `↑` | Next slice, clockwise. |
| `←` / `↓` | Previous slice. |
| `Home` / `End` | First or last slice. |
| `Escape` | Back to the total. |

## Accessibility

- The ring is one `role="slider"` named by `label`. Its `aria-valuetext` reads the active slice's name, value and share, or the total when none is active, so arrowing across slices announces each one.
- A visually hidden `<table>` lists every slice's value and share; the SVG, the centre and the outside labels are `aria-hidden`.
- Identity never rests on colour: the legend is on by default, and slots 3 to 6 of the palette sit under 3:1 on a white surface.
- Touch: a tap activates the slice under it and keeps it until the next tap; the page still scrolls over the chart.

## Motion

Slices sweep in clockwise once, the first time the chart scrolls into view (0.9s, ease in-out), so the eye reads the whole before the parts. A new dataset morphs every slice's start and end angle from what is on screen on the chart transition (`smooth`, no overshoot, shared through `ChartRangeContext`): a slice that appears grows out of the seam it will sit in, and one that leaves closes into its seam, so no slice ever crosses another. The active slice lifts out 4px along its middle on `snappy`, because pointing is direct feedback and has to land within a beat; the others dim over 150ms. The centre's name swaps and its number rolls in the direction of change. Outside labels wait for the sweep, then ride every morph frame on their leader lines and step apart where they would collide; their numbers roll. Reduced motion: no sweep, no morph, no lift travel; dimming stays.

## Notes for AI

Pick Donut Chart for a share of one total across six or fewer categories, especially when a range switch should show shares shifting. Pass stable `key`s and a fixed order, `format` for the units, and `labels="percent"` when the card is wide enough to name slices beside the ring. Use `variant="pie"` only where there is no room for a centre.

Do not sort the data per range (slices would trade places instead of morphing), do not build a separate legend or tooltip (the legend, centre and labels are built in), and do not wrap it in another slider or `role="img"`; the ring already is the accessible control.

## Related

- [Bar Chart](/components/bar-chart): compares many categories or several totals more precisely.
- [Line Chart](/components/line-chart): the same values over time.
- [Stat Tile](/components/stat-tile): one number, when the parts don't matter.
