Charts
Box Plot Chart
A distribution summary that shows minimum, quartiles, median, and maximum together.
When to use it: Use it to compare spread, skew, and typical values across several groups.
Example
import {
BoxPlotChart,
BoxPlotChartBox,
BoxPlotChartPlot,
ChartDescription,
ChartTable,
ChartTitle,
ChartTooltip,
} from "@comp0/react";
const responseTimes = [
{ label: "Read", min: 90, q1: 120, median: 150, q3: 190, max: 280 },
{ label: "Write", min: 110, q1: 145, median: 180, q3: 230, max: 340 },
{ label: "Search", min: 70, q1: 100, median: 130, q3: 170, max: 250 },
] as const;
const formatMilliseconds = (value: number) => `${value} ms`;
export function Example() {
return (
<BoxPlotChart
values={responseTimes}
categoryLabel="Operation"
valueLabel="Response time"
formatValue={formatMilliseconds}
className="w-full max-w-2xl rounded-lg has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-4 has-[:focus-visible]:outline-teal-600 dark:has-[:focus-visible]:outline-teal-400"
>
<ChartTitle className="text-base font-semibold text-zinc-950 dark:text-zinc-50">
Response-time spread
</ChartTitle>
<BoxPlotChartPlot
aria-label="Box plot chart comparing response-time spread"
className="mx-auto mt-5 aspect-square w-full max-w-md overflow-visible"
>
{(box) => (
<BoxPlotChartBox box={box} className="group outline-none">
<line
x1={box.x}
x2={box.x}
y1={box.minY}
y2={box.maxY}
className="stroke-teal-700 dark:stroke-teal-300"
vectorEffect="non-scaling-stroke"
/>
<line
x1={box.x - box.width / 3}
x2={box.x + box.width / 3}
y1={box.minY}
y2={box.minY}
className="stroke-teal-700 dark:stroke-teal-300"
vectorEffect="non-scaling-stroke"
/>
<line
x1={box.x - box.width / 3}
x2={box.x + box.width / 3}
y1={box.maxY}
y2={box.maxY}
className="stroke-teal-700 dark:stroke-teal-300"
vectorEffect="non-scaling-stroke"
/>
<rect
x={box.x - box.width / 2}
y={box.q3Y}
width={box.width}
height={box.q1Y - box.q3Y}
className="fill-teal-100 stroke-teal-700 group-data-active:stroke-zinc-950 dark:fill-teal-950 dark:stroke-teal-300 dark:group-data-active:stroke-white"
strokeWidth="1.5"
vectorEffect="non-scaling-stroke"
/>
<line
x1={box.x - box.width / 2}
x2={box.x + box.width / 2}
y1={box.medianY}
y2={box.medianY}
className="stroke-zinc-950 dark:stroke-white"
strokeWidth="2"
vectorEffect="non-scaling-stroke"
/>
</BoxPlotChartBox>
)}
</BoxPlotChartPlot>
<ChartDescription className="mt-5 text-sm text-zinc-600 dark:text-zinc-400">
Write operations have the highest median and widest spread.
</ChartDescription>
<ChartTable className="mt-4 w-full border-collapse text-left text-sm [&_td]:border-t [&_td]:border-zinc-200 [&_td]:py-2 [&_th]:border-zinc-200 [&_th]:py-2 dark:[&_td]:border-zinc-800 dark:[&_th]:border-zinc-800">
<caption className="sr-only">Response-time five-number summaries</caption>
<thead>
<tr>
<th scope="col">Operation</th>
<th scope="col">Min</th>
<th scope="col">Q1</th>
<th scope="col">Median</th>
<th scope="col">Q3</th>
<th scope="col">Max</th>
</tr>
</thead>
<tbody>
{responseTimes.map((item) => (
<tr key={item.label}>
<th scope="row">{item.label}</th>
<td>{formatMilliseconds(item.min)}</td>
<td>{formatMilliseconds(item.q1)}</td>
<td>{formatMilliseconds(item.median)}</td>
<td>{formatMilliseconds(item.q3)}</td>
<td>{formatMilliseconds(item.max)}</td>
</tr>
))}
</tbody>
</ChartTable>
<ChartTooltip className="pointer-events-none z-50 rounded-md bg-zinc-950 px-2 py-1 text-sm text-white shadow-lg dark:bg-zinc-50 dark:text-zinc-950" />
</BoxPlotChart>
);
}Anatomy
Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.
A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.
BoxPlotChart
Native figure sharing ordered five-number summaries and formatting. Owns a DOM element.
ChartTitle
Native figcaption that visibly names the figure. Owns a DOM element.
BoxPlotChartPlot / BoxPlotChartBox
Vertical boxes, whiskers, and median marks against visible axes. Owns a DOM element.
ChartDescription
Visible prose summarizing spread and median. Owns a DOM element.
ChartTable
Native table with every five-number summary. Owns a DOM element.
ChartTooltipOptional
Optional floating summary label shown on hover or focus. Owns a DOM element.
Step by step
- 1
Add the main part
Start BoxPlotChart with ordered five-number summaries and visible category and value labels.
- 2
Add the supporting parts
Render each summary with BoxPlotChartBox so the whole five-number summary is one keyboard-reachable mark.
- 3
Make the behavior clear
Keep the median and whiskers visibly distinct and include every summary value in a native table.
Exampletsx <BoxPlotChart values={responseTimes} categoryLabel="Operation" valueLabel="Response time"> <ChartTitle>Response-time spread</ChartTitle> <BoxPlotChartPlot aria-label="Box plot comparing response-time spread"> {(box) => ( <BoxPlotChartBox box={box}> <rect x={box.x - box.width / 2} y={box.q3Y} width={box.width} height={box.q1Y - box.q3Y} /> </BoxPlotChartBox> )} </BoxPlotChartPlot> <ChartTable> <caption>Five-number summaries</caption> </ChartTable> <ChartTooltip /> </BoxPlotChart>;
Keyboard
- ⇥
- Enters the chart at its current box and leaves with one more Tab.
- →
- Moves to the next box without wrapping.
- ←
- Moves to the previous box without wrapping.
- Home
- Moves to the first box.
- End
- Moves to the last box.
- Esc
- Dismisses an open ChartTooltip.
Forms and accessibility
Charts are descriptive content and do not create form values.
Accessibility checklist
- Keep category and value axis labels visible and format all five summary values with the same units as the table.
- Wrap each summary in BoxPlotChartBox so minimum, quartiles, median, and maximum are one roving tab stop.
- Make whiskers, box, and median visibly distinct without relying on fill color alone.
- Include every five-number summary in a native table; ChartTooltip is an optional enhancement.
API reference
import { BoxPlotChart, BoxPlotChartBox, BoxPlotChartPlot, ChartDescription, ChartTable, ChartTitle, ChartTooltip } from "@comp0/react";BoxPlotChart
DOM elementNative figure sharing ordered five-number summaries and formatting.
| Prop | Type | Description |
|---|---|---|
values | readonly BoxPlotChartValue[] | Finite min, q1, median, q3, and max values in order. |
categoryLabel | string | Visible headings for category and numeric axes. |
valueLabel | string | Visible headings for category and numeric axes. |
formatValue | (value: number) => string | Formats summary ticks and table cells. |
ChartTitle
DOM elementNative figcaption that visibly names the figure.
BoxPlotChartPlot / BoxPlotChartBox
DOM elementVertical boxes, whiskers, and median marks against visible axes.
| Prop | Type | Description |
|---|---|---|
aria-label | string | Concise text alternative naming the distribution comparison. |
yMin | number | Optional numeric bounds and tick count. |
yMax | number | Optional numeric bounds and tick count. |
yTickCount | number | Optional numeric bounds and tick count. |
children | (box: BoxPlotChartBoxState) => ReactNode | Custom box renderer. |
box | BoxPlotChartBoxState | Five-number summary state passed to the mark. |
ChartDescription
DOM elementVisible prose summarizing spread and median.
ChartTable
DOM elementNative table with every five-number summary.
ChartTooltip
OptionalDOM elementOptional floating summary label shown on hover or focus.
Style hooks
Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.
BoxPlotChartPlot / BoxPlotChartBox
| Style hook | Meaning |
|---|---|
[data-min] | The summary endpoints and median. |
[data-median] | The summary endpoints and median. |
[data-max] | The summary endpoints and median. |
[data-active] | The box currently reached by pointer or keyboard. |
ChartTooltip
| Style hook | Meaning |
|---|---|
[data-open] | A value tooltip is visible. |