# WinProbWorm (/docs/charts/win-prob-worm)

WinProbWorm answers "who's winning, and when did the lead flip?". It plots one win-probability series on a **fixed 0–100
axis that is never truncated** — the honesty rule for a probability — and splits the worm at the 50% line: while your
side leads the line reads accent, when it trails it reads neutral, and a dot marks each lead change. Because a
win-probability curve is a modelled read, the summary always says so, "per the supplied model".

```tsx
<WinProbWorm
  data={[50, 48, 45, 52, 60, 58, 42, 38, 55, 68, 82, 90, 88, 94, 98]}
  sides={["home", "away"]}
  title="Win probability"
  width={220}
  height={28}
/>
```

## Install

```tsx
import { WinProbWorm } from "@microcharts/react/win-prob-worm";

<WinProbWorm
  data={[50, 48, 45, 52, 60, 58, 42, 38, 55, 68, 82, 90, 88, 94, 98]}
  sides={["home", "away"]}
  title="Win probability"
/>
```

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** — live win or election probability where the lead flips, a modelled forecast whose crossings are the
  story, and any 0–100 probability you must not truncate.
- **Avoid for** — a raw score or margin (Sparkline), or a value that isn't a bounded 0–100 probability.


## Variants

```tsx
<WinProbWorm data={[50, 58, 66, 74, 80, 86, 90, 93, 96, 98, 99]} sides={["home", "away"]} />
<WinProbWorm data={[50, 53, 49, 52, 48, 51, 47, 50, 46, 49, 45, 48, 52]} sides={["home", "away"]} />
```

At a taller size the biggest single momentum swing gets a hair connector and a signed delta; drop it with
`markSwing={false}`, and drop the endpoint probability with `label="none"`.

```tsx
<WinProbWorm
  data={[50, 48, 45, 52, 60, 58, 42, 38, 55, 68, 82, 90, 88, 94, 98]}
  sides={["home", "away"]}
  markSwing={false}
  label="none"
/>
```

## Edge cases

```tsx
<WinProbWorm
  data={[50, 55.5, 61.2, 58.4, 63.5]}
  sides={["Ja", "Nein"]}
  format={{ minimumFractionDigits: 1 }}
  locale="de-DE"
/>
```

```tsx
// a constant 50 reads as tied — the worm sits on the midline
<WinProbWorm data={[50, 50, 50, 50, 50]} sides={["home", "away"]} />
```

```tsx
<WinProbWorm data={[72]} sides={["home", "away"]} />
```

With a `format` and `locale`, the endpoint label and the summary format their numbers in that locale's own decimal mark
— `[50, 55.5, 61.2, 58.4, 63.5]` in `de-DE` ends at **"63,5%"**. Values outside 0–100 are clamped to the axis and
dev-warn — a probability cannot exceed 100. A constant series has no crossing and no swing: an all-50 series reads
"even… throughout", any other constant reads "…holds X% throughout". A `null` breaks the worm into disconnected runs the
way a gap should; a single point has no line to draw, so it renders just the endpoint dot and its label; `data={[]}`
renders the frame with the "no data" summary.


## Why this default

The fixed 0–100 axis is the whole point: a win probability that truncated its own scale would exaggerate a lead, so the
axis never moves and a 2-point edge always looks like a 2-point edge. The 50% split does the reasoning for the reader —
accent while your side leads, neutral while it trails, a dot at each flip — so "who's ahead, and did it ever change?" is
answered before the second glance. The endpoint carries the current number, and the biggest momentum swing is marked
only when there's room for it. The worm is a modelled read, never a fact, so its accessible name is framed "per the
supplied model" and the chart never editorializes the forecast.

## Accessibility

The accessible name states the current leader, their probability, the number of lead changes, and the biggest momentum
swing — **"Per the supplied model, home leads at 98%; 3 lead changes, biggest swing +17 at point 8."** — or, for a
constant series, "Per the supplied model, even at 50% throughout." The interactive entry roves the points with ←/→
(Home/End jump to the ends), each announcing the leader and probability at that point, with a live readout chip.

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[]` | A single win-probability series, clamped to 0–100. |
| `sides` | `[string, string]` | Names for the two sides — [>50, <50]. Default ["A", "B"]. |
| `label` | `"last" \| "none"` | Print the current leader's probability at the endpoint (default "last"). |
| `markSwing` | `boolean` | Mark the biggest momentum swing (default true; seat-gated). |
| `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).
