Charts

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import type {
  ChartPadelWallPlayClass,
  ChartPadelWallPlayData,
  PadelWallClass,
} from "./chart-padel-wall-play.contract"

export interface ChartPadelWallPlayProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
    ChartPadelWallPlayData {

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chart-padel-wall-play.json

Prompt

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.

On This Page