Skip to content

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.

On this page

Example

Loading example…
Box Plot Chart.tsxtsx
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.

  1. BoxPlotChart

    Native figure sharing ordered five-number summaries and formatting. Owns a DOM element.

  2. ChartTitle

    Native figcaption that visibly names the figure. Owns a DOM element.

  3. BoxPlotChartPlot / BoxPlotChartBox

    Vertical boxes, whiskers, and median marks against visible axes. Owns a DOM element.

  4. ChartDescription

    Visible prose summarizing spread and median. Owns a DOM element.

  5. ChartTable

    Native table with every five-number summary. Owns a DOM element.

  6. ChartTooltipOptional

    Optional floating summary label shown on hover or focus. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start BoxPlotChart with ordered five-number summaries and visible category and value labels.

  2. 2

    Add the supporting parts

    Render each summary with BoxPlotChartBox so the whole five-number summary is one keyboard-reachable mark.

  3. 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

Importtsx
import { BoxPlotChart, BoxPlotChartBox, BoxPlotChartPlot, ChartDescription, ChartTable, ChartTitle, ChartTooltip } from "@comp0/react";

BoxPlotChart

DOM element

Native figure sharing ordered five-number summaries and formatting.

PropTypeDescription
valuesreadonly BoxPlotChartValue[]Finite min, q1, median, q3, and max values in order.
categoryLabelstringVisible headings for category and numeric axes.
valueLabelstringVisible headings for category and numeric axes.
formatValue(value: number) => stringFormats summary ticks and table cells.

ChartTitle

DOM element

Native figcaption that visibly names the figure.

BoxPlotChartPlot / BoxPlotChartBox

DOM element

Vertical boxes, whiskers, and median marks against visible axes.

PropTypeDescription
aria-labelstringConcise text alternative naming the distribution comparison.
yMinnumberOptional numeric bounds and tick count.
yMaxnumberOptional numeric bounds and tick count.
yTickCountnumberOptional numeric bounds and tick count.
children(box: BoxPlotChartBoxState) => ReactNodeCustom box renderer.
boxBoxPlotChartBoxStateFive-number summary state passed to the mark.

ChartDescription

DOM element

Visible prose summarizing spread and median.

ChartTable

DOM element

Native table with every five-number summary.

ChartTooltip

OptionalDOM element

Optional 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 hookMeaning
[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 hookMeaning
[data-open]A value tooltip is visible.

Keep exploring