Skip to content
microcharts
ReferenceFrontierWinProbWorm

WinProbWorm

WinProbWorm plots win probability on a fixed 0–100 axis and marks every lead change.

encodes
y = win probability on a fixed 0–100 axis, split at the 50% line
precision
high
nodes
≤ 5 + 1 per lead change

A model has the home side at 98% after three lead changes. WinProbWorm plots that series on a fixed 0–100 axis that is never truncated, so a 2-point edge always looks like a 2-point edge. 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".

WinProbWorm
interactive · 6.02 kB · static · 3.67 kB

Install

Import & usereact/win-prob-worm
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"/>
Needs package + stylesheetSet up with AI

Try it

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

Variants

wire-to-wire vs nailbiter

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

markSwing and label

Edge cases

locale
a dead heat
single point

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

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: 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

PropTypeNotes
data*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").
markSwingbooleanMark the biggest momentum swing (default true; seat-gated).
animatebooleaninteractiveOpt-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.

Related charts