Padel Wall Play
A four-state padel wall-play ledger — one bar per wall-interaction class (direct, back wall, side wall, double wall) sized by shot count on a shared scale, split into points won and lost, with usage shares, win rates and a bounce glyph per class.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-padel-wall-play.jsonPrompt
Build a React + TypeScript + Tailwind "ChartPadelWallPlay" card — a padel
wall-interaction ledger drawn as one won/lost split bar per class on a shared
count scale, in hand-rolled CSS + tiny inline SVG glyphs (no chart library)
with zod for the contract.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready";
classes: { key: "direct" | "backWall" | "sideWall" | "doubleWall",
label?, shots >= 0, won >= 0 }[];
meta: { player, context? } }
with refines that won <= shots per entry and that keys are unique.
Component props = z.infer of that schema plus title?, barHeight? (10-28,
default 16), onRetry?, emptyState? and className. No parallel interface.
- Semantics, stated in the schema's JSDoc: the key classifies what the ball
did off the walls immediately BEFORE this player hit it; `won` counts how
many of those points the player went on to win. Keys are identities with a
fixed row order and a fixed glyph — a class the feed does not send is a
zero row, never a missing one. `label` only overrides the printed name.
Behavior
- Four first-class branches inside one bg-card panel:
- loading: an aria-hidden skeleton keeping the row silhouette (glyph, label
block, bar, count column), plus one sr-only role=status line.
- empty: a plan-view padel court glyph (FIP 20 x 10 m, service lines 6.95 m
from the net, centre lines running to the BACK wall, perimeter drawn
heavier because in padel the boundary is a wall the ball plays off) and
copy naming what will appear.
- error: a destructive message and a "Try again" button only when onRetry
is supplied.
- ready: legend, four rows in fixed order, a totals row, a footnote.
- Layout maths live in one exported pure function returning the four rows
(shots, won, lost, largest-remainder sharePct, winPct or null), pooled
totals, and the shared scale maximum (the largest class).
- Bar length is the SHOT COUNT on one shared scale — how often the situation
happened — never the win rate; the split inside the bar carries how those
points went. Conflating them would make a rare-but-strong class look like a
pillar of the game.
- Usage shares partition a whole, so they go through largest-remainder
rounding and sum to exactly 100. Win rates are plain rounds.
- Counts that arrive negative, fractional or non-finite are repaired to whole
counts and the repair is REPORTED under the chart, never swallowed.
- Hover or keyboard focus on a row shows a tooltip (shots, share, won, lost,
win %); rows are tabIndex=0 with a full aria-label and a focus-visible
ring, and hover/focus are tracked separately.
Rendering & styling
- Per row, left to right: an aria-hidden bounce glyph (size-9, currentColor
strokes — walls fainter and thicker at strokeOpacity 0.45, the ball's path
solid with a hand-placed arrowhead: direct = plain arrow, backWall = arrow
off a vertical line behind, sideWall = arrow off a top line, doubleWall =
two bounces off a corner); a fixed 6rem label block (name over muted
share %); the bar; a fixed right column printing "n · win %" in
tabular-nums with the rate muted.
- The bar: won first in solid var(--chart-2), lost continuing outward in
color-mix(var(--chart-5) 45%, transparent), parted by a 2px flex gap that
shows the card surface. Outer corners rounded-xs; a segment present on its
own rounds both ends. Segments floor at 2px so one shot never vanishes; a
zero class keeps a hairline muted tick at the axis.
- Totals row under a border-t, aligned to the same columns via a size-9
spacer, printing pooled shots and the pooled win rate.
- Semantic tokens only: bg-card, border, text-muted-foreground, bg-popover
for the tooltip, var(--chart-2), var(--chart-5) through color-mix. cn()
merges className, remaining props spread on the root, skeleton pulses carry
motion-reduce:animate-none, glyphs and bar internals are aria-hidden.
Customization levers
- Class vocabulary: labels (per-entry `label`) rename what prints without
touching keys, order or glyphs — e.g. Spanish "salida de pared" for the
back-wall row; the glyphs live in one WallGlyph switch if a sport variant
needs different diagrams.
- Palette: won reads var(--chart-2) and lost var(--chart-5) at 45% mix — remap
to any two chart tokens, but keep lost visibly quieter than won so the bar
reads "earned vs leaked" at a glance, and keep the 2px surface gap.
- Density: barHeight (10-28) plus the gap-3 list spacing set the card height;
drop the glyph column (and its totals-row spacer) for a text-only variant.
- Scale: the shared maximum is the largest class; to compare two players'
cards side by side, lift scaleMax out of the layout and pass one shared
value to both.
- Sample honesty: the footnote prints counts beside rates on purpose; add a
minimum-sample tag per row (like a "too few to read" note) if your feeds
produce single-digit classes routinely.Concepts
- How often vs how it went — bar length is the shot count and only the split inside is the outcome. Encoding win rate into length would let a nine-shot class out-shout a ninety-six-shot one; keeping them orthogonal is the whole read of the card.
- Shared count scale — all four bars are drawn against the largest class, so reaches compare across rows. A per-row scale would make every bar full-width and turn the count axis into decoration.
- Wall classes as identities — the four keys have a fixed order and a fixed bounce glyph each; a class the feed doesn't send is a zero row with a tick, never a missing row, so two match cards always have the same silhouette and an absent situation is visibly absent.
- Glyphs teach the vocabulary — the little diagrams (arrow, arrow off the back glass, off the side glass, two bounces) mean a reader who has never seen padel stats can parse the rows without a glossary; they are decoration, so they ride currentColor and stay aria-hidden.
- No rate without its sample — 67% off the double wall sounds like a weapon until the card shows it was nine shots; counts print beside every rate and the footnote says why, which is the difference between analytics and anecdotes.
- Largest-remainder shares — the four usage shares partition one whole, so they round by largest remainder and sum to exactly 100 in every state instead of drifting to 99 or 101.
T Dominance
A four-state squash T-control card — one mirrored bar per game split at each player's share of rally time on the T around a 50% centre rule, the score under every bar, a match row on top, and the games that defied the T called out.
Head-to-Head History
A four-state rivalry ledger — one row per meeting on a center-spine timeline with the score and a surface chip on the winner's side, a running tally with per-surface splits, and the closing streak named in the header.