Form Guide
A four-state team form guide — ranked rows of W/D/L result chips, a rolling points-per-game sparkline and season totals, driven by one contract.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-form-guide.jsonPrompt
Build a React + TypeScript + Tailwind "ChartFormGuide" sports widget with zod.
No chart library — the sparkline is a hand-rolled inline SVG polyline.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready"; title: string;
teams: { label: string; results: ("W" | "D" | "L")[]; points >= 0 }[] }.
- results are ordered oldest → newest; points is the season total and the
ranking key; label doubles as the row key, so it must be unique.
- Component props = z.infer of the schema, plus lastN?: number (how many
most-recent results become chips, clamped 1–10, default 5),
onRetry?: () => void, and remaining div props spread onto the root.
No hand-written parallel interface.
Behavior
- Four first-class branches in one bg-card panel:
- loading: aria-hidden skeleton rows that mirror the real row shape (rank
block, name bar, five chip squares, sparkline bar, points bar) plus a
sr-only role="status" line; pulse gets motion-reduce:animate-none.
- empty: three dashed placeholder chips + "No results yet" copy; a ready
payload with zero teams renders this same branch.
- error: role="alert" message + a "Try again" button rendered only when
onRetry exists, with a focus-visible ring.
- ready: meta line (team count, rounds played), ranked <ol>, caption.
- Ranking: sort by points desc, tie-break on win count then label; the rank
number is the render index + 1.
- Chips: the last `lastN` entries of results, oldest → newest; the newest
chip wears an inset outline in its own tone; a team with fewer results than
the window pads the left with dashed "unplayed" slots so every row keeps
the same chip grid.
- Sparkline: rolling points per game — cumulative points (W=3, D=1, L=0)
divided by matches played after every round — plotted on a FIXED 0–3
y-domain so slopes compare across teams; a single-match series degrades to
an end dot, an empty one to an em-dash of the same width.
Rendering & styling
- Tones by token only: W = var(--chart-1), D = var(--muted-foreground),
L = var(--destructive); one map feeds the chip text color, the chip tint
(color-mix of the same tone at 15% over transparent, so light and dark both
work) and the spoken word. Sparkline stroke is var(--chart-2).
- Rows are an <ol>; the chips group and the sparkline are role="img" with
spoken sentences ("Last 5 matches, oldest first: won, drew, …" /
"Rolling points per game, now 2.20"); individual chips are aria-hidden.
- Panel: rounded-xl border bg-card p-6; rank/points are tabular-nums; the
team name truncates so fixed columns never overflow; cn() merges className.
Customization levers
- Scoring: WIN_POINTS / DRAW_POINTS constants (3/1 football default) — set
2/1 for hockey-style leagues; the sparkline domain follows WIN_POINTS.
- Window: lastN chips (default 5); widen to 10 for a half-season view or
drop to 3 for a compact mobile card.
- Row density: py-2.5 rows with size-5 chips — shrink both for a sidebar
widget, or drop the sparkline column entirely for very narrow cards.
- Tones: remap the W/D/L → token map in one place (e.g. W to a brand token);
keep three distinct tokens or the chips stop being scannable.
- Ranking: swap the tie-break chain (goal difference upstream, alphabetical)
without touching the render — it is a single comparator.Concepts
- Form window vs season — the chips answer "how are they doing lately" over a sliding last-N window, while the sparkline and points column carry the whole season; the two never disagree because both derive from the same
resultsarray. - Rolling average as trend — points per game is cumulative points divided by matches played, recomputed after every round, so early volatility flattens into a season-shape curve instead of a noisy win/loss zigzag.
- Fixed shared domain — every sparkline is plotted on the same 0–3 scale rather than each team's own min/max; a line sitting high genuinely means a stronger run, so rows can be compared at a glance.
- One tone map, three consumers — the W/D/L → token map colors the chip text, tints its background via color-mix and picks the spoken word, so an outcome can never look green in one place and read "lost" in another.
- Honest padding — a team with fewer matches than the window gets dashed "unplayed" slots instead of a shorter chip strip, keeping the grid aligned and making a short season visible rather than hidden.
Race Splits
A four-state race-splits spaghetti chart: every athlete's cumulative gap to the split leader against a zero baseline, lines named at their ends, lead changes and DNFs disclosed, every split keyboard-reachable.
Player Percentile Pizza
A scouting percentile pizza in hand-rolled SVG — one wedge per stat, radius as cohort percentile rank, wedges grouped and coloured by skill category, with guide rings at 25/50/75.