DonutChart
A donut/pie chart with center content, percentage display, arc labels, and interactive legends.
import { DonutChart } from "metricui";Use DonutChart to show proportional data — how parts relate to a whole. It supports center content for summary values, arc labels, link labels, interactive legends, and configurable inner radius (set to 0 for a pie chart). For categorical comparison with absolute values, use BarChart instead.
Basic Example
<DonutChart
data={[
{ id: "organic", label: "Organic", value: 42 },
{ id: "direct", label: "Direct", value: 26 },
{ id: "referral", label: "Referral", value: 16 },
{ id: "social", label: "Social", value: 10 },
{ id: "email", label: "Email", value: 6 },
]}
title="Traffic Sources"
/>Center Content
Use centerValue and centerLabel for simple text, or centerContent for a fully custom React node.
<DonutChart
data={browserData}
title="Browser Share"
centerValue="63.5%"
centerLabel="Chrome"
showPercentage
/>Arc & Link Labels
Enable enableArcLabels to show values directly on slices, or enableArcLinkLabels for lines connecting slices to external labels. Use arcLabelsSkipAngle to hide labels on small slices.
<DonutChart
data={trafficData}
title="Traffic Sources"
enableArcLabels
enableArcLinkLabels
arcLabelsSkipAngle={15}
arcLinkLabelsSkipAngle={15}
/>Half Donut
Use startAngle and endAngle to create a semi-circle or gauge-style visualization.
<DonutChart
data={trafficData}
startAngle={-90}
endAngle={90}
innerRadius={0.7}
centerValue="42%"
centerLabel="Organic"
sortSlices="none"
title="Traffic Distribution"
/>Simple Data Format
For quick prototyping, pass a plain key-value object via simpleData instead of the full DonutDatum[]array. It's converted internally.
Nothing to show — try adjusting your filters
<DonutChart
simpleData={{
Desktop: 60,
Mobile: 35,
Tablet: 5,
}}
title="Traffic by Device"
centerValue="60%"
centerLabel="Desktop"
/>Data States
Every component handles loading, empty, and error states. Pass individual props or use the grouped state prop.
Loading
Error
Failed to load data
// Loading state
<DonutChart data={[]} title="Traffic" loading />
// Error state
<DonutChart data={[]} title="Traffic" error={{ message: "Failed to load" }} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
data* | DonutDatum[] | — | Array of slices with id, label, value, and optional color. |
index | string | — | Column key for slice labels. Used with the unified flat-row data format. If omitted with categories, auto-inferred as the first string column. |
categories | Category[] | — | Column to use as slice values (typically one entry). Accepts plain strings or CategoryConfig objects ({ key, label?, format?, color?, axis? }). If omitted with index, auto-inferred as all number columns. |
simpleData | Record<string, number> | — | Simple key-value object like { "Chrome": 45, "Firefox": 25 }. Converted to DonutDatum[] internally. `data` takes precedence. |
title | string | — | Chart card title. |
subtitle | string | — | Chart card subtitle. |
description | string | React.ReactNode | — | Description popover. |
footnote | string | — | Footnote. |
action | React.ReactNode | — | Action slot. |
format | FormatOption | — | Format for values in tooltips and labels. |
height | number | 300 | Chart height in px. |
innerRadius | number | 0.6 | Inner radius ratio (0-1). 0 = pie chart. |
padAngle | number | 0.7 | Gap between slices in degrees. |
cornerRadius | number | 3 | Rounded slice edges in px. |
startAngle | number | 0 | Start angle in degrees. |
endAngle | number | 360 | End angle in degrees. |
sortSlices | "desc" | "asc" | "none" | "desc" | Sort slices by value. |
activeOuterRadiusOffset | number | 4 | Hover expansion offset in px. Dense mode defaults to 2. |
enableArcLabels | boolean | false | Show value labels on slices. |
arcLabelsSkipAngle | number | 10 | Minimum angle to show arc label (degrees). |
enableArcLinkLabels | boolean | false | Show lines connecting slices to external labels. |
arcLinkLabelsSkipAngle | number | 10 | Minimum angle to show arc link label. |
showPercentage | boolean | false | Show percentages instead of raw values in tooltips/labels. |
centerValue | string | — | Big number displayed in the donut center. |
centerLabel | string | — | Label below the center value. |
centerContent | React.ReactNode | — | Custom ReactNode for full control of center content. |
borderWidth | number | 1 | Slice border width. |
colors | string[] | — | Series colors. Default: theme palette. |
seriesStyles | Record<string, DonutSeriesStyle> | — | Per-slice color overrides keyed by slice id. |
legend | boolean | LegendConfig | — | Legend configuration. Default: shown with toggle. |
hideZeroSlices | boolean | true | Hide slices with value 0 or null. |
onSliceClick | (slice: { id: string; value: number; label: string; percentage: number }) => void | — | Click handler for slices. |
dense | boolean | false | Compact layout. |
chartNullMode | ChartNullMode | — | Included for API consistency. No behavioral change for donut. |
animate | boolean | true | Enable/disable animation. |
variant | CardVariant | — | Card variant (supports custom strings). CSS-variable-driven via [data-variant]. |
className | string | — | Additional CSS class names. |
classNames | { root?: string; header?: string; chart?: string; legend?: string } | — | Sub-element class name overrides. |
id | string | — | HTML id attribute. |
data-testid | string | — | Test id. |
loading | boolean | — | Loading state. |
empty | EmptyState | — | Empty state. |
error | ErrorState | — | Error state. |
stale | StaleState | — | Stale indicator. |
crossFilter | boolean | { field: string } | — | Enable cross-filter signal on click. true uses the slice id field, { field } overrides. Emits selection via CrossFilterProvider — does NOT change chart appearance. Dev reads selection with useCrossFilter() and filters their own data. |
tooltipHint | boolean | string | — | Show action hint at bottom of tooltip. true: auto ('Click to drill down' / 'Click to filter'). string: custom hint text. false: disable. Default: respects MetricConfig.tooltipHint (true). |
drillDown | boolean | ((event: { id: string; value: number; label: string; percentage: number }) => React.ReactNode) | — | Enable drill-down on slice click. `true` auto-generates a summary KPI row + filtered DataTable from the chart's source data. Pass a render function for full control over the panel content. Requires DrillDown.Root wrapper. When both drillDown and crossFilter are set, drillDown wins. |
drillDownMode | DrillDownMode | "slide-over" | Presentation mode for the drill-down panel. "slide-over" (default) slides from the right, full height. "modal" renders centered and compact. |
Data Shape
interface DonutDatum {
id: string;
label: string;
value: number;
color?: string; // Optional per-slice color
}
// Simple format alternative:
type SimpleDonutData = Record<string, number>;
// e.g. { "Chrome": 45, "Firefox": 25, "Safari": 20, "Edge": 10 }Notes
- Uses forwardRef.
- Uses forwardRef — attach a ref to the root div.
- Legend always shows (alwaysShow: true) regardless of series count.
- simpleData is converted to DonutDatum[] internally; `data` takes precedence when non-empty.
- Center content is rendered using SVG foreignObject for centerContent, or native SVG text for centerValue/centerLabel.
- Set innerRadius to 0 for a pie chart (no hole).
- Stable color assignments — DonutChart remembers which color belongs to which slice across data changes. Filtering down to one slice keeps its original color. No dev action needed.
- crossFilter prop emits a selection signal on click — it does NOT change the chart's appearance. The dev reads the selection via useCrossFilter() and filters their own data.
- drillDown={true} auto-generates a summary KPI row + filtered DataTable from the chart's source data. Pass a render function for custom panel content. Requires DrillDown.Root wrapper.
- When both drillDown and crossFilter are set on the same component, drillDown wins.
Playground
Experiment with every prop interactively. Adjust the controls on the right to see the component update in real time.
Live Preview
Distribution by channel
Code
<DonutChart
data={[{ id: "organic", label: "Organic", value: 42 }, { id: "direct", label: "Direct", value: 26 }, ...]}
title="Traffic Sources"
subtitle="Distribution by channel"
format={{ style: "number" }}
centerValue="42%"
centerLabel="Organic"
/>Props
Adjust props to see the chart update in real time
Switch between sample datasets
Applied to tooltips and arc labels
0 = pie chart, 0.6 = donut (default)
Gap between slices in degrees
Rounded slice edges in px
Start angle in degrees
End angle in degrees (360 = full circle)
How slices are ordered
Hover expansion in px
Border between slices