Point Leverage
Every point of a match plotted by how much of the win probability it could swing, with a draggable high-leverage line and per-player conversion above it.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-point-leverage.jsonPrompt
Build a React + TypeScript + Tailwind "ChartPointLeverage" card with zod and
hand-rolled SVG (no chart library — the plot is one scatter, one reference line
and two axes).
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready";
players: { a: { name }, b: { name } };
context?: string;
points: { t: number; // minutes of match time
gameScore: string; // SERVER FIRST: "30-40", "40-40",
// "A-40"; a tiebreak is numeric
setScore: string; // SERVER FIRST games: "4-5"
server: "a" | "b";
winner: "a" | "b";
setIndex?: number; // 0-based; drives set dividers
winProbBefore?: number; // ALWAYS player a's chance of
winProbIfServerWins?: number; // winning the match
winProbIfReturnerWins?: number }[];
highLeverageAt?: number; // default 0.1
servePointWin?: number } // default 0.62, fallback table only
- The three win probabilities travel together or not at all (refine).
- Component props = z.infer of the schema plus title?, height?, labelCount?,
onRetry? and className. No hand-written parallel interface.
Behavior
- Leverage of a point = the gap between the two conditional win probabilities:
what player a would be worth if the server took the point and if the returner
did. Kept as ONE integer in tenths of a win-probability point, and that
integer places the dot, ranks it, decides which side of the line it lands on
and is what gets printed — so a dot can never sit under a line it is counted
above.
- Two pricing paths, and the card says which paid:
- model: all three probabilities present, in 0..1, and `before` between the
two conditionals (half a percentage point of slack for rounded feeds).
A reading that fails that test is contradictory, not merely noisy: count it
as repaired and fall through.
- table: derived from the score alone. Importance chains, so it is a product
of three exact differences — the point inside its game, the game inside its
set, the set inside the match. Solve the game and the tiebreak as races to
4 and to 7 by two (the deuce tail in closed form, p^2 / (p^2 + q^2)), the
set as a recursion over games where the serve alternates and 6-6 is a coin
flip, and weight a deciding set twice a non-deciding one. Everything comes
from one number: the share of points won on serve.
- Points that carry neither a usable model reading nor a readable score are
dropped and counted; the counts are printed, never swallowed.
- The high-leverage line is the card's one interaction: role="slider",
aria-orientation="vertical", dragged with a pointer (pointer capture,
touch-action none) or moved with arrows / PageUp / PageDown / Home / End.
Every count on the card follows it; keyboard moves speak through a live
region, pointer moves do not.
- Its handle is a pill in the axis gutter carrying the value in axis units — a
pill parked on the plot would hide the very dots the line exists to sort —
and the y tick it covers is dropped rather than stacked underneath it. The
hit area reaches into the gutter so the handle itself is draggable.
- Ready with zero drawable points renders the empty branch, not an empty frame.
- Percentages that partition a whole (each player's share of the high-leverage
points, the high/low split of the match) are apportioned by LARGEST
REMAINDER, so they total exactly 100 at every position of the line.
- The N biggest points of the whole match (not of what the line catches) are
ringed, labelled in place with their score when the plot is wide enough, and
listed with clock, set, games, who won and the realised probability move.
- Scores are SERVER FIRST, and the serve changes hands every game — so a score
printed bare reads backwards half the time. Every score on a visible surface
carries the server's initial in front of it, the row under it names the
server in full, and the hidden table's game and set columns say "server
first" in their headers.
Rendering & styling
- Only semantic tokens: var(--chart-1) for player a and var(--chart-2) for
player b everywhere (dot, legend, conversion bar); stroke-border gridlines
and set dividers; stroke-foreground for the line, its handle and the rings;
fill-primary/10 dark:fill-primary/20 for the band above the line; bg-card,
text-muted-foreground, text-destructive, stroke-ring for focus.
- Under the line a dot keeps its player's token and loses its fill — shape,
never opacity, carries "this one didn't matter much", because a tinted dot
vanishes into a dark card.
- In-place labels paint a card-coloured outline under themselves
(paintOrder="stroke") and are placed in three lanes — beside the dot, then
below it, then above it, in that order of preference; a label that would
overhang the frame or collide in all three is dropped, and the prose only
claims labels when some were drawn.
- The plot measures itself with a ResizeObserver (disconnected on unmount and
before any rebuild) and scales as a whole below a floor width.
- A visually hidden table repeats the biggest high-leverage points, capped at a
row budget and captioned with how many of them it is showing ("24 of 32
points at or above the line") rather than with the cap alone; the svg is
role="group", never role="img", so the slider inside it keeps its voice.
Customization levers
- The line: `highLeverageAt` sets where it starts; drop the slider entirely for
a static report card and keep the counts.
- Density: `height` (150-360) and `labelCount` (0-5). Zero labels leaves a
clean scatter; five suits a wide broadcast card.
- Sport: the fallback table is tennis scoring. For badminton or pickleball
(rally scoring, games to 21 or 11), swap the two race targets and the set
recursion — or feed the model probabilities and never touch the table.
- Field strength: `servePointWin` is the one knob the whole table hangs on.
0.62 is a men's tour field, 0.55 a women's or club field.
- Blocks: the conversion panel, the biggest-points list and the footnote are
independent sections — keep the plot alone for a dashboard tile.
- Palette: a and b are fixed to chart-1 and chart-2 so two matches read the
same way; re-map both in one place if a team palette is needed.Concepts
- Leverage, not outcome — a point is measured by what it could have changed, so a 30-40 in a deciding set outranks a rally that won a game already won. The dot's height is the swing between the two futures, and the colour is which of them happened.
- The line is the reader's, not the author's — "high leverage" is a judgement call, so it is a draggable slider rather than a constant. Every number under the plot recomputes from wherever it is left, which is exactly why the shares are apportioned by largest remainder: a split that prints 101% in one position of the line would be a defect in all of them.
- One integer, four jobs — the same tenths-of-a-point value places the mark, ranks it, classifies it against the line and prints in the list. Rounding for display separately from measuring is how a dot ends up drawn under a line it is counted above.
- Provenance travels with the number — a point priced by the supplied model and one priced by the fallback table are both plotted, but the card counts them apart and says so, because mixing a measured probability with a lookup silently claims more than it knows.
- Contradiction is not noise —
winProbBeforehas to sit between the two conditionals; it is their average. A reading that does not is repaired to the table and reported, rather than averaged into something nobody computed. - Stable annotation under a moving line — the biggest points are ranked over the whole match, so dragging the line never reshuffles the rings and labels; only the counts move.
Partner Chemistry
A four-state doubles pairing grid — one roster on both axes, the lower triangle carrying each partnership's win rate over its match count, samples under the bar hatched out of every read, each player's own average on the diagonal to compare against, and a load strip whose widths are largest-remainder shares of the season.
Chat Sidebar
A conversation-history sidebar — calendar-day groups cut in the display time zone, in-place rename with rollback, delete confirmed by title, pinning, local search with its own no-match state, and real loading/empty/error branches.