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.
Assets by type
All active assets
- Laptops 412 · 47%
- Phones 268 · 31%
- Monitors 131 · 15%
- Tablets 54 · 6%
- Accessories 9 · 1%
| Segment | Value | Share |
|---|---|---|
| Laptops | 412 | 47% |
| Phones | 268 | 31% |
| Monitors | 131 | 15% |
| Tablets | 54 | 6% |
| Accessories | 9 | 1% |
| Total | 874 | 100% |
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' }}
/>Usage examples
Folding into Other, percentages only
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
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>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
colorso 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
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%), …") orsummary.
- 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
sizeunless 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
maxSegmentsfold the long tail into Other. - Pin
colorper entity when the same parts appear in several charts. - Use
centerLabelto 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: The container, states and table every chart shares.
- StackedProportionBar: The same breakdown in one compact bar.
- BarList: Ranking or comparing parts precisely.
- StatCard: A single total without a breakdown.
Dependencies
| Kind | Depends on |
|---|---|
| npm packages | react |
| Monarch components | ChartFrame |
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