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

A model has the home side at 98% after three lead changes. WinProbWorm plots that series on the **full 0–100 axis**, so
a 2-point edge always looks like a 2-point edge. The default frame is never truncated; `domain` narrows it when you are
reading a game that never left the middle. It 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. A win-probability curve is a modelled read,
so 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.

## Try it

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

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

## When to use it

Use it for live win or election probability where the lead flips, for a modelled forecast whose crossings are the point,
and for any 0–100 probability you must not truncate. For a raw score or margin use Sparkline, and for a value that isn't
a bounded 0–100 probability use another chart.

## Sizing

**a wire-to-wire lead never crosses 50**

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

**a nailbiter hovers around the line**

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

## 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"]} />
```

The endpoint carries the current number. At a taller size the biggest single momentum swing also gets a hair connector
and a signed delta; drop that 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, since 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, and `data={[]}`
renders the frame with the "no data" summary.

## Four homes

**In a sentence**

```tsx
<p>
  Win probability{" "}
  <span className="mc-inline">
    <WinProbWorm data={winProb} sides={["home", "away"]} summary={false} />
  </span>{" "}
  — home team leads 62%, swing at Q3.
</p>
```

**In a table cell**

```tsx
<td>
  <WinProbWorm data={winProb} sides={["home", "away"]} label="none" />
</td>
```

**In a KPI card**

```tsx
<div className="kpi">
  <span className="figure">98%</span>
  <span className="unit">win probability</span>
  <WinProbWorm data={winProb} sides={["home", "away"]} />
</div>
```

**In a tab header**

```tsx
<button className="tab">
  Finals G1 <WinProbWorm data={winProb} sides={["home", "away"]} label="none" />
</button>
```

## 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."** A constant series
reads "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, press `Escape`, or
press outside the chart. 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. |
| `domain` | `[number, number]` | Probability extent (default [0, 100] — the full range). |
| `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).
