Charts

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import type {
  ChartPointLeverageData,
  ChartPointLeveragePoint,
  ChartPointLeverageSide,
} from "./chart-point-leverage.contract"

export interface ChartPointLeverageProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
    ChartPointLeverageData {

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chart-point-leverage.json

Prompt

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 noisewinProbBefore has 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.

On This Page