# BoxPlot

> Package `@umriss-ui/charts`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/charts/boxplot/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/charts/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/charts/llms-full.txt).

A distribution per position (a box plot, box-and-whisker): quartiles, median and whiskers, per service, per group or per hour. Reach for it to compare spreads, not single values.

```ts
import { useChart } from "@umriss-ui/charts";
```

Demo page: https://romanhaendler.github.io/umriss-ui/charts/boxplot/

Every number is the caller's: `median`, the two quartiles and where each whisker ends. The library computes none of them and names no quartile method or whisker rule, so a box aggregated in a database draws as well as one computed in the browser.

A box stands on the x axis as a bar does: a group is a category the axis reads from the row, and a time axis carries a box per hour ([ADR-0052](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0052-a-chart-reads-its-rows-as-they-come.md)). A missing median is a gap - no box is drawn there.

## Examples

### One box per service

Five numbers per row, computed wherever the caller likes. The x axis reads the service's name: each one is a category with a place and a tick of its own.

```tsx
import { Tooltip, useChart } from "@umriss-ui/charts";

interface ResponseTime {
  service: string;
  low: number;
  q1: number;
  median: number;
  q3: number;
  high: number;
}

/* Milliseconds per request over the last hour, aggregated by the database. */
const RESPONSE_TIMES: ResponseTime[] = [
  { service: "Checkout", low: 112, q1: 138, median: 149, q3: 161, high: 195 },
  { service: "Billing", low: 85, q1: 121, median: 140, q3: 173, high: 238 },
  { service: "Search", low: 140, q1: 152, median: 158, q3: 165, high: 181 },
];

export default function Minimal() {
  const { Chart, XAxis, YAxis, BoxPlot } = useChart(RESPONSE_TIMES);
  return (
    <Chart height={260} ariaLabel="Response time per service over the last hour">
      <XAxis value="service" />
      <YAxis tickFormat={(v) => `${v} ms`} />
      <BoxPlot
        name="Response time"
        median="median"
        lowerQuartile="q1"
        upperQuartile="q3"
        lowerWhisker="low"
        upperWhisker="high"
      />
      <Tooltip />
    </Chart>
  );
}
```

### A box per hour on a time axis

Checkout's median latency, every five minutes, gathered into one box per hour. The quartiles and the whisker rule are the caller's - here the last value within 1.5 IQR - and the library draws what it is given.

```tsx
import { Tooltip, useChart } from "@umriss-ui/charts";

/* Data from the operations world, written out here so the example runs on its own. */

/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}

const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();

const MINUTE = 60_000;

/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);

interface Service {
  id: string;
  name: string;
  team: string;
  /** 1 is customer-facing and pages at night; 3 waits for the morning. */
  tier: 1 | 2 | 3;
  /** The latency objective: the 95th percentile stays below this, in ms. */
  latencySlo: number;
  /** The availability promised for a month, in per cent. */
  availabilityTarget: number;
}

const SERVICES: readonly Service[] = [
  { id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
  { id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
  { id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
  { id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
  { id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
  { id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
  { id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
  { id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];

interface MetricPoint {
  t: number;
  /** Median and 95th percentile latency, in ms. */
  p50: number;
  p95: number;
  /** Failed requests, in per cent. */
  errorRate: number;
  /** Requests per minute. */
  requests: number;
}

/** Requests per minute at the busiest hour, by tier. */
const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;

/** A service's latency and error rate every five minutes of today, from
    midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
    climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): MetricPoint[] {
  const index = SERVICES.findIndex((one) => one.id === serviceId);
  const service = SERVICES[index];
  if (service === undefined) throw new Error(`No service "${serviceId}".`);
  const r = random(1000 + index * 37);
  const points: MetricPoint[] = [];
  let drift = 0;
  for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
    const hour = (t - at(17, 0)) / (60 * MINUTE);
    const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
    drift = 0.8 * drift + (r() - 0.5) * 0.08;
    const base = service.latencySlo * (0.45 + 0.2 * load + drift);
    const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
    const p95 = Math.round(base * surge * (1 + r() * 0.08));
    points.push({
      t,
      p50: Math.round(p95 * (0.42 + r() * 0.06)),
      p95,
      errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
      requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
    });
  }
  return points;
}

const HOUR = 3_600_000;

interface HourBox {
  hour: number;
  low: number;
  q1: number;
  median: number;
  q3: number;
  high: number;
}

/** The value at share p of the sorted values, between neighbours. */
function quantile(sorted: readonly number[], p: number): number {
  const at = (sorted.length - 1) * p;
  const lo = Math.floor(at);
  return (sorted[lo] as number) + ((sorted[Math.ceil(at)] as number) - (sorted[lo] as number)) * (at - lo);
}

function hourBoxes(): HourBox[] {
  const byHour = new Map<number, number[]>();
  for (const point of metrics("checkout")) {
    const hour = Math.floor(point.t / HOUR) * HOUR;
    byHour.set(hour, [...(byHour.get(hour) ?? []), point.p50]);
  }
  return [...byHour].map(([hour, values]) => {
    const sorted = values.sort((a, b) => a - b);
    const q1 = quantile(sorted, 0.25);
    const q3 = quantile(sorted, 0.75);
    const reach = 1.5 * (q3 - q1);
    const inside = sorted.filter((v) => v >= q1 - reach && v <= q3 + reach);
    return { hour, low: inside[0] as number, q1, median: quantile(sorted, 0.5), q3, high: inside[inside.length - 1] as number };
  });
}

const BOXES = hourBoxes();

export default function OverTime() {
  const { Chart, XAxis, YAxis, BoxPlot } = useChart(BOXES);
  return (
    <Chart height={280} ariaLabel="Checkout median latency per hour today">
      <XAxis value="hour" time />
      <YAxis tickFormat={(v) => `${v} ms`} />
      <BoxPlot
        name="Checkout p50"
        median="median"
        lowerQuartile="q1"
        upperQuartile="q3"
        lowerWhisker="low"
        upperWhisker="high"
      />
      <Tooltip />
    </Chart>
  );
}
```

### With outliers

`outliers` returns the values beyond the whiskers, a list per box. They are drawn with their box and read in its tooltip, the first five written out; one more than three IQR beyond its box is a ring.

```tsx
import { Tooltip, useChart } from "@umriss-ui/charts";

interface ResponseTime {
  service: string;
  low: number;
  q1: number;
  median: number;
  q3: number;
  high: number;
  outliers: number[];
}

/* Milliseconds per request over the last hour. The whiskers end at the last
   value within 1.5 IQR; the database returned everything beyond as outliers. */
const RESPONSE_TIMES: ResponseTime[] = [
  { service: "Checkout", low: 112, q1: 138, median: 149, q3: 161, high: 195, outliers: [224, 98] },
  { service: "Billing", low: 85, q1: 121, median: 140, q3: 173, high: 238, outliers: [281, 395] },
  { service: "Search", low: 140, q1: 152, median: 158, q3: 165, high: 181, outliers: [189, 214, 104] },
  { service: "Images", low: 99, q1: 126, median: 137, q3: 150, high: 186, outliers: [193, 198, 204, 210, 222, 235, 259, 71] },
];

export default function WithOutliers() {
  const { Chart, XAxis, YAxis, BoxPlot } = useChart(RESPONSE_TIMES);
  return (
    <Chart height={300} ariaLabel="Response time per service over the last hour, with outliers">
      <XAxis value="service" />
      <YAxis tickFormat={(v) => `${v} ms`} />
      <BoxPlot
        name="Response time"
        median="median"
        lowerQuartile="q1"
        upperQuartile="q3"
        lowerWhisker="low"
        upperWhisker="high"
        outliers="outliers"
      />
      <Tooltip />
    </Chart>
  );
}
```

### Mean and notches: do two medians differ?

Where two boxes' notches do not overlap, their medians differ. The bounds are yours - here the usual median ± 1.57 · IQR / √n. `mean` draws a small ×: beside the median it shows a skew. `count` is read, not drawn.

```tsx
import { DataTable, Legend, Tooltip, useChart } from "@umriss-ui/charts";

interface Batch {
  supplier: string;
  low: number;
  q1: number;
  median: number;
  q3: number;
  high: number;
  mean: number;
  n: number;
}

/* Breaking strength of tiles, in newtons, per clay supplier. */
const BATCHES: Batch[] = [
  { supplier: "Hollen", low: 1310, q1: 1395, median: 1430, q3: 1468, high: 1540, mean: 1431, n: 120 },
  { supplier: "Marsk", low: 1290, q1: 1380, median: 1418, q3: 1462, high: 1555, mean: 1426, n: 64 },
  { supplier: "Velde", low: 1240, q1: 1322, median: 1352, q3: 1398, high: 1490, mean: 1371, n: 96 },
];

const notch = (d: Batch) => (1.57 * (d.q3 - d.q1)) / Math.sqrt(d.n);

export default function MeanAndNotches() {
  const { Chart, XAxis, YAxis, BoxPlot } = useChart(BATCHES);
  return (
    <Chart height={300} ariaLabel="Breaking strength per clay supplier, with mean and notches">
      <XAxis value="supplier" />
      <YAxis tickFormat={(v) => `${v} N`} />
      <BoxPlot
        name="Breaking strength"
        median="median"
        lowerQuartile="q1"
        upperQuartile="q3"
        lowerWhisker="low"
        upperWhisker="high"
        mean="mean"
        notchLower={(d) => d.median - notch(d)}
        notchUpper={(d) => d.median + notch(d)}
        count="n"
        format={(v) => `${Math.round(v)} N`}
        boxWidth={0.5}
      />
      <Legend />
      <Tooltip />
      <DataTable />
    </Chart>
  );
}
```

### Before and after, side by side

Box series on one x axis stand beside each other, as bars do. A click in the legend hides one; it leaves the drawing and the y extent.

```tsx
import { Legend, Tooltip, useChart } from "@umriss-ui/charts";

interface Spread {
  service: string;
  low: number;
  q1: number;
  median: number;
  q3: number;
  high: number;
}

/* Milliseconds per request, the week before and the week after the release. */
const BEFORE: Spread[] = [
  { service: "Checkout", low: 112, q1: 138, median: 149, q3: 161, high: 195 },
  { service: "Billing", low: 85, q1: 121, median: 140, q3: 173, high: 238 },
  { service: "Search", low: 140, q1: 152, median: 158, q3: 165, high: 181 },
  { service: "Images", low: 99, q1: 126, median: 137, q3: 150, high: 186 },
];
const AFTER: Spread[] = [
  { service: "Checkout", low: 118, q1: 135, median: 143, q3: 152, high: 176 },
  { service: "Billing", low: 101, q1: 119, median: 128, q3: 139, high: 164 },
  { service: "Search", low: 142, q1: 151, median: 157, q3: 164, high: 179 },
  { service: "Images", low: 103, q1: 124, median: 135, q3: 146, high: 172 },
];

export default function Grouped() {
  const { Chart, XAxis, YAxis, BoxPlot } = useChart(BEFORE);
  return (
    <Chart height={280} ariaLabel="Response time per service before and after the release">
      <XAxis value="service" />
      <YAxis tickFormat={(v) => `${v} ms`} />
      {[
        { name: "Before", data: BEFORE },
        { name: "After", data: AFTER },
      ].map((one) => (
        <BoxPlot
          key={one.name}
          name={one.name}
          data={one.data}
          median="median"
          lowerQuartile="q1"
          upperQuartile="q3"
          lowerWhisker="low"
          upperWhisker="high"
        />
      ))}
      <Legend />
      <Tooltip />
    </Chart>
  );
}
```

### Against the objective

A `LimitLine` shows what was promised. Whether a box breaks it is your judgement, not the library's: this week's whiskers reach past the objective in every region, so its series carries `tone="alarm"`.

```tsx
import { Legend, LimitLine, Tooltip, useChart } from "@umriss-ui/charts";

interface Spread {
  region: string;
  low: number;
  q1: number;
  median: number;
  q3: number;
  high: number;
}

/* Checkout's 95th percentile per five minutes, in ms, per region and week. */
const LAST_WEEK: Spread[] = [
  { region: "Europe", low: 182, q1: 204, median: 216, q3: 229, high: 262 },
  { region: "Americas", low: 191, q1: 213, median: 226, q3: 240, high: 276 },
  { region: "Asia", low: 176, q1: 199, median: 211, q3: 224, high: 258 },
];
const THIS_WEEK: Spread[] = [
  { region: "Europe", low: 196, q1: 231, median: 249, q3: 268, high: 318 },
  { region: "Americas", low: 205, q1: 242, median: 262, q3: 283, high: 339 },
  { region: "Asia", low: 190, q1: 226, median: 244, q3: 263, high: 311 },
];

export default function AgainstTheObjective() {
  const { Chart, XAxis, YAxis, BoxPlot } = useChart(LAST_WEEK);
  return (
    <Chart height={280} ariaLabel="Checkout 95th percentile per region and week against its objective">
      <XAxis value="region" />
      <YAxis tickFormat={(v) => `${v} ms`} />
      <BoxPlot
        name="Last week"
        median="median"
        lowerQuartile="q1"
        upperQuartile="q3"
        lowerWhisker="low"
        upperWhisker="high"
      />
      <BoxPlot
        name="This week"
        data={THIS_WEEK}
        tone="alarm"
        median="median"
        lowerQuartile="q1"
        upperQuartile="q3"
        lowerWhisker="low"
        upperWhisker="high"
      />
      <LimitLine value={300} severity="alarm" label="Objective" />
      <Legend />
      <Tooltip />
    </Chart>
  );
}
```

### A box per shift, its medians as a line

The tiles measured in each of six shifts, one box each, and a `Line` through the medians to read the drift. The tooltip reads box and line together; `DataTable` lists every number as text.

```tsx
import { DataTable, Legend, Tooltip, useChart } from "@umriss-ui/charts";

/* Data from the plant world: tile length per shift in millimetres, as the
   quality database sends it - quartiles and median, the whiskers at the last
   length within 1.5 IQR. */
const BOXES = [
  { shift: "Mon early", low: 599.72, q1: 599.855, median: 599.97, q3: 600.1575, high: 600.4 },
  { shift: "Mon late", low: 599.53, q1: 599.875, median: 600.025, q3: 600.15, high: 600.38 },
  { shift: "Tue early", low: 599.6, q1: 599.88, median: 600.015, q3: 600.1925, high: 600.39 },
  { shift: "Tue late", low: 599.59, q1: 599.89, median: 599.975, q3: 600.1475, high: 600.36 },
  { shift: "Wed early", low: 599.61, q1: 599.8175, median: 600.01, q3: 600.1775, high: 600.45 },
  { shift: "Wed late", low: 599.68, q1: 599.8575, median: 600.035, q3: 600.16, high: 600.53 },
];

const mm = (v: number) => `${v.toFixed(2)} mm`;

export default function Detailed() {
  const { Chart, XAxis, YAxis, BoxPlot, Line } = useChart(BOXES);
  return (
    <Chart height={320} ariaLabel="Tile length per shift, with the medians as a line">
      <XAxis value="shift" label="Shift" />
      <YAxis tickFormat={mm} />
      <BoxPlot
        name="Tile length"
        median="median"
        lowerQuartile="q1"
        upperQuartile="q3"
        lowerWhisker="low"
        upperWhisker="high"
        boxWidth={0.5}
      />
      <Line value="median" name="Median" markers="always" />
      <Legend />
      <Tooltip />
      <DataTable />
    </Chart>
  );
}
```

## Keyboard

The keys of [Chart](https://romanhaendler.github.io/umriss-ui/charts/chart/#keyboard-chart) apply here.

## API

### `BoxPlotProps<T>`

| Prop | Type | Default | Description |
|---|---|---|---|
| `median` *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#type-Value)`<T>` | — | The line across the box; null/undefined/NaN/±Infinity means a gap - no box is drawn there, whatever the other numbers say.<br>Shown in: [One box per service](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#minimal), [A box per hour on a time axis](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#over-time), [With outliers](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#outliers) and 4 more |
| `lowerQuartile` *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#type-Value)`<T>` | — | The box's lower edge.<br>Shown in: [One box per service](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#minimal), [A box per hour on a time axis](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#over-time), [With outliers](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#outliers) and 4 more |
| `upperQuartile` *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#type-Value)`<T>` | — | The box's upper edge.<br>Shown in: [One box per service](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#minimal), [A box per hour on a time axis](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#over-time), [With outliers](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#outliers) and 4 more |
| `lowerWhisker` *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#type-Value)`<T>` | — | Where the lower whisker ends - the caller's rule, not the minimum.<br>Shown in: [One box per service](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#minimal), [A box per hour on a time axis](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#over-time), [With outliers](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#outliers) and 4 more |
| `upperWhisker` *required* | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#type-Value)`<T>` | — | Where the upper whisker ends - the caller's rule, not the maximum.<br>Shown in: [One box per service](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#minimal), [A box per hour on a time axis](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#over-time), [With outliers](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#outliers) and 4 more |
| `outliers` | [`ListValue`](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#type-ListValue)`<T>` | — | The values beyond the whiskers, per box: an array, empty or missing where there are none. Drawn with their box in its colour - one beyond three IQR of the box's own quartiles as a ring -, read in its tooltip and table row, never hit on their own ([ADR-0040](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0040-a-box-carries-its-outliers.md)).<br>Shown in: [With outliers](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#outliers) |
| `mean` | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#type-Value)`<T>` | — | The mean, drawn as a small ×: beside the median it shows a skew.<br>Shown in: [Mean and notches: do two medians differ?](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#mean-and-notches) |
| `notchLower` | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#type-Value)`<T>` | — | The notch's lower bound - usually of a confidence interval of the median. Both bounds or neither: one alone warns in DEV and is not drawn.<br>Shown in: [Mean and notches: do two medians differ?](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#mean-and-notches) |
| `notchUpper` | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#type-Value)`<T>` | — | The notch's upper bound; see `notchLower`.<br>Shown in: [Mean and notches: do two medians differ?](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#mean-and-notches) |
| `count` | [`Value`](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#type-Value)`<T>` | — | How many values stand behind the box; read in the tooltip and the table, not drawn.<br>Shown in: [Mean and notches: do two medians differ?](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#mean-and-notches) |
| `xAxisId` | `string` | `"x"` | Binding to an x axis. |
| `yAxisId` | `string` | `"y"` | Binding to a y axis. |
| `data` | `readonly T[]` | — | Series-own data; overrides the container data.<br>Shown in: [Before and after, side by side](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#grouped), [Against the objective](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#objective) |
| `name` | `string` | "Series n", after its place in the chart | The name in legend and tooltip. Without one a warning stands in DEV.<br>Shown in: [One box per service](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#minimal), [A box per hour on a time axis](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#over-time), [With outliers](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#outliers) and 4 more |
| `format` | `(value: number) => string` | the y axis' `tickFormat`, else the built-in number format | Every number of the box as the tooltip and the table write it.<br>Shown in: [Mean and notches: do two medians differ?](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#mean-and-notches) |
| `color` | `string` | the palette's `--uc-series-N` | Any CSS colour value. |
| `tone` | `"ok" \| "warning" \| "alarm"` | — | A role instead of a colour value; the theme resolves it. `color` beats it. Set by the caller: a box out of its specification is the caller's judgement, not the library's.<br>Shown in: [Against the objective](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#objective) |
| `boxWidth` | `number` | `0.8` | Width as a fraction of the step, as a bar's: boxes and bars on one x axis share this fraction and stand side by side.<br>Shown in: [Mean and notches: do two medians differ?](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#mean-and-notches), [A box per shift, its medians as a line](https://romanhaendler.github.io/umriss-ui/charts/boxplot/#detailed) |

### Types on this page

#### `Value<T>`

A value read from the row ([ADR-0048](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0048-charts-are-bound-to-their-rows-by-the-hook.md)): the name of a number field, compared
by its name, or a function, compared by its source text (`ValueFunction`).

```ts
type Value<T> = NumberField<T> | ValueFunction<T>;
```

#### `ListValue<T>`

A list read from the row: the name of a list field or a function.

```ts
type ListValue<T> = ListField<T> | ListFunction<T>;
```

#### `NumberField<T>`

The fields of a row whose value is a number, or absent.

```ts
type NumberField<T> = NoInfer<
  {
    [K in keyof T & string]-?: T[K] extends number | null | undefined ? K : never;
  }[keyof T & string]
>;
```

#### `ValueFunction<T>`

A **value** computed from the row - the function form of `Value`.
Compared by its source text, not its identity - an inline function is new
on every render. Known limit: one that reads a
changed closure variable under the same text is not run again; pass a new
data reference then. A native or bound function is compared by identity.

```ts
type ValueFunction<T> = (d: T, index: number) => number | null | undefined;
```

#### `ListField<T>`

The fields of a row whose value is a list of numbers, or absent.

```ts
type ListField<T> = NoInfer<
  {
    [K in keyof T & string]-?: T[K] extends readonly number[] | null | undefined ? K : never;
  }[keyof T & string]
>;
```

#### `ListFunction<T>`

A list computed from the row - a box's outliers ([ADR-0040](https://github.com/romanhaendler/umriss-ui/blob/main/docs/adr/0040-a-box-carries-its-outliers.md)), the
function form of `ListValue`. null/undefined or an
empty array is none; a value that is not finite is left out.

```ts
type ListFunction<T> = (d: T, index: number) => readonly number[] | null | undefined;
```

## Known limits

- No horizontal boxes, no violin or jittered points, no box width by count: a density is smoothing, a jitter a picture that does not repeat.

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
