Draw Path
A four-state seeded route through a draw — one round per rail station with the projected opponent's rating and head-to-head, faint branches for whoever could arrive instead, Elo win chances compounded into a survival bar, and a tab panel for each round's whole field.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-draw-path.jsonPrompt
Build a React + TypeScript + Tailwind "ChartDrawPath" card — one seeded player's
route through a knockout draw, drawn as a vertical rail of rounds with a
hand-rolled SVG spine behind HTML rows (no chart library), plus zod for the
contract.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready";
player: { name, seed?, rating };
event?: string;
rounds: { round: string;
opponents: { id, label, seed?, rating, h2hWins, h2hLosses,
probability }[] }[] }.
- Component props = z.infer of the schema, plus title?, defaultRound?,
onRetry?, emptyState? and the div's own attributes. No hand-written
parallel interface.
- `probability` is any non-negative weight — a probability, a percentage or a
count of simulation runs — because it is normalised inside its round.
- `h2hWins` / `h2hLosses` are always the SEED's side of the head-to-head — the
meetings the seed has won off this opponent and lost to them — never the
opponent's, since every readout prints them in that order as "h2h wins–losses"
and sums them into the route record.
- `rating` is Elo-scale for both the player and every opponent; ids are unique
across the WHOLE route, since the pools that can reach you in two different
rounds are disjoint. Refine both, and refine that a ready payload carries at
least one round with at least one opponent.
Behavior
- Derive, never assert: a pure buildDrawPathModel(player, rounds) turns the
payload into per-round candidates (share, printed percent, win chance),
field rating, round win chance and the survival chain. Everything the card
prints is read back out of that model.
- The field rating is Σ arrival odds × rating — the expected rating of whoever
arrives, NOT the plain mean of the ratings listed under it — so it has to be
labelled as such wherever it is printed: "field averages N by arrival odds"
on the tile and in the spoken sentence, "expected opponent N" in the panel
header. An unqualified "the field averages N" reads as a bug against the
very rows the card shows.
- Arrival odds: normalise each round's weights (exactly one opponent arrives),
then apportion whole percents by LARGEST REMAINDER — floor, then hand the
leftover points to the biggest fractional parts, ties to the bigger weight
and then to the earlier opponent. Every round must print percents that add
to exactly 100. A round with no usable weights is read as evenly likely and
says so.
- Win chance: Elo expectation 1 / (1 + 10^((opponent − player) / 400)). The
round's chance is the odds-weighted mean over the field; survival is the
running product down the route, so a bar can only shrink.
- Verdicts computed from the model, never hardcoded: route survival, the
toughest round (lowest round win chance, earliest breaking ties), and the
head-to-head across the PROJECTED opponents only, reported as "N of M
projected opponents, K never met".
- Opponents without a usable rating are dropped and counted; a round left with
no field is skipped and counted; both are reported under the chart rather
than silently swallowed. A ready payload with nothing drawable falls through
to the empty branch WITH that reason.
- The four states are first-class branches of one card: loading keeps the
ready silhouette (tiles, narrowing bars, panel), empty draws the bare rail,
error offers retry only when onRetry exists, ready draws the route.
- Rounds are a vertical tablist with automatic activation: click, or Arrow
up/down (left/right too), Home and End; selection follows focus and focus
follows selection; roving tabIndex; one tabpanel showing the selected
round's whole field.
Rendering & styling
- Semantic tokens only: bg-card / border / bg-muted / text-muted-foreground /
text-destructive / bg-primary for the "projected" chip, var(--chart-1) for
survival and var(--chart-2) for arrival odds. No hex, no palette classes.
- One number, one mark: a row's bar width, its fill opacity and the percent
printed beside it are all the same survival value; the pale tail beside it is
exactly what that round takes out of the route. The panel's arrival bars are
drawn at the PRINTED percent, so bar and label cannot disagree.
- The rail SVG is absolutely positioned in a fixed-width gutter and sized from
the same row-height constant the rows use, so nothing is measured at runtime
— no observers, no timers, nothing to tear down. Spine thickness follows
survival; each alternate gets a faint fork whose curve opacity and dot radius
follow its arrival odds.
- Accessibility: each tab's aria-label is a full sentence (round, projected
opponent, rating, odds, head-to-head, round win chance, survival), the rail
SVG is aria-hidden decoration, and an sr-only table lists every opponent of
every round. focus-visible rings on tabs and the retry button; transitions
carry motion-reduce:transition-none and the skeleton motion-reduce:
animate-none.
- cn() merges className; remaining props spread on the root, which also carries
data-slot and data-status.
Customization levers
- Rail density: ROW_H is the single row-height constant the SVG and the rows
share — drop it to ~64 for a compact rail (and trim the alternates line), or
raise it for a roomier one.
- How many branches are drawn: FORKS caps the fork stubs per round; the rest
collapse into "+N more". Set it to 0 for a bare spine.
- Which numbers ride the row: the three-percent line (arrive / if you meet /
alive) is the densest reading — drop "if you meet" for a broadcast card, or
drop the alternates line entirely and let the panel carry the field.
- Difficulty model: swap eloWinProbability for your own (a surface-adjusted
Elo, a logistic on ranking points, a market price) and every derived number,
bar and verdict follows — nothing else knows where the chance came from.
- Palette: survival rides var(--chart-1) with an opacity ramp, arrival odds
var(--chart-2); re-map either, or give each round its own token if the route
should read as five series rather than one.
- Opening round: defaultRound picks the tab the card opens on; a preview page
usually wants the first round, a mid-tournament card the next one to be
played.Concepts
- Route as a chain, not a list — each round's survival is the previous round's survival times this round's odds-weighted win chance, so the rail can only ever narrow and the "still alive" number beside a bar is the number the bar was drawn from.
- Arrival odds are one whole — exactly one opponent walks out of a round, so the field is apportioned together by largest remainder; every round's printed percents add to exactly 100 instead of the 101 that per-opponent rounding produces.
- Alternative branches stay visible — the opponent who could arrive instead is drawn as a faint fork whose curve opacity and dot size follow its own arrival odds, so a 13% outsider never looks like a 66% favourite, and the full field is one tab away.
- Difficulty is derived, never asserted — win chances come from ratings through one swappable Elo function, so the "toughest round" verdict and the head-to-head tile are consequences of the data rather than editorial labels someone typed.
- Projected-only head-to-head — the record tile counts the likeliest arrival of each round and reports how many of them have actually been played, because averaging over every candidate would credit the seed with matches against players who will mostly never arrive.
- Measurement-free geometry — the rail SVG derives its height from the same row-height constant the rows use, so the spine, nodes and forks land on their rows with no ResizeObserver, no timer and nothing to clean up on unmount.
Tournament Schedule · Order of Play
A four-state order-of-play sheet — courts as columns against a vertical time axis, every match a block as long as its estimate, floating "not before" slots, live progress, and the double bookings and rest gaps the sheet implies.
Serve +1 Patterns
A four-state tennis serve +1 flow in hand-rolled SVG — serve target, then the +1 wing and direction, then won or lost, joined by bands sized by points and coloured by win rate against a benchmark, with the most-used and the most-effective pattern named.