Spec version v0.15.0

Donut chart

Purpose

A Donut chart shows the composition of a meaningful whole at a single moment — how a total breaks down into its parts, sized by their share. Use it when the primary question is proportion and the parts sum to 100% of something real (cloud spend, test outcomes, traffic by region). Do NOT use a Donut chart to compare absolute values — segment area is harder to judge than bar length. Do NOT use a Donut chart for more than five categories; beyond that, thin segments become indistinguishable. The system does not provide a solid-pie variant; the center hole is always present and is used to surface a KPI or progress metric.

Anatomy

A Donut chart is composed of:

  • Title (required at md and lg): Headline label above the chart. Wraps to two lines; truncates with ellipsis beyond that. Optional at sm.
  • Subtitle (optional): One line of context under the title. Does not wrap.
  • Headline metric (optional): A KPI tile placed beside or above the donut. Recommended for most configurations — the number is the takeaway, the donut shows the breakdown. This is the Puppet pattern.
  • Action menu (optional): Per-widget controls — export, refresh, filter — pinned to the top-right corner.
  • Plot area: The square region containing the donut ring. Inner and outer radii are fixed by density; the inner radius is 60% of the outer radius, giving a ring thickness of 40%.
  • Segments: Annular arcs, one per category, sized in angle as a share of the total. The largest segment starts at 12 o'clock and continues clockwise in descending order by default. Negative values are not representable and are rejected.
  • Centre region: The circular hole inside the ring. In the centre-kpi variant, it holds the total value and a label. In the goal-progress variant, it holds the percentage achieved. In all other variants it is empty.
  • Separator stroke: A 2px gap rendered in the chart background color (--force-color-bg-surface) between adjacent segments. This gap ensures segments remain distinguishable when fills are close in hue.
  • Legend: A list of swatches with category names, values, and percentages. Required at all densities except mini. Default position is to the right of the ring. Each item shows: color swatch, category name, percentage, and (at lg density) the raw value.
  • Tooltip: Appears on hover or keyboard focus of a segment. Shows the category name, the raw value formatted by yFormat, and the percentage of total.

When to use

Use a Donut chart when:

  • The categories sum to a meaningful whole — 100% of cloud spend, 100% of test outcomes.
  • There are between 2 and 5 categories. Aggregate the tail beyond 5 into "Other."
  • The reader cares about each category's share, not its absolute value.
  • A single moment is being summarised, not a trend over time.

Do NOT use a Donut chart when:

  • The reader needs to compare absolute values precisely — use a Bar chart. Segment area is harder to judge than bar length.
  • Two segments are close in size — the eye cannot resolve angular differences below about 5%. Use a Bar chart.
  • More than 5 categories compete for attention — aggregate the tail into "Other" or use a horizontal Stacked bar chart.
  • The values change over time and the trend matters — use a Stacked area chart.
  • The total is not meaningful (parts represent different units) — use a Bar chart.
  • The widget is rendered below 120px wide — use a mini donut or a status pill.

Data requirements

Requirement Constraint Behavior at the limit or when violated
Required fields category (string), value (numeric, non-negative) If either is missing, render the Empty state with an explanation.
Accepted data types String or string-coercible categories. Number values. Mixed types in the same field are rejected at design time.
Minimum data points 2 segments Below 2, fall back to a KPI tile.
Maximum data points 5 segments Above 5, aggregate the tail into "Other" or redirect to a horizontal Stacked bar.
Series count 1 — donut is single-series by definition Multi-series payloads render the Error state with a redirect to a Stacked bar chart.
Null and missing values Treated as zero. Zero-value categories are dropped from the ring but retained in the legend with a (0) badge. Never silently dropped from legend — the reader must see what is missing.
Negative values Not allowed A payload containing negatives renders the Error state with a redirect to a Bar chart.
Duplicate categories Not allowed Duplicate category keys render the Error state.
Sort order Default 'value-desc': largest segment at 12 o'clock, continuing clockwise. sortBy accepts 'value-desc', 'value-asc', 'category-asc', or 'as-provided'.
Aggregation No client-side aggregation. Component sums values for percentage calculation only. Caller provides pre-aggregated category values.

Configuration

Prop Type Default Purpose
CONTENT
title string required Headline label above the chart. Wraps to two lines, then truncates.
subtitle string undefined Optional one-line context under the title. Does not wrap.
headlineMetric { value, label, delta } undefined External KPI shown beside the donut. The Puppet pattern — the number is the takeaway.
DATA
data Array<{ category, value }> required The data payload. Schema defined in Data requirements above.
sortBy 'value-desc' | 'value-asc' | 'category-asc' | 'as-provided' 'value-desc' Controls segment order. Default places the largest segment at 12 o'clock.
yFormat string 'auto' Custom number format for values in tooltip and legend. Supports 'currency', 'percent', 'abbreviated', 'raw'.
VISUAL
variant 'external-kpi' | 'centre-kpi' | 'legend-only' | 'mini' | 'kpi-with-delta' | 'goal-progress' 'external-kpi' Approved variants documented in the Variants section.
colorMapping Record<category, token> system Overrides the automatic categorical color mapping. When shared across sibling widgets, must use the same mapping for consistency.
density 'sm' | 'md' | 'lg' 'md' Widget density. Affects ring size, legend layout, and label visibility.
theme 'light' | 'dark' | 'auto' 'auto' Overrides the dashboard theme.
BEHAVIORAL
legend 'right' | 'bottom' | 'none' 'right' Legend position. 'none' is only valid for the mini variant; all other variants require a legend.
tooltip boolean true When false, hover and focus produce no tooltip.
onMarkClick function undefined Optional drill-down callback. When set, segments become individually focusable and clicking fires the handler with the segment data.
exportable boolean true Whether the action menu exposes an Export option.
interactive boolean true Master switch. When false, the chart renders as a static image.
crossFilter boolean false When true, selecting a segment updates other widgets on the dashboard sharing the same data binding.

Variants

Six approved variants, selected via the variant prop.

External KPI (default)

The donut ring on the left with a KPI tile to the right. The KPI shows a summary number (the total, a count, or another relevant metric) with an optional label. This is the recommended default — the Puppet pattern. The number is the takeaway; the donut shows the breakdown. Use when the total is as important as the composition.

Centre KPI

The total value and a short label are placed inside the ring's hole. The legend sits to the right. Use when horizontal space is constrained or when the breakdown is the primary story and the total is secondary. Avoid this variant when the hole is too small to hold the text legibly — at sm density the centre label is suppressed and the variant degrades to legend-only behavior.

Legend only

The donut ring with a legend and no KPI metric. Use when the donut is one of several comparable charts on the same view — for example, a row of small donuts comparing pass/fail breakdowns across environments. No KPI is rendered.

Mini

No labels, no legend, tooltip-only. The ring occupies the full widget, maximising the visual footprint of the arc. Use inside table cells or as a sub-element within a KPI tile where only the shape of the composition matters. legend: 'none' is implied; setting any other legend value on the mini variant has no effect.

KPI with delta

The external-kpi layout extended with a delta indicator on the KPI tile — a directional badge showing week-over-week or period-over-period change (e.g., +18% w/w). Use when both the total and its movement are the takeaway.

Goal progress

A single filled arc showing actual versus a target value. The filled arc represents the achieved amount; the unfilled arc represents the remaining capacity to target. The centre region always shows the percentage achieved. The legend is replaced by two KPI-style values: the achieved amount and the target. Use when the question is "how close are we to a target?" — not a composition of parts.

This variant uses a single color (the primary chart color) for the filled arc and --force-color-bg-muted for the unfilled arc. sortBy and colorMapping are ignored.

States

Title and chart header always remain visible. The plot area and legend change.

Default (data loaded): Segments are rendered in order, separated by the 2px gap stroke. Zero-value categories are absent from the ring but present in the legend with a (0) badge.

Loading: The ring renders as a full skeleton circle in --force-color-bg-muted. The legend area shows skeleton rows — swatch placeholder, a short bar for the name, a shorter bar for the percentage — in --force-color-bg-muted.

Empty: No data for the selected view. Show a brief explanation and, where applicable, a call to action. An empty ring outline is shown so the chart layout does not collapse.

Error: Data fetch failed, or the payload contained negatives or duplicate categories. Show an explanation and a "Retry" button. For data validation errors (negatives, multi-series), include a redirect suggestion (e.g., "Switch to a Bar chart for data with negative values").

No permission: Replace the entire chart with a locked-state illustration and the message "You do not have access to this data." Do not expose any values or percentages.

Partial data: Some categories are missing for the period. Render the available segments. Add an inline banner below the title explaining what is missing. Show the missing categories in the legend with "n/a" instead of a percentage, and note that displayed percentages exclude the missing share.

Single data point: Only one category exists. Fall back to a KPI tile with the single value and the category name as the label.

Interaction

Behavior Specification
Hover The focused segment lifts radially outward by 8px and brightens. All other segments dim to --force-opacity-subtle (0.7). On touch, tap a segment to focus it; tap the center or background to clear.
Tooltip Anchored to the focused segment, offset 12px toward the outer edge. Shows: category name, raw value formatted by yFormat, percentage of total. For goal-progress: shows the achieved value and the target. Closes on pointer leave or Escape.
Click on segment No-op by default. When onMarkClick is set, segments become individually focusable and clicking fires the handler with { category, value, percentage }.
Click on legend item Toggles that segment's visibility. Remaining segments rescale to fill 100% of the ring; percentages and legend values update accordingly.
Cross-filter Off by default. When crossFilter is true, selecting a segment updates other widgets sharing the same data binding.
Keyboard navigation Tab focuses the chart container. Left and Right arrows step through segments clockwise, lifting each segment and opening its tooltip. Enter fires onMarkClick if set. Escape closes the tooltip and returns focus to the chart container. Continued tabbing moves focus through legend items, then the action menu.
Zoom and pan Not applicable. Donuts are not zoomable or pannable.

Accessibility

Requirement Specification
Color contrast Every segment fill and adjacent segment pair meets WCAG 2.2 AA. Tokens resolve to compliant pairs by default. Custom colorMapping values that break contrast against an adjacent segment are rejected at design review.
Color-blind safety Categorical palette passes Deuteranopia, Protanopia, Tritanopia, and Achromatopsia simulation. Adjacent segments are separated by a 2px gap in the background color so that neighboring fills are distinguishable even when hue contrast alone is insufficient. Color is never the only signal — legend labels and tooltip text always name the category.
Screen reader The chart element has an accessible name (aria-label) derived from title. A summary sentence is exposed to assistive technology: "<title>, <n> segments totalling <total>". Each segment has an accessible label: "<category>, <value>, <percentage>%". A data table fallback is reachable via a "View as table" affordance on keyboard focus.
Keyboard reachability Every interaction is reachable via keyboard. Tab order: chart container → individual segments (clockwise) → legend items → action menu.
Focus indication A visible focus ring (--force-shadow-focus-ring + --force-color-border-focus) on the chart container, each legend item, and each individually focused segment. Segment focus combines the ring with the lift offset so focus state is distinct from hover.
Reduced motion When prefers-reduced-motion: reduce is set, segment lift animations and rescale-on-toggle transitions are removed. Tooltip and focus appear immediately.
Text scaling Component reflows at up to 200% text size without clipping. Legend wraps below the donut when labels overflow. Percentage labels on arcs collapse to tooltip-only when they would overlap.

Sizing and responsive behavior

Density Width range Use case What changes
Small (sm) < 320px Inline widget, narrow column Legend drops to tooltip-only. Centre label suppressed. Inner radius reduced to maximise ring width.
Medium (md) 320–599px Standard dashboard widget Legend on right (or bottom) with swatches, category names, and percentages. Values in tooltip only.
Large (lg) ≥ 600px Full-width report Legend includes raw values alongside percentages. On-arc percentage labels rendered for segments above 8% arc angle.

At sm density with variant="mini", the entire plot area is the ring — no header, no legend, no padding.

Guidance

Choose the variant based on the primary question:

  • External KPI: "How is this total broken down and what is the total?" — most common, the Puppet pattern.
  • Centre KPI: "What is the total, and what is the breakdown?" — when the hole is large enough to hold the number legibly.
  • Legend only: "How do these parts compare in share?" — in a grid of comparable donuts where each stands alone.
  • Mini: "What does the composition shape look like?" — inside a table cell or KPI tile.
  • KPI with delta: "What is the total, how has it changed, and what is the breakdown?" — when trend matters alongside composition.
  • Goal progress: "How close are we to the target?" — not a composition; a progress arc.

Common mistakes to avoid:

  • Do NOT use more than five segments. Thin slices become impossible to compare. Group the tail into "Other."
  • Do NOT use a Donut chart when two segments are nearly equal. An angular difference below ~5% is unreadable. Use a Bar chart instead.
  • Do NOT put categories that do not sum to a meaningful whole into a donut. If the parts are in different units (latency in ms, errors in count, CPU in %), the implied "total" is meaningless — use a Bar chart.
  • Do NOT use a random segment order. Always sort by value descending so the largest segment starts at 12 o'clock, making the dominant share immediately readable.
  • Do NOT suppress the legend on any non-mini variant. The ring alone is never sufficient for screen readers or for precise reading of small segments.
  • Do NOT use colorMapping inconsistently across sibling widgets. If AWS is indigo on the donut, it must be indigo on the bar chart and stacked area on the same dashboard.

Token usage

Segments

  • Fill: --force-color-chart-1 through --force-color-chart-5 (categorical sequence, up to 5 segments)
  • Separator gap: 2px, rendered as --force-color-bg-surface (the chart background)
  • Hover lifted segment: full fill opacity; --force-opacity-subtle (0.7) on all other segments
  • Goal progress unfilled arc: --force-color-bg-muted

Ring geometry

  • Outer radius: fills the plot area square, inset by padding
  • Inner radius: 60% of outer radius (ring thickness = 40%)
  • Segment lift on hover: 8px radial outward offset

Legend

  • Swatch: 12px square at md; 14px square at lg; 10px square at sm
  • Category name: --force-color-text-primary, --force-font-size-sm
  • Percentage: --force-color-text-secondary, --force-font-size-sm
  • Raw value (lg only): --force-color-text-secondary, --force-font-size-sm

Centre region (centre-kpi and goal-progress variants)

  • Value: --force-color-text-primary, --force-font-size-xl (24px) at md; scale with density
  • Label: --force-color-text-tertiary, --force-font-size-xs

On-arc labels (lg density, segments > 8%)

  • Text: --force-color-text-on-primary (white) when the segment fill is dark enough; --force-color-text-primary otherwise
  • Size: --force-font-size-xs

Tooltip

  • Background: --force-color-bg-inverse
  • Text: --force-color-text-on-inverse
  • Swatch: 10px square using the segment's chart color token
  • Anchor offset: 12px from the outer arc edge toward the pointer

Background and structure

  • Chart background: --force-color-bg-surface
  • Focus ring: --force-shadow-focus-ring + --force-color-border-focus
  • Loading skeleton: --force-color-bg-muted

Motion

  • Segment lift on hover: --force-duration-fast (150ms), --force-easing-standard
  • Segment rescale on legend toggle: --force-duration-normal (250ms), --force-easing-standard
  • Tooltip open: --force-duration-fast (150ms), --force-easing-standard
  • Reduced motion override: all durations collapse to 0ms when prefers-reduced-motion: reduce

Related components

Component Relationship When to redirect
Bar chart Categorical alternative. When the reader needs precise comparison of absolute values, when two segments are nearly equal in size, or when there are more than five categories.
KPI tile Single-value fallback. When only one category exists, or when the total is the takeaway without a composition breakdown.
Stacked area chart Composition over time. When the categorical composition changes over time and the trend in each part matters.
Stacked bar chart Horizontal composition alternative. When there are more than five categories, or when the x axis is discrete and comparison across multiple rows is needed.