# Donut Chart

> A ring split into slices that show how parts make up a whole, with the total in the middle and every value in the legend.

- Name: `donut-chart`
- Status: beta
- Categories: charts
- File: the install writes `donut-chart.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `recharts`, `react-is`. The install adds them.
- Other registry items: `@txbrendan/chart`, `@txbrendan/skeleton`. The install adds them.
- Gallery: https://txbrendan.dev/components/donut-chart
- Registry item: https://txbrendan.dev/r/donut-chart.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@txbrendan": "https://txbrendan.dev/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@txbrendan` in a public list that txbrendan/ui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @txbrendan/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @txbrendan/donut-chart
   ```

## Use it for

- Showing how two to five parts make up one whole, like visitors by browser or seats by plan.
- Pass data with one object per slice, the categoryKey that names each slice, and the valueKey that sizes it.
- Map slice names to labels and colors with config, like { chrome: { label: "Chrome" } }.
- Pass centerLabel to say what the total in the middle counts.

## Avoid it for

- Comparing sizes that are close. People judge angles poorly. Use BarChart.
- More than five slices. The theme has five chart colors. Group the rest into Other.
- Values that do not add up to a whole, or negative values. Use BarChart.
- Showing change over time. Use LineChart or AreaChart.

## Props

- `data`: `TData[]`. One object per slice, like { browser: "chrome", visitors: 275 }.
- `categoryKey`: `keyof TData`. The field that names each slice.
- `valueKey`: `keyof TData`. The number field that sizes each slice.
- `config`: `{ [slice: string]: { label: string; color?: string } }`. Labels and colors by slice name. A slice it leaves out shows its name and takes the next chart color.
- `title`: `ReactNode`. A visible title. It names the chart. TypeScript requires title, aria-label, or aria-labelledby.
- `aria-label`: `string`. Names the chart when it has no visible title.
- `centerLabel`: `string`. A word under the total in the middle, like "Visitors".
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. Height: 5, 7, or 9 times --control-h-md, so density scales it. The ring and the total scale with it.
- `valueFormat`: `Intl.NumberFormatOptions`. Formats values in the tooltip, the legend, and the middle.
- `locale`: `string`. Default `"en-US"`. The locale for numbers. Fixed, so server and browser print the same text.
- `loading`: `boolean`. Default `false`. Shows a skeleton ring at the same size, and holds the legend with config names and placeholder values. When data arrives, the ring fades out while the slices draw in.
- `error`: `ReactNode`. Shows this message in place of the chart. true shows a default message.
- `onRetry`: `() => void`. Shows a retry button under the error.
- `emptyText`: `ReactNode`. Default `"No data yet"`. Shown when there is no data, or every value is 0.
- `className`: `string`. Extra classes. Later classes win, so className='h-96' replaces the height.

## Accessibility

- Renders a \<figure> named by its title, or by aria-label. TypeScript requires one.
- The legend is a list of every slice with its value in text, so the numbers do not depend on color or angle.
- The total in the middle is text, and screen readers read it after the chart.
- Recharts' keyboard layer stays on: Tab focuses the chart, the arrow keys move between slices, and the tooltip reads each one out. The ring is one tab stop.
- Keeps one height while it loads, draws, fails, or has no data, so the page does not move. When the page prints, it asks the browser to keep a chart that fits on one page whole. In the light theme, it asks the browser to print legend dots and loading shapes even with backgrounds off.
- Slice colors come from --chart-1 to --chart-5, which the theme tests hold to 3:1 against the page.
- Slices sweep in over three times --motion-duration-base, once the chart is on screen. They finish at once when the page prints. At 0ms they draw at once, and Recharts skips the animation for reduced motion.

## Examples

### Slices with a label in the center

```tsx
<DonutChart
  title="Visitors by browser"
  data={[
    { browser: "chrome", visitors: 275 },
    { browser: "safari", visitors: 200 },
    { browser: "firefox", visitors: 187 },
  ]}
  categoryKey="browser"
  valueKey="visitors"
  config={{ chrome: { label: "Chrome" }, safari: { label: "Safari" }, firefox: { label: "Firefox" } }}
  centerLabel="Visitors"
/>
```

### Currency

```tsx
<DonutChart
  title="Ad spend by channel"
  data={[
    { channel: "Search", amount: 48200 },
    { channel: "Social", amount: 31750 },
  ]}
  categoryKey="channel"
  valueKey="amount"
  centerLabel="Spent"
  valueFormat={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
/>
```

### While it loads, and when it fails

```tsx
<DonutChart
  aria-label="Visitors by browser"
  data={data ?? []}
  categoryKey="browser"
  valueKey="visitors"
  loading={isLoading}
  error={error ? "Visitors could not load." : undefined}
  onRetry={() => refetch()}
/>
```
