Charts

Front Wall Targets

A four-state squash wall map in hand-rolled SVG — the front wall drawn face-on from WSF markings (hatched tin, service line, out line), every strike a dot whose colour AND shape carry the shot type, tin and out strikes hollow, with largest-remainder type shares and a repair guard that keeps heights honest to their calls.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import type {
  ChartFrontWallResult,
  ChartFrontWallShotType,
  ChartFrontWallTargetsData,
  ChartFrontWallTargetsShot,
} from "./chart-front-wall-targets.contract"

export interface ChartFrontWallTargetsProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">,

Installation

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

Prompt

Build a React + TypeScript + Tailwind "ChartFrontWallTargets" chart — a squash
front-wall strike map in hand-rolled SVG (no chart library), with zod.

Contract
- A zod schema is the single source of truth:
  { status: "loading" | "empty" | "error" | "ready";
    shots: { x: 0..6.4 (metres across the wall, 0 = left wall as the striker
             faces it); y: 0..5.2 (metres above the floor);
             type: "drive" | "drop" | "boast" | "kill";
             result: "in" | "tin" | "out" }[];
    meta: { player: string; context?: string } }.
- Props = z.infer of the schema plus title?, onRetry?, emptyState? and the
  native div props, spread on the root. forwardRef. No hand-written parallel
  interface.
- Shots stay in struck order — the keyboard walk follows it.

Wall geometry (from the WSF specifications, not from pixels)
- The singles court is 6.40 m wide, so the wall face runs 0–6.40 across.
- The tin (board) tops out 0.43 m above the floor: a ball at or below it is
  down. Draw it as a hatched token band from the floor to that edge, with the
  edge itself stronger than the paint — it is the line the attacking game is
  played against.
- The service line sits at 1.78 m (only a serve must strike above it) and the
  front-wall out line at 4.57 m (at or above is out); both are labelled
  court-paint lines. Shade the strip above the out line as a muted out zone.
- The frame tops out at 5.20 m — a drawing device so out strikes have
  somewhere to sit, not a rule number. Map metres through one scale constant.

Behavior
- Four first-class branches in one bg-card panel: loading (the ready layout
  piece for piece — stat line, pulsing wall beside the type-panel rows,
  footnote bar), empty (faint wall + copy, reused when a ready payload has
  nothing placeable), error (message + a "Try again" button only when onRetry
  exists), ready.
- Ingest is a pure exported function: clamp a coordinate a centimetre over an
  edge (a recorded strike happened), drop a row whose type, call or coordinate
  is unreadable, and print the dropped count — never swallow rows silently.
- The consistency guard: the result call names a zone on the wall. "tin"
  belongs at or below 0.43 m, "out" at or above 4.57 m, "in" between the two.
  A height that contradicts its call is repaired — moved a stated inset just
  inside the called zone — with the repair counted under the wall and noted on
  that mark's tooltip. The call is the verdict; the height is only the
  measurement; disagreement is reported, never silently resolved.
- The side column counts strikes per type with shares apportioned together by
  largest remainder so the four print exactly 100, plus a tin/out error line
  (tin + out over all strikes, as a standalone share).
- Hover raises a per-dot tooltip (type, call, height and width in metres, plus
  the repair note) over a generous transparent hit circle; the highlight dims
  the other dots, with the pointer-leave reset guarded so crossing dots never
  blinks. Keyboard: the svg is one role="listbox" tab stop, arrow keys walk
  the strikes in struck order, aria-activedescendant follows, and only
  :focus-visible arms the cursor so a mouse click doesn't pin the tooltip.
  Tooltips flip below the dot near the frame top and slide near the side
  walls, anchored in percentages of the viewBox. The tooltip is aria-hidden —
  each dot's aria-label already says it all — and an sr-only summary + table
  repeat every strike.

Rendering & styling
- Semantic tokens only. Identity is double-encoded and never re-assigned:
  drive = var(--chart-1) circle, drop = var(--chart-2) triangle, boast =
  var(--chart-4) diamond, kill = var(--chart-3) square — shapes sized to
  matched visual weight, so the wall survives colour-vision deficiency and
  greyscale. In strikes wear the type fill under a 2px var(--card) ring so
  overlapping dots stay separable; tin and out strikes invert it — no fill,
  the type token as a 2px stroke, sitting hollow in their zones.
- Wall paint: outline stroke-border; service and out lines
  stroke-muted-foreground; the tin band fill-muted under a rotated token-line
  pattern with a stroke-foreground/70 top edge; the floor
  stroke-muted-foreground and heavier; the out strip fill-muted at reduced
  opacity. Line labels fill-muted-foreground with paintOrder="stroke" and a
  var(--card) halo so a dot can never make them unreadable.
- Legend chips carry shape + colour + count per type, plus a filled/hollow
  pair for in versus tin-or-out. Type meter bars ride neutral tokens
  (bg-muted track, bg-muted-foreground fill) so they never compete with the
  four type hues.
- Panel rounded-xl border bg-card p-4, numbers tabular-nums, cn() merges
  className, root spreads remaining props and carries data-status; highlight
  transitions respect prefers-reduced-motion; the wall group is aria-hidden.

Customization levers
- REPAIR_INSET (0.1 m) is how far inside its called zone a contradicting
  strike is drawn; widen it on a dense wall or shrink it toward the line.
- DOT_R / HIT_R trade dot presence against touch-target size; the halo ring
  and dim opacity are single constants.
- TYPE_INK / the glyph switch are the only home of the shot identity — add a
  fifth shot (lob, volley drop) by extending the enum, the glyph switch and
  the legend, and ingest, panel and table follow.
- WALL_TOP only sets the drawn frame above the out line; raise it if your
  feed records strikes high on the wall.
- Drop the side column for a wall-only card, or the wall for a shares-only
  strip — each reads from the same ingest.
- Sport swap: the same skeleton reads any "strikes on a vertical target face"
  story — a goal mouth, a climbing wall — by replacing the four line
  constants and the zone names the calls map to.

Concepts

  • The wall is the ruler — the tin's top edge at 0.43 m, the service line at 1.78 m and the out line at 4.57 m come from the WSF book and are drawn to scale, so "shaved over the tin" and "floated out" are read against the court's own lines rather than against a guessed grid. Nothing on the wall is a pixel offset.
  • Call and measurement are different fields — the result is the referee's verdict and it names a zone; the height is only where the tracker saw the ball. When they contradict, the mark is repaired just inside the called zone, the repair is counted under the wall and noted on the tooltip — a hollow "tin" mark floating mid-wall would assert two facts at once, and picking one silently would hide that the feed disagrees with itself.
  • Dual encoding is the identity — every shot type owns one colour and one shape (circle, triangle, diamond, square), assigned in a fixed order that never re-shuffles when a type is absent, so a reader who cannot separate the four chart hues still reads the wall.
  • Hollow means it didn't count — an in strike is the type token under a 2px card ring; a tin or out strike is the same shape hollow, sitting in the hatched band or the shaded strip its call names, so the error pattern pops before any number is read.
  • Shares that add to 100 — the four type shares are apportioned together by largest remainder, ties resolved by count then by fixed order, so the side column, the summary and the table always print the same split and it always sums to exactly 100; the tin/out error line is a standalone share on top.
  • One tab stop, every strike reachable — the svg is a single listbox; arrow keys walk the strikes in struck order with aria-activedescendant following the visible halo, and only :focus-visible arms the cursor so a mouse click never pins a tooltip nobody asked for.

On This Page