# PercentileLadder (/docs/charts/percentile-ladder)

PercentileLadder answers "what does the tail look like — not just the median?". Ticks at the chosen percentiles sit on a
zero-anchored track; graduated height and accent make the **tail** read strongest. Tick distances carry the story, so
the origin is never cropped — and a log transform (for long latency tails) is never silent: an in-chart `log` tag
renders when it applies.

```tsx
import { PercentileLadder } from "@microcharts/react/percentile-ladder";

<PercentileLadder
  data={Array.from({ length: 200 }, (_, i) =>
    i < 130
      ? 90 + (i % 50)
      : i < 180
        ? 150 + ((i * 7) % 320)
        : i < 196
          ? 480 + ((i * 11) % 900)
          : 1500 + ((i * 13) % 800),
  )}
  title="Request latency"
/>
```

## Install

```tsx
import { PercentileLadder } from "@microcharts/react/percentile-ladder";

<PercentileLadder
  data={Array.from({ length: 200 }, (_, i) =>
    i < 130
      ? 90 + (i % 50)
      : i < 180
        ? 150 + ((i * 7) % 320)
        : i < 196
          ? 480 + ((i * 11) % 900)
          : 1500 + ((i * 13) % 800),
  )}
  format={{ style: "unit", unit: "millisecond" }}
  title="Request latency"
/>
```

Setup (package + stylesheet): [Quickstart](/docs/quickstart#set-up-with-an-ai-agent) or paste [`/agent-setup.md`](/agent-setup.md) into your agent.


## When to use it

- **Good for** — latency SLOs in a sentence, the tail per endpoint in tables, payment-size spread.
- **Avoid for** — the odds of an outcome (QuantileDots) or the full distribution shape (MicroBox).


## Variants

```tsx
const latencies = Array.from({ length: 200 }, (_, i) =>
  i < 130
    ? 90 + (i % 50)
    : i < 180
      ? 150 + ((i * 7) % 320)
      : i < 196
        ? 480 + ((i * 11) % 900)
        : 1500 + ((i * 13) % 800),
);

<PercentileLadder data={latencies} scale="log" />
<PercentileLadder data={latencies} ps={[50, 95, 99.9]} label="values" />
<PercentileLadder data={latencies} marks="dot" />
// value labels and the summary format through Intl — "1.661 ms", not "1,661 ms"
<PercentileLadder
  data={latencies}
  label="values"
  format={{ style: "unit", unit: "millisecond", maximumFractionDigits: 0 }}
  locale="de-DE"
/>
```

## Edge cases

```tsx
// every percentile lands on the same value, so the ladder renders one
// tick instead of three overlapping ones
<PercentileLadder data={[7, 7, 7, 7]} />
```

```tsx
// a single ≤ 0 sample would make a log axis a lie, so scale="log" falls
// back to linear — no in-chart "log" tag renders, because the transform
// wasn't actually applied
<PercentileLadder data={[0, 10, 100, 1000]} scale="log" />
```

```tsx
// under the documented minimum width, tick labels drop out cleanly
// rather than overlapping
<PercentileLadder
  data={[10, 20, 30, 90, 900]}
  width={40}
/>
```


## Why this default

The default linear track is zero-anchored — tick **distances** carry the story, so cropping the origin would distort
every gap between percentiles. Height and accent grow toward the tail because the tail, not the median, is usually the
SLO question. A log transform for long tails is never silent: it only applies when every sample is strictly positive,
and an in-chart `log` tag renders whenever it does, so the axis is never misread as linear.

## Accessibility

The accessible name states every marked percentile and the tail's multiple of the median — **"p50 124.5, p90 488.1, p99
1,661.13 — the slowest 1% take 13.3× the median."** The interactive entry roves ticks with ←/→ (Home/End jump to the
ends), announcing each tick's value and its multiple of the median (**"p99: 1,661.13 — 13.3× the median."**).

The interactive entry follows the shared [interaction contract](/docs/accessibility#one-interaction-contract):
arrow keys rove between units on both axes, `Home` and `End` jump to the ends, and a click, tap, `Enter` or
`Space` selects a unit — pinning its readout so it survives blur, until you select it again or press `Escape`.
On touch, a tap pins and a drag scrubs.

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `data` (required) | `number[]` | Raw sample; quantiles are derived. |
| `ps` | `number[]` | Percentiles to mark (default [50, 90, 99], 2–4). |
| `scale` | `"linear" \| "log"` | Log for long tails (falls back on any value ≤ 0; renders a log tag). |
| `label` | `"ps" \| "values" \| "both" \| "none"` | What the tick labels state. |
| `marks` | `"tick" \| "dot"` | Tick marks (default) or dot marks — dots read calmer over dense text. |
| `animate` | `boolean` | (interactive) Opt-in entrance motion when the chart mounts client-side — add `import "@microcharts/react/motion"` once. Inert on the server, on hydrated server HTML, and under `prefers-reduced-motion`. |

Plus the shared grammar — `data`, `domain`, `color`, `title`, `summary`, `format` — and the layout props (`width`, `height`, `className`, `style`) that every chart accepts. Interactive entries also share `animate` and `live`, and — wherever a chart has more than one navigable unit — `onActive`, `onSelect`, `selectedIndex` and `defaultSelectedIndex`; and — wherever the chart shows a hover value — `readout`. See [the shared grammar](/docs/quickstart#the-shared-grammar).
