Source: https://monarchui.dev/components/donut-chart/

Components · Charts

# DonutChart

Parts of one whole as a donut in ChartFrame: total in the middle, a legend of values and shares, and a data table.

@rookcom/monarch-react/charts

Component

## Assets by type

## Usage examples

### Folding into Other, percentages only

### Loading and empty

## Purpose

Shows how a total splits into a few named parts (assets by type, tickets by channel), with the numbers always available as text in the legend and the table.

### Use when

- Two to six parts of one whole, where the share matters more than exact comparison.
- A dashboard card that needs a total and its breakdown together.

### Avoid when

- Comparing parts precisely or ranking many categories: use BarList or BarChart.
- A single bar of proportions in a tight row: use StackedProportionBar.
- Values that do not add up to a meaningful whole, or change over time.

## Design principles

- Built on ChartFrame: the title labels the figure, and loading, empty, error and "View as table" behave like every Monarch chart.
- Series colours come only from the data palette in a fixed order; status colours are never slices. Pin `color` so a slice keeps its colour when filters change.
- More than six slices fold the smallest into a neutral "Other"; the table still lists every segment.
- Text carries the data: the legend shows each value and share, the plot has a generated summary, and small shares read "<1%" instead of disappearing.

## API reference

```tsx
import { DonutChart } from '@rookcom/monarch-react/charts';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `DonutChart` | component | server-safe | `.mn-donut-chart` |

Takes every ChartFrame prop except `children`, `legend`, `table` and `height`; the ref goes to the `<figure>`.

Exported from `@rookcom/monarch-react/charts` with the `DonutChartSegment` and `DonutChartLabels` types.

Generated from the TypeScript declarations of `packages/react/src/components/charts/donut-chart`.

## Properties

Negative and non-finite values count as 0; a zero total shows the empty state.

### `DonutChart`

A donut of parts of one whole, inside ChartFrame: palette slices, a total in the middle, a legend of values and shares, folding into "Other", and the data table ("View as table") with every segment.

| Prop | Type | Description |
| --- | --- | --- |
| `segments` required | `readonly DonutChartSegment[]` |  |
| `title` required | `ReactNode` | The chart's name. Labels the figure. Required. |
| `actions` | `ReactNode` | Period or metric controls in the header, such as a pill TabsFilter or a Select. |
| `centerLabel` | `ReactNode` | Small text under `centerValue`, e.g. "assets". |
| `centerValue` | `ReactNode` | Big text in the middle. Default: the formatted total. Pass `null` for none. |
| `className` | `string` |  |
| `defaultShowTable` | `boolean` | Uncontrolled initial table view. Default `false`. |
| `description` | `ReactNode` | One line under the title: the period, unit or what the chart answers. Describes the figure. |
| `emptyActions` | `ReactNode` | Actions in the empty state, such as a link to set up tracking. |
| `emptyDescription` | `ReactNode` | Empty-state description, such as what will appear and when. |
| `footer` | `ReactNode` | Notes under the chart: source, last updated, a footnote. |
| `formatValue` | `ValueFormatter` | Formats values. Wins over `valueFormat`. |
| `headingLevel` | `ChartFrameHeadingLevel` | Render the title as a heading (`h2`–`h6`) to match the page outline. Default: not a heading. |
| `id` | `string` |  |
| `labels` | `DonutChartLabels` |  |
| `legendContent` | `"value" \| "none" \| "percent" \| "both"` | What each legend entry shows after its label. Default `both` ("1,204 · 34%"). |
| `locale` | `string` | BCP 47 locale for numbers. Pass it when rendering on the server. |
| `maxSegments` | `number` | Most slices drawn. Beyond it the smallest fold into one neutral "Other" slice (the table still lists every segment). Default 6: the five palette colours plus Other. |
| `onRetry` | `(() => unknown)` | Error-state retry. Return the refetch promise to keep the button busy until it settles. |
| `onShowTableChange` | `((showTable: boolean) => void)` |  |
| `percentDigits` | `number` | Decimal places in shares. Default 0 (shares under 1% read "<1%"). |
| `showLegend` | `boolean` | Show the legend beside the donut. Default `true`. |
| `showTable` | `boolean` | Controlled table view. |
| `size` | `number` | Outer diameter in px. Default 160. |
| `sort` | `"desc" \| "none"` | `desc` (default) orders slices largest first; `none` keeps the given order. |
| `status` | `ChartFrameStatus` | `ready` (default) shows the chart; `loading` a skeleton with `aria-busy`; `empty` an empty state; `error` an error with Try again when `onRetry` is set. Exposed as `data-status`. |
| `summary` | `string` | Replaces the generated text summary (the donut's accessible name). |
| `tableToggle` | `boolean` | Show the "View as table" toggle. Default `true` when `table` is set. |
| `thickness` | `number` | Ring thickness in px. Default 22% of `size`. |
| `valueFormat` | `Intl.NumberFormatOptions` | `Intl.NumberFormat` options for values. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `legendContent` | `'both'``'value'``'percent'``'none'` | What each legend entry shows after its label. |
| `sort` | `'desc'``'none'` | Largest slice first (default), or the given order. |
| `size / thickness` | `'number (px)'` | Outer diameter (default 160) and ring width (default 22% of size). |
| `status` | `'ready'``'loading'``'empty'``'error'` | ChartFrame states, exposed as `data-status` on the figure. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Ready | `data-status="ready"` | Donut, centre and legend. |
| Loading | `data-status="loading"` | ChartFrame skeleton with `aria-busy` and "Loading chart". |
| Empty | `data-status="empty"` | Automatically when the total is 0, or set explicitly. |
| Error | `data-status="error"` | ChartFrame error, with Try again when `onRetry` is set. |
| Table view | `data-view="table"` | The visible data table replaces the donut. |

## Interaction behaviour

- Hovering a slice dims the others and shows its label, value and share as a native tooltip.
- "View as table" swaps the donut for the data table and back.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab, then Enter or Space | Toggles "View as table". |

Slices are not focusable; their values are in the legend, the summary and the table.

## Accessibility

Role

figure, with the donut as role="img"

Accessible name

The figure is labelled by

title

; the donut by the generated summary ("Total 864. Laptops 412 (48%), …") or

summary

.

- The SVG and the centre text are hidden; the legend list and the data table are real text.
- The data table is always in the page for screen readers, and visible through the toggle.
- Slices are separated by gaps, so neighbours stay distinct in forced-colours mode, where every slice is drawn in CanvasText.
- Every word (Other, Segment, Value, Share, Total, ChartFrame labels) is replaceable through `labels`.

## Responsive behaviour

- The legend sits beside the donut and wraps below it on narrow screens; the donut never shrinks below `size` unless the container is narrower.

## Theme support

- Slices use `--mn-color-data-1` … `-5`, contrast-checked against every chart background in light, dark and high-contrast themes; "Other" uses `--mn-color-text-subtle`.
- The empty ring is `--mn-color-bg-muted`; the frame uses ChartFrame tokens.

## Do and don't

### Do

- Keep to a handful of parts; let `maxSegments` fold the long tail into Other.
- Pin `color` per entity when the same parts appear in several charts.
- Use `centerLabel` to name the unit of the total ("assets", "tickets").

### Don't

- Do not use status colours for slices, even for statuses: label them instead.
- Do not use a donut for values that do not sum to a whole.
- Do not hide the legend unless the same breakdown is listed next to it.

## Related components

- [ChartFrame](https://monarchui.dev/components/chart-frame/): The container, states and table every chart shares.
- [StackedProportionBar](https://monarchui.dev/components/stacked-proportion-bar/): The same breakdown in one compact bar.
- [BarList](https://monarchui.dev/components/bar-list/): Ranking or comparing parts precisely.
- [StatCard](https://monarchui.dev/components/stat-card/): A single total without a breakdown.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [ChartFrame](https://monarchui.dev/components/chart-frame/) |

## Testing coverage

- Figure named by the title; generated summary; centre total; legend with value and share; hidden table with a total row.
- One palette arc per non-zero segment, largest first; sort none and pinned colours.
- Folding into a neutral Other with every segment kept in the table.
- View-as-table toggle with the keyboard and back.
- Custom centre, formatter, legend content and summary; hiding centre and legend.
- Empty, loading and error states; tiny shares keep a sliver and read "<1%"; ref; SSR.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/donut-chart; Cross-browser axe: /components/donut-chart |

Tests: `packages/react/src/components/charts/donut-chart`

## All example code

### Assets by type

```tsx
import { DonutChart } from '@rookcom/monarch-react/charts';

const devices = [
  { key: 'laptop', label: 'Laptops', value: 412 },
  { key: 'phone', label: 'Phones', value: 268 },
  { key: 'monitor', label: 'Monitors', value: 131 },
  { key: 'tablet', label: 'Tablets', value: 54 },
  { key: 'other', label: 'Accessories', value: 9 },
];

<DonutChart
  title="Assets by type"
  description="All active assets"
  segments={devices}
  centerLabel="assets"
  locale="en-US"
  style={{ width: '100%', maxWidth: '30rem' }}
/>
```

### Folding into Other, percentages only

```tsx
import { DonutChart } from '@rookcom/monarch-react/charts';

<DonutChart
  title="Tickets by channel"
  segments={[
    { key: 'email', label: 'Email', value: 540 },
    { key: 'chat', label: 'Chat', value: 410 },
    { key: 'form', label: 'Web form', value: 160 },
    { key: 'phone', label: 'Phone', value: 70 },
    { key: 'api', label: 'API', value: 25 },
    { key: 'social', label: 'Social', value: 12 },
    { key: 'sms', label: 'SMS', value: 6 },
  ]}
  maxSegments={5}
  legendContent="percent"
  size={140}
  locale="en-US"
  style={{ width: '100%', maxWidth: '30rem' }}
/>
```

### Loading and empty

```tsx
import { DonutChart } from '@rookcom/monarch-react/charts';

<div style={{ display: 'grid', gap: '1rem', width: '100%', maxWidth: '30rem' }}>
  <DonutChart title="Assets by type" segments={[]} status="loading" size={120} />
  <DonutChart
    title="Assets by type"
    segments={[]}
    size={120}
    emptyDescription="Assets appear here once you import them."
  />
</div>
```
