> ## Documentation Index
> Fetch the complete documentation index at: https://docs.graphy.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Boxplot

export const GraphEmbed = ({src, story, aspectRatio, borderRadius = '14px', args}) => {
  const STORYBOOK_BASE_URL = typeof window !== 'undefined' && window.location.hostname === 'localhost' ? 'http://localhost:6006' : 'https://storybook-sdk.vercel.app';
  function buildArgsString(args) {
    if (!args) return '';
    const encodeValue = value => {
      if (value === null) return '!null';
      if (value === undefined) return '!undefined';
      if (typeof value === 'boolean') return `!${value}`;
      if (typeof value === 'number') return String(value);
      return encodeURIComponent(String(value));
    };
    const properties = Object.entries(args).map(([key, value]) => `${key}:${encodeValue(value)}`).join(';');
    return `&args=${properties}`;
  }
  const resolvedSrc = story ? `${STORYBOOK_BASE_URL}/iframe.html?id=${story}&viewMode=story&embed=1&globals=mode:readonly${buildArgsString(args)}` : src;
  const resolvedAspectRatio = aspectRatio ?? (story ? '16 / 9' : '10 / 6');
  return <iframe src={resolvedSrc} loading="lazy" allowfullscreen="true" style={{
    width: '100%',
    aspectRatio: resolvedAspectRatio,
    border: 'none',
    borderRadius,
    colorScheme: 'light'
  }} />;
};

A boxplot compares how values spread across categories. Each box runs from the first to the third quartile with a line at the median, whiskers reach out to the furthest values close to the box, and any value beyond them is drawn on its own as an outlier.

<Warning>
  The boxplot ships as its own package, `@graphysdk/geom-boxplot`, on a `0.x`
  version line. While it is on `0.x`, a minor release may change its aesthetics,
  params, style parts or look; pin a caret range (`^0.1.0`) to take only
  patches. Tell us what works and what doesn't on
  [Discord](https://discord.gg/yGmYCjkSr).
</Warning>

## Install

```bash theme={null}
npm install @graphysdk/geom-boxplot
```

It peer-depends on `@graphysdk/react` and nothing else. Register the plugin once with `createGraphyKit`, which adds `kit.geom.boxplot()` and binds `kit.GraphProvider` to the same plugin:

```tsx theme={null}
import {
  createGraphyKit,
  GraphRenderer,
  highlight,
  styles,
} from '@graphysdk/react';
import { boxplot } from '@graphysdk/geom-boxplot';

const kit = createGraphyKit({ plugins: [boxplot] });
```

## One box per category

Pass the raw values, one row per value, and map the category to `x` and the value to `y`. The geom's `boxplot` stat computes the summary:

<GraphEmbed story="chart-types-boxplot--response-times" args={{ orientation: 'vertical', extent: '1.5' }} />

```tsx theme={null}
const data = {
  columns: [{ key: 'service' }, { key: 'region' }, { key: 'ms' }],
  rows: [
    { service: 'Search', region: 'EU', ms: 128 },
    { service: 'Search', region: 'US', ms: 141 },
    { service: 'Checkout', region: 'EU', ms: 204 },
    // …
  ],
};

const spec = kit.pipe(
  kit.createSpec({ x: 'service', y: 'ms' }),
  kit.geom.boxplot(),
  kit.scale.x.discrete(),
  kit.scale.y.continuous()
);

export function ResponseTimes() {
  return (
    <kit.GraphProvider spec={spec} data={data}>
      <GraphRenderer />
    </kit.GraphProvider>
  );
}
```

The quartiles interpolate between the middle values, as R, NumPy and d3 compute them. Each whisker reaches the furthest value within 1.5 interquartile ranges of the box (Tukey's rule), and every value past it is an outlier. The value axis fits every value, outliers included, rather than holding zero. The category axis is always discrete.

## Whisker reach

`extent` sets how far a whisker may reach, in interquartile ranges. `'min-max'` runs the whiskers to the smallest and largest values, so no value is drawn as an outlier:

```tsx theme={null}
kit.geom.boxplot({ params: { extent: 3 } });
kit.geom.boxplot({ params: { extent: 'min-max' } });
```

The whiskers and outliers are computed on the raw values, before a log scale transforms them.

## Summarised data

If your data already holds the summary, one row per box, set `stat` to `identity` and map your own columns: `lowerWhisker`, `q1`, `median`, `q3` and `upperWhisker` are required.

<GraphEmbed story="chart-types-boxplot--summarised" args={{ orientation: 'vertical', varwidth: false }} />

```tsx theme={null}
const data = {
  columns: [
    'service',
    'low',
    'lower',
    'middle',
    'upper',
    'high',
    'requests',
    'slow',
  ].map((key) => ({ key })),
  rows: [
    {
      service: 'Search',
      low: 120,
      lower: 126,
      middle: 131,
      upper: 135,
      high: 138,
      requests: 10,
      slow: null,
    },
    {
      service: 'Checkout',
      low: 80,
      lower: 130,
      middle: 190,
      upper: 250,
      high: 310,
      requests: 7,
      slow: null,
    },
    {
      service: 'Search',
      low: null,
      lower: null,
      middle: null,
      upper: null,
      high: null,
      requests: null,
      slow: 260,
    },
  ],
};

const spec = kit.pipe(
  kit.createSpec({ x: 'service' }),
  kit.geom.boxplot({
    stat: kit.stat.identity(),
    aes: {
      lowerWhisker: 'low',
      q1: 'lower',
      median: 'middle',
      q3: 'upper',
      upperWhisker: 'high',
      count: 'requests',
      outlier: 'slow',
    },
  }),
  kit.scale.x.discrete(),
  kit.scale.y.continuous()
);
```

The rest is optional. A row that holds only an `outlier` value is drawn as an outlier on its category's box. `mean`, `notchLower` and `notchUpper` are drawn when you map them, and `count` appears in the tooltip and sets the widths under `varwidth`. Grouping by `color` works as it does for raw values.

The default is `stat: kit.stat.boxplot()`. Naming it does the same as leaving `stat` out.

## Outliers, notches and means

Each option is a param, and each is off by default except the outliers:

```tsx theme={null}
kit.geom.boxplot({ params: { outliers: false } }); // hide them, and the room they held on the value axis
kit.geom.boxplot({ params: { notch: true } }); // notch each box at its median's rough 95% interval
kit.geom.boxplot({ params: { showMean: true } }); // mark each box's mean
```

Two boxes whose notches do not overlap have medians that differ, roughly at the 95% level. A notch wider than its box folds over the box's end, which says the median is uncertain.

## Box width

`width` sets how much of its band a category's boxes span, from `0` to `1`. It is `0.6` by default, and grouped boxes share it:

```tsx theme={null}
kit.geom.boxplot({ params: { width: 0.8 } });
```

`varwidth: true` makes each box's width follow the square root of how many values it summarises, against the layer's largest, so a box drawn from few values reads as thinner evidence.

## Groups

Map a group to `color` and each category draws one box per group, side by side across its band, coloured from the scale:

<GraphEmbed story="chart-types-boxplot--by-region" args={{ orientation: 'vertical' }} />

```tsx theme={null}
const spec = kit.pipe(
  kit.createSpec({ x: 'service', y: 'ms', color: 'region' }),
  kit.geom.boxplot(),
  kit.scale.x.discrete(),
  kit.scale.y.continuous()
);
```

`position: 'identity'` draws a category's boxes over one another instead. A boxplot does not stack.

## Horizontal

Add `kit.coord.flip()`. The categories move to the vertical axis and the boxes lie across.

<GraphEmbed story="chart-types-boxplot--response-times" args={{ orientation: 'horizontal', extent: '1.5' }} />

## Styling

The boxes are the observations, and the rest of each mark has a builder of its own. Every piece takes its group's colour from the scale unless an entry names one:

| Builder | Properties | Default |
| - | - | - |
| `kit.style.geom.boxplot()` | `fill`, `stroke`, `strokeWidth`, their alphas and more | Fill at 0.3 alpha, 1.5px border |
| `kit.style.geom.boxplot.median()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | 2px |
| `kit.style.geom.boxplot.whisker()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | 1.5px |
| `kit.style.geom.boxplot.cap()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | 1.5px, across half the box |
| `kit.style.geom.boxplot.outlier()` | `fill`, `fillAlpha`, `stroke`, `alpha`, `size`, `strokeWidth` | A hollow 6px ring |
| `kit.style.geom.boxplot.mean()` | `fill`, `fillAlpha`, `stroke`, `alpha`, `size`, `strokeWidth` | A solid 7px dot, ringed |

To drop the caps and keep the whiskers, set `kit.style.geom.boxplot.cap({ strokeWidth: 0 })`.

<GraphEmbed story="chart-types-boxplot--styled" />

```tsx theme={null}
const spec = kit.pipe(
  // …
  styles({
    defaults: [
      kit.style.geom.boxplot({ fillAlpha: 0.85, stroke: 'transparent' }),
      kit.style.geom.boxplot.median({ stroke: '#3a3833', strokeWidth: 2.5 }),
      kit.style.geom.boxplot.whisker({ stroke: '#898373', dashArray: [3, 3] }),
      kit.style.geom.boxplot.outlier({ fillAlpha: 1, size: 5 }),
    ],
  })
);
```

## The summary variables

A box's summary values are variables you can name in a `highlight`, an entry's `where` or an aesthetic's mapping, in the units of `y`. The package exports their names as `BOXPLOT_COLUMNS`:

| Variable | Value |
| - | - |
| `boxplotQ1` | The first quartile |
| `boxplotMedian` | The median |
| `boxplotQ3` | The third quartile |
| `boxplotLowerWhisker` | The lowest value the lower whisker reaches |
| `boxplotUpperWhisker` | The highest value the upper whisker reaches |
| `boxplotCount` | How many values the box summarises |
| `boxplotMean` | The mean, outliers included |
| `boxplotNotchLower` | The notch's lower end |
| `boxplotNotchUpper` | The notch's upper end |
| `boxplotOutlier` | An outlier's value, on the outlier alone |

A box keeps any other variable its rows all agree on, such as the category and the group, and leaves the rest empty. `y` is one of them, so a box has a `y` only when all its values are equal. An outlier keeps its own row, `y` included, so a highlight on `y` reaches the outliers it matches.

## Hover and highlight

Hovering a box emphasises it and its outliers, and the tooltip lists its upper whisker, third quartile, median, first quartile, lower whisker and count, then its mean when `showMean` is on. A press anywhere from one whisker's end to the other takes the box. In a grouped boxplot the box under the cursor answers alone. Hovering an outlier emphasises it alone, and the tooltip shows its value.

A highlight raises each matched box with its outliers. Match on the category, the group or a summary variable:

<GraphEmbed story="chart-types-boxplot--highlight" args={{ target: 'Checkout' }} />

```tsx theme={null}
const spec = kit.pipe(
  // …
  highlight({ variable: 'boxplotQ3', gt: 250 })
);
```

## Not yet supported

* **The editor's chart-type picker and the chart agent.** In editable mode the boxplot renders, hovers, selects and takes annotations on each box's median, but it is authored in code.
* **Other stats.** A layer `stat` that computes `y` (`count`, `mean`, `sum`) is rejected: a box needs a summary, from the `boxplot` stat or your own columns.
* **A continuous category axis or polar coordinates.** `x` is always read as categories, and the boxplot draws in cartesian coordinates, vertical or under `coord.flip()`.
* **Data labels.** `dataLabels` prints nothing on a box yet.
* **Raw points, jittered or as a violin.** Draw them as a separate point layer.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.