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

A service answers at 124.5 ms for half its requests and 1,661.13 ms for the slowest 1%. PercentileLadder puts both ticks
on one zero-anchored track, where the distance between them is the reading. Graduated height and accent make the
**tail** read strongest, because the tail is usually the SLO question. The origin is never cropped: cropping it would
distort every gap. A log transform for long tails is never silent — `scale="log"` applies only when every sample is
strictly positive, and an in-chart `log` tag renders whenever it does.

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

## Try it

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

<PercentileLadder
  data={latencies}
/>
```

## When to use it

Use it for latency SLOs in a sentence, the tail per endpoint in tables, and payment-size spread. For the odds of an
outcome use QuantileDots; for the full distribution shape use MicroBox.

## Sizing

**stricter SLO percentiles**

```tsx
<PercentileLadder data={latencies} ps={[50, 95, 99.9]} />
```

**log for long tails**

```tsx
// the transform is never silent — a log tag renders
<PercentileLadder data={latencies} scale="log" />
```

## 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
// was never 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}
/>
```

When every sample is identical the ladder collapses to a single tick instead of stacking overlapping ones. A single
value at or below zero makes `scale="log"` fall back to linear, and no `log` tag renders, because the transform was not
applied. Below 56 px wide the tick labels drop out rather than overlap.

## Four homes

**In a sentence**

```tsx
<p>
  Request latency{" "}
  <span className="mc-inline">
    <PercentileLadder data={latencies} summary={false} />
  </span>{" "}
  — p99 at 2.1 s, long tail visible.
</p>
```

**In a table cell**

```tsx
<td>
  <PercentileLadder data={latencies} />
</td>
```

**In a KPI card**

```tsx
<div className="kpi">
  <span className="figure">2.1s</span>
  <span className="unit">latency</span>
  <PercentileLadder data={latencies} />
</div>
```

**In a tab header**

```tsx
<button className="tab">
  checkout <PercentileLadder data={latencies} />
</button>
```

## 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, press `Escape`, or
press outside the chart. 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).
