M
MetricUI
Charts

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

Traffic Sources
<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.

Browser Share
<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.

Traffic Sources
<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.

Traffic Distribution
<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.

Traffic by Device

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

PropTypeDescription
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

Chart height in px.

innerRadius
number

Inner radius ratio (0-1). 0 = pie chart.

padAngle
number

Gap between slices in degrees.

cornerRadius
number

Rounded slice edges in px.

startAngle
number

Start angle in degrees.

endAngle
number

End angle in degrees.

sortSlices
"desc" | "asc" | "none"

Sort slices by value.

activeOuterRadiusOffset
number

Hover expansion offset in px. Dense mode defaults to 2.

enableArcLabels
boolean

Show value labels on slices.

arcLabelsSkipAngle
number

Minimum angle to show arc label (degrees).

enableArcLinkLabels
boolean

Show lines connecting slices to external labels.

arcLinkLabelsSkipAngle
number

Minimum angle to show arc link label.

showPercentage
boolean

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

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

Hide slices with value 0 or null.

onSliceClick
(slice: { id: string; value: number; label: string; percentage: number }) => void

Click handler for slices.

dense
boolean

Compact layout.

chartNullMode
ChartNullMode

Included for API consistency. No behavioral change for donut.

animate
boolean

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

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

Traffic Sources

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