DonutChart
A whole and its parts at a glance: a ring with the total in the middle, or a pie, beside a legend of values and shares.
CI minutes in October
By repository · sample data
- cennge-ui1,61056.7%
- playwright-msi-scraper39013.7%
- payload-cache35012.3%
- ai-niche-scout2809.9%
- spredo-crypto-screener2107.4%
Arrow keys move between the values.
| Repository | Minutes | Share |
|---|---|---|
| cennge-ui | 1,610 | 56.7% |
| playwright-msi-scraper | 390 | 13.7% |
| payload-cache | 350 | 12.3% |
| ai-niche-scout | 280 | 9.9% |
| spredo-crypto-screener | 210 | 7.4% |
| Minutes | 2,840 | 100% |
- The middle holds the total. Hover a part or tap it: the middle shows that part, and its legend row lights up.
- Tab to the ring: the arrows step from part to part, Home and End jump to the first and the last, Escape returns the middle to the total.
- The legend is where the values are read: each part's amount and its share of the whole.
Field marks
- Import
import { DonutChart } from "cennge-ui/charts";- Size
- 3.2 KB gzip for this import, its dependencies aside
- Pattern
- A
<figure>with a focusable group over the ring and a hidden<table>of the parts - Keyboard
- Arrow left Arrow right Arrow up Arrow down HomeEndEsc
- SSR
- Rendered on the server in the Next.js check; hydrates without a warning
- Parts
- Six at most read well; a part of zero or less is left out of the ring
- Tests
- Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: donut-chart
- Source
- src/charts
When to use#
Use a DonutChart to show how one whole splits into a few parts, six at most: minutes by repository, runs by result. The ring gives the proportions at a glance and the legend beside it gives each part's value and share, which is where the eye reads them.
Similar#
- BarChart compares the same values by length, one bar each: the better choice when they are many or close.
- AreaChart with
isStackedshows how a whole splits over time, not at one moment. - Progress shows one part of one whole, such as a task 70% done.
A ring with its total#
index names each part and value holds its amount. The middle shows the total over labels.total; a hovered, tapped or focused part takes its place. The numbers on this page are sample data.
- bug317.6%
- enhancement847.1%
- documentation423.5%
- question211.8%
Arrow keys move between the values.
| Label | Open issues | Share |
|---|---|---|
| bug | 3 | 17.6% |
| enhancement | 8 | 47.1% |
| documentation | 4 | 23.5% |
| question | 2 | 11.8% |
| Open issues | 17 | 100% |
import { DonutChart } from "cennge-ui/charts";
const labels = [
{ label: "bug", issues: 3 },
{ label: "enhancement", issues: 8 },
{ label: "documentation", issues: 4 },
{ label: "question", issues: 2 },
];
export default function OpenIssuesByLabel() {
return (
<DonutChart
aria-label="Open issues in cennge-ui by label"
data={labels}
index="label"
value="issues"
indexLabel="Label"
valueLabel="Open issues"
labels={{ total: "Open issues" }}
/>
);
}A pie#
variant="pie" fills the middle, so there is no total in it: the picked part's legend row lights up and the other parts fade. Here theme colours carry a meaning: passed in success, failed in danger, cancelled in the grey of default.
- Passed1,18491.9%
- Failed634.9%
- Cancelled413.2%
Arrow keys move between the values.
| Result | Runs | Share |
|---|---|---|
| Passed | 1,184 | 91.9% |
| Failed | 63 | 4.9% |
| Cancelled | 41 | 3.2% |
| Total | 1,288 | 100% |
import { DonutChart } from "cennge-ui/charts";
const results = [
{ result: "Passed", runs: 1184 },
{ result: "Failed", runs: 63 },
{ result: "Cancelled", runs: 41 },
];
export default function RunsByResult() {
return (
<DonutChart
aria-label="CI runs in October by result"
data={results}
index="result"
value="runs"
indexLabel="Result"
valueLabel="Runs"
variant="pie"
colors={["success", "danger", "default"]}
/>
);
}The small parts as Other#
colors gives the parts their colours in order; without it they take the palette's slots. default is the grey for a part with no colour of its own, such as the rest folded into Other. valueFormat writes the amounts in the middle, the legend and the table; the shares stay percentages in the provider's locale.
- Pages412 MB62%
- Media139 MB20.9%
- Search index61 MB9.2%
- Logs33 MB5%
- Other19 MB2.9%
Arrow keys move between the values.
| Kind | Size | Share |
|---|---|---|
| Pages | 412 MB | 62% |
| Media | 139 MB | 20.9% |
| Search index | 61 MB | 9.2% |
| Logs | 33 MB | 5% |
| Other | 19 MB | 2.9% |
| Used | 664 MB | 100% |
import { DonutChart } from "cennge-ui/charts";
const disk = [
{ kind: "Pages", megabytes: 412 },
{ kind: "Media", megabytes: 139 },
{ kind: "Search index", megabytes: 61 },
{ kind: "Logs", megabytes: 33 },
{ kind: "Other", megabytes: 19 },
];
export default function DiskUse() {
return (
<DonutChart
aria-label="Disk used by payload-cache"
data={disk}
index="kind"
value="megabytes"
indexLabel="Kind"
valueLabel="Size"
colors={["chart-1", "chart-2", "chart-3", "chart-4", "default"]}
valueFormat={(megabytes) => `${megabytes} MB`}
labels={{ total: "Used" }}
/>
);
}Nothing to divide#
A part of zero or less is left out of the ring, and the arrow keys skip it, though the legend still lists it. When no part is left, emptyContent sits in an empty grey ring, or labels.empty ("No data") without it, the legend is gone and nothing takes focus. The table screen readers get still lists every part, at zero.
Arrow keys move between the values.
| Repository | Failed runs | Share |
|---|---|---|
| cennge-ui | 0 | 0% |
| payload-cache | 0 | 0% |
| ai-niche-scout | 0 | 0% |
| Total | 0 | 0% |
import { DonutChart } from "cennge-ui/charts";
const failed = [
{ repository: "cennge-ui", runs: 0 },
{ repository: "payload-cache", runs: 0 },
{ repository: "ai-niche-scout", runs: 0 },
];
export default function FailedRuns() {
return (
<DonutChart
aria-label="Failed CI runs this week by repository"
data={failed}
index="repository"
value="runs"
indexLabel="Repository"
valueLabel="Failed runs"
emptyContent="No failed runs this week"
/>
);
}Accessibility#
- The figure and the ring are named by
aria-label; the ring is a focusable group described bylabels.keyboardHint. - Focus from the keyboard picks the first part at once, and a polite live region reads each part the arrows reach, as "enhancement: 8 (47.1%)".
- The picked part's legend row lights up; the rows of the others keep their full contrast.
- Screen readers get a table of every part with its value and share, and a last row with the total.
Server rendering#
Nothing in a DonutChart waits for a measure: the ring, the total, the legend and the table are all rendered on the server. The ring pops in as it appears; with reduced motion it is there at once.