Skip to content

Box Plot

ChartsSince 0.4.0
import { BoxPlot } from '@absoluteui/components/box-plot';
import '@absoluteui/components/box-plot/style.css';

Using Angular? See Box Plot for Angular.

An accessible SVG box plot for comparing distributions across categories. It provides:

  • one box per category and series, from the lower to the upper quartile, with a median line, capped whiskers, and outlier dots;
  • data as precomputed statistics ({ min, q1, median, q3, max, outliers? }) or raw values; raw values use type-7 quantiles (linear interpolation, as in spreadsheets) and Tukey whiskers at 1.5 IQR (whiskers: 'tukey') or min/max whiskers without outliers ('minmax');
  • several series grouped side by side, with a legend of toggle buttons and textures (with patterns) so series never differ by color alone;
  • keyboard-reachable boxes (one tab stop, arrow keys) whose names read the five-number summary and outlier count, with a tooltip on focus and hover;
  • a generated summary and a data table fallback with every statistic and the outlier values;
  • the module functions boxStats and quantile, to precompute statistics with the same rules.
  • Comparing the spread, skew, and outliers of a measure across categories (for example latency per region).
  • Summarizing many raw values per category when individual values do not matter.
  • Few values per category: show them directly (Scatter Chart) instead of quartiles.
  • Audiences unfamiliar with quartiles: a Bar Chart of medians or a Histogram may read faster.
  • The shape of one distribution in detail (bimodal data): use Histogram.

Raw values and precomputed statistics, two series

Section titled “Raw values and precomputed statistics, two series”
import { BoxPlot, type BoxPlotSeries } from '@absoluteui/components/box-plot';
const series: BoxPlotSeries[] = [
{ id: 'prod', label: 'Production', data: [[120, 135, 150, 160, 180, 420], { min: 90, q1: 110, median: 125, q3: 140, max: 170, outliers: [310] }] },
{ id: 'staging', label: 'Staging', data: [[140, 150, 170, 190, 210], null] },
];
const host = document.createElement('div');
document.body.append(host);
BoxPlot.create(host, {
label: 'Request latency by region',
categories: ['Europe', 'North America'],
series,
valueAxis: { label: 'Milliseconds' },
valueFormat: { maximumFractionDigits: 0 },
});

Precompute statistics with boxStats and use min/max whiskers

Section titled “Precompute statistics with boxStats and use min/max whiskers”
import { BoxPlot, boxStats, quantile } from '@absoluteui/components/box-plot';
const samples = [4.1, 3.8, 5.2, 4.7, 9.9, 4.4, 4.0];
const stats = boxStats(samples);
const p90 = quantile([...samples].sort((a, b) => a - b), 0.9);
const host = document.querySelector<HTMLElement>('#build-times');
if (host) {
const chart = BoxPlot.create(host, {
label: 'Build time',
description: `90th percentile: ${p90.toFixed(1)} min`,
categories: ['main'],
series: [{ id: 'ci', label: 'CI', data: [stats] }],
});
chart.update({ whiskers: 'minmax', series: [{ id: 'ci', label: 'CI', data: [samples] }] });
}
import { BoxPlot } from '@absoluteui/components/box-plot';
import '@absoluteui/components/box-plot/style.css';
const host = document.querySelector<HTMLElement>('#scores');
if (host) {
const chart = BoxPlot.create(host, {
label: 'Test scores by class',
categories: ['Class A', 'Class B'],
series: [
{ id: 'math', label: 'Math', data: [[62, 70, 71, 78, 85, 99], [55, 60, 68, 72, 74]] },
{ id: 'reading', label: 'Reading', data: [[66, 72, 75, 80, 88], [58, 64, 70, 76, 82]] },
],
valueAxis: { label: 'Score', min: 0, max: 100 },
});
chart.host.addEventListener('aui-series-toggle', (e) => {
console.log(e);
});
}
import { BoxPlot, boxStats } from '@absoluteui/components/box-plot';
const host = document.createElement('div');
document.body.append(host);
BoxPlot.create(host, {
label: 'Latency',
categories: ['EU', 'US'],
series: [{ id: 'prod', label: 'Prod', data: [[10, 12, 14, 16, 90], boxStats([8, 10, 11, 13, 15])] }],
});
<div class="aui-chart aui-box-plot [aui-chart--patterns]">
<div class="aui-chart__header">title, description</div>
<ul class="aui-chart__legend" aria-label="Legend">toggle buttons with texture swatches (more than one series)</ul>
<div class="aui-chart__body">
<svg class="aui-chart__svg" role="group" aria-roledescription="box plot" aria-labelledby="T" aria-describedby="S">
<g class="aui-chart__grid" aria-hidden="true"/> <g class="aui-chart__axis …" aria-hidden="true"/>
<g class="aui-box-plot__boxes">
<g class="aui-box-plot__series aui-chart__series--c1">
<g class="aui-box-plot__whiskers" aria-hidden="true">
<path class="aui-box-plot__whisker"/> <circle class="aui-box-plot__outlier"/>
</g>
<rect class="aui-box-plot__box aui-chart__mark" role="img" aria-label="EU, Prod: median 14, quartiles 12 to 16, whiskers 10 to 16, 1 outlier"/>
<rect class="aui-chart__pattern" aria-hidden="true"/> (second and later series)
<line class="aui-box-plot__median" aria-hidden="true"/>
</g>
</g>
</svg>
<div class="aui-chart__tooltip" aria-hidden="true" hidden/>
</div>
<p class="aui-chart__summary" hidden/> <div class="aui-chart__data">Show data table, table</div>
</div>
BoxPlot.create(host: HTMLElement, options: BoxPlotOptions): BoxPlot

Global configuration key: boxPlot (see Configuration).

Option Type Default Description
label string Chart title: visible text and the accessible name of the plot (aria-labelledby). Must not be blank. Required.
hideLabel boolean false Visually hide the title; it stays the accessible name.
description string Visible description below the title, also referenced by aria-describedby (text only).
summary string Replaces the generated accessible summary (series and category counts, per-series lowest and highest median, hidden series).
valueFormat Intl.NumberFormatOptions Number format for statistics in box names, the tooltip, the data table, the summary, and axis ticks (ticks use fixed fraction digits).
formatValue (value: number) => string Custom value text; overrides valueFormat everywhere except axis ticks.
hiddenSeries readonly string[] Ids of series hidden initially; update() replaces the hidden set.
categories readonly string[] Category labels, one per box group (text only). Required.
series readonly BoxPlotSeries[] Series: { id, label, data: BoxPlotDatum[], color?: 1-8 }, one datum per category. A datum is precomputed statistics { min, q1, median, q3, max, outliers? } (finite, min <= q1 <= median <= q3 <= max; min and max are the whisker ends), raw values number[] (non-finite values ignored), or null (no box). Required.
categoryAxis { label?: string } Category axis title; also the first data table column header (default “Category”).
valueAxis BoxPlotValueAxis ({ label?: string; min?: number; max?: number; tickCount?: number; includeZero?: boolean }) Value axis title, fixed domain (max > min; marks are clamped to it), and approximate tick count (2-20). includeZero is validated but has no effect on a box plot; the automatic domain spans every whisker and outlier.
legend 'auto' | 'show' | 'hide' "auto" auto: legend (toggle buttons) when there is more than one series. Globally configurable.
dataTable 'toggle' | 'visible' | 'hidden' | 'none' "toggle" Data table fallback (one row per category and series: Minimum, Lower quartile, Median, Upper quartile, Maximum, Outliers): behind a Show data table button (built on first open), always visible, visually hidden (screen readers only), or none. Globally configurable.
tooltip boolean true Tooltip on pointer hover and keyboard focus (maximum, quartiles, median, minimum, outlier count). Globally configurable.
patterns boolean false Texture fills on the boxes (and legend keys) of the second and later series so series never differ by color alone (always on in forced colors). Globally configurable.
height number 300 Plot height in CSS px (integer 120-2000); the width follows the container. Globally configurable.
whiskers 'tukey' | 'minmax' "tukey" Whiskers computed from raw values: tukey (whiskers end at the most extreme values within 1.5 IQR of the quartiles; values beyond are outliers) or minmax (whiskers at the minimum and maximum, no outliers). Precomputed statistics are drawn as given. Globally configurable.
locale string "en-US" Number, list, and message locale. Globally configurable. Inherited from common configuration.
direction 'ltr' | 'rtl' "ltr" rtl mirrors the category axis and series order, moves the value axis to the right, and swaps ArrowLeft/ArrowRight. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement The host element.
element SVGSVGElement The plot (svg role=“group”), named by the title and described by the summary.
summary string The generated (or custom) accessible summary.
hiddenSeries string[] Ids of hidden series, in data order.
Method Description
update(options: Partial<BoxPlotOptions>): void Merge, validate, and re-render. Invalid options throw and keep the previous state. Keyboard focus stays on the same box (series id and category).
setSeriesVisible(id: string, visible: boolean): void Show or hide a series programmatically; emits no event.
destroy(): void Remove rendering, host classes, listeners, the resize observer, and the configuration subscription.
boxStats(values: readonly number[], whiskers?: BoxPlotWhiskers): BoxPlotStats | null Module function (not an instance method), exported from @absoluteui/components/box-plot: five-number summary and outliers of raw values with the same rules the chart uses (non-finite values ignored; whiskers default ‘tukey’). Returns null when no finite value remains. Use it to precompute statistics for large samples.
quantile(sorted: readonly number[], p: number): number Module function (not an instance method): quantile p (0-1) of ascending values with linear interpolation (Hyndman-Fan type 7, as in spreadsheets). NaN for an empty array. The input must already be sorted.
Event Detail Bubbles Cancelable Description
aui-series-toggle { id: string; visible: boolean; hidden: string[] } Yes No The user toggled a legend entry (pointer or keyboard). Not emitted for update() or setSeriesVisible().

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-chart Host (shared chart chrome).
aui-box-plot Host modifier for this chart type.
aui-chart--patterns patterns option on: textures shown.
aui-chart__header / __title / __title--hidden / __description / __toolbar Title and description; the toolbar stays hidden.
aui-chart__legend / __legend-entry / __legend-item / __legend-item--hidden / __legend-label Legend list and toggle buttons.
aui-chart__swatch aria-hidden legend and tooltip key (fill and texture).
aui-chart__body / __svg / __layer Plot container (height from –aui-chart-height), SVG, and drawing layer.
aui-chart__grid / __gridline / __zero-line / __axis / __axis-line / __tick-label / __axis-title Axes (aria-hidden).
aui-chart__series--c1 ... --c8 Palette slot classes; set –aui-chart-color from color-chart-<n>.
aui-chart__pattern / aui-chart__pattern-tile Texture overlays on boxes.
aui-chart__mark / __mark--active / __mark--hover Focusable box and its states.
aui-chart__tooltip / __tooltip-title / __tooltip-rows / __tooltip-row / __tooltip-label / __tooltip-value Tooltip (aria-hidden; the focused box carries the same text).
aui-chart__empty Empty state text (No data, All series are hidden).
aui-chart__summary Hidden summary referenced by aria-describedby.
aui-chart__data / __data--hidden / __table-toggle / __table-scroll / __table / __cell--numeric Data table fallback.
aui-box-plot__boxes / __series All boxes, and one group per box (series color slot).
aui-box-plot__box The focusable box (lower to upper quartile).
aui-box-plot__median Median line (aria-hidden).
aui-box-plot__whiskers / __whisker / __outlier Decorative (aria-hidden) whiskers with caps and outlier dots.

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-chart-1 | --aui-color-chart-2 | --aui-color-chart-3 | --aui-color-chart-4 | --aui-color-chart-5 | --aui-color-chart-6 | --aui-color-chart-7 | --aui-color-chart-8 | --aui-color-chart-axis | --aui-color-chart-grid | --aui-color-focus-ring | --aui-color-hover | --aui-color-link | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-muted | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-size-xs | --aui-font-weight-medium | --aui-font-weight-regular | --aui-font-weight-semibold | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-radius-sm | --aui-shadow-md | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-tooltip-max-width | --aui-touch-target-min

type BoxPlotWhiskers = 'tukey' | 'minmax';
interface BoxPlotConfig extends ChartBaseConfig { whiskers: BoxPlotWhiskers }
interface BoxPlotStats { min: number; q1: number; median: number; q3: number; max: number; outliers?: readonly number[] }
type BoxPlotDatum = BoxPlotStats | readonly number[] | null;
interface BoxPlotSeries { id: string; label: string; data: readonly BoxPlotDatum[]; color?: ChartColor }
interface BoxPlotOptions extends ChartCommonOptions, Partial<BoxPlotConfig> {
categories: readonly string[];
series: readonly BoxPlotSeries[];
categoryAxis?: { label?: string };
valueAxis?: BoxPlotValueAxis; // { label?; min?; max?; tickCount?; includeZero? }
}
type BoxPlotSeriesToggleDetail = { id: string; visible: boolean; hidden: string[] };
function boxStats(values: readonly number[], whiskers?: BoxPlotWhiskers): BoxPlotStats | null;
function quantile(sorted: readonly number[], p: number): number;

Pattern: SVG box plot as a named, described group (role=“group” with aria-roledescription “box plot”) of role=“img” boxes with roving focus; data table fallback (native table with every statistic and the outlier values).

Key Context Action
ArrowLeft / ArrowRight Box focused Previous / next category in the same series (mirrored in RTL); categories without a box are skipped.
ArrowUp / ArrowDown Box focused, more than one visible series Same category in the previous / next visible series (nearest box when missing).
ArrowUp / ArrowDown Box focused, one visible series Previous / next category.
Home / End Box focused First / last box of the series.
PageUp / PageDown Box focused Ten boxes back / forward.
Escape Tooltip visible Hides the tooltip without moving focus (WCAG 1.4.13); it returns on the next move.
Tab / Shift+Tab Any Legend buttons, then one tab stop for the plot (the active box), then the Show data table button and table region.
Enter / Space Legend button Shows or hides the series (native button).
Element Role
svg.aui-chart__svg group with aria-roledescription (localized “box plot”), named by the title
boxes img with an aria-label: category, series, five-number summary, outlier count
legend list (ul) named “Legend”; entries are toggle buttons (aria-pressed)
data table table (native) with caption, column headers, and row headers; wide tables scroll in a focusable region
axes, whiskers, median lines, outlier dots, textures, tooltip aria-hidden (decorative duplicates of the accessible data)
Attribute Element Value
aria-labelledby svg id of the visible (or visually hidden) title
aria-describedby svg description id (when provided) and the generated summary id
aria-roledescription svg Localized chart type, “box plot”
aria-label each box Localized “{category}, {series}: median {median}, quartiles {q1} to {q3}, whiskers {min} to {max}”, followed by “, {n} outlier(s)” when there are outliers (for example “EU, Prod: median 14, quartiles 12 to 16, whiskers 10 to 16, 1 outlier”)
tabindex active box 0 on exactly one box (roving); other boxes have none
aria-pressed legend button true while the series is visible
aria-expanded / aria-controls Show data table button Table open state and the table region id

Roving focus: one box is in the tab sequence and remembers the last visited box (series id and category), also across re-renders (resize, update). Toggling a legend entry keeps focus on its button; toggling the table keeps focus on the table button.

Criterion Level Note
1.1.1 Non-text Content A
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.10 Reflow AA
1.4.11 Non-text Contrast AA
1.4.13 Content on Hover or Focus AA
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
4.1.2 Name, Role, Value A

Automated test results: Accessibility and compliance.

  • Each box name reads the five-number summary (median, quartiles, whisker ends) and the outlier count.
  • Outliers are decorative dots; their values are listed in the Outliers column of the data table.
  • The generated summary lists the series and category counts, each series’ lowest and highest median with their categories, and hidden series.
  • Meaning never depends on color alone: textures on the boxes of the second and later series (patterns option), plus legend keys that repeat them.
  • Box outlines are drawn in the series color (3:1 against surfaces); the median line uses the text color.
  • Forced colors: medians CanvasText, outlier dots Canvas-filled; focus and hover use Highlight.
  • No animation: nothing moves on render; hover and focus emphasis transitions are removed under prefers-reduced-motion and animation: none.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver, TalkBack) is pending.
  • aria-roledescription support varies; screen readers without it announce “group”.
  • Individual outliers are not focusable; their values are available in the data table only.

default, empty, all series hidden, series hidden, box missing, box focused (tooltip), box hovered, table open.

From Trigger To
any legend button series hidden or shown; aui-series-toggle; domain rescales
plot not focused Tab active box focused, tooltip shown
box focused arrow keys, Home, End, PageUp, PageDown another box focused
tooltip visible Escape tooltip hidden
table closed Show data table table shown
any update({ whiskers }) raw-value boxes recomputed
  • The box name carries the full five-number summary and the outlier count; outliers themselves are decorative dots, and their values are in the table’s Outliers column.
  • Textures are the non-color cue for series; keep patterns on with several series.
  • Box outlines use the series color (3:1 against surfaces); the translucent fill is decorative.
  • Forced colors: medians CanvasText, outliers Canvas-filled; focus and hover use Highlight.
  • No animation. Manual screen-reader verification is pending.

Width follows the container; each category gets a band shared by its series’ boxes. Category labels are truncated or thinned. Reflows at 320 CSS px.

The category axis runs right to left, series order within a category is mirrored, the value axis moves to the right, and ArrowLeft/ArrowRight swap.

Series use color-chart-1..8 (3:1 against surface, background, and sunken surfaces in all themes). Gridlines color-chart-grid, axes color-chart-axis, labels color-text-muted.

configureAbsoluteUI({ components: { boxPlot: { whiskers: 'minmax', legend: 'show' } } });

Instance options override global and theme defaults. All options are dynamic.

Not applicable (categories are text).

All text uses textContent; SVG uses createElementNS; colors are classes; texture fills reference per-instance pattern ids through the fill attribute. No innerHTML or style attributes.

Raw values are summarized in memory on each render; nothing is persisted, sent, or logged.

Budget: 4 series x 50 categories from raw values (100 values each) < 80 ms in headless Chromium (e2e performance test), including the first render. Raw values are sorted per box on every render; pass boxStats results for large samples.

Up to about 200 boxes (each is one focusable node plus whiskers, a median, an optional texture overlay, and one dot per outlier). Aggregate or precompute larger data.

  • No categories or series, or every datum empty: “No data”; every series hidden: “All series are hidden”.
  • null datum, or raw values with no finite number: no box; “No value” in every table cell of that row.
  • A single raw value: a flat box (quartiles, median, and whiskers equal).
  • Precomputed statistics are drawn as given (whiskers does not apply); outliers defaults to none.
  • A fixed valueAxis domain clamps boxes, whiskers, and outliers to its edges.
  • Long category labels are truncated; the full text stays in box names, tooltip, and table.
Code Category Cause
BOX_PLOT_OPTIONS configuration Blank label; categories not strings; series not an array; empty or duplicate id; label not a string; color not 1-8; data length differs from categories; raw values not numbers; statistics not finite or not ordered min <= q1 <= median <= q3 <= max; outliers not finite numbers; invalid categoryAxis or valueAxis (max <= min, tickCount outside 2-20).
CFG_INVALID_VALUE / CFG_UNKNOWN_OPTION configuration Invalid or unknown configuration (for example whiskers: 'iqr').

A failed construction leaves the host untouched.

  • Vertical boxes only; one datum per category per series.
  • Outliers are not individually focusable.
  • valueAxis.includeZero has no effect.
  • Manual screen-reader verification is pending.
  • Horizontal, notched, or violin plots; mean markers.
  • Logarithmic or broken axes.
  • HTML in labels.