Charts

T Battle Replay

A four-state playable squash T-control replay — two tracked dots move on a metre-true WSF floor plan, a control ring on the T tints toward whoever holds it, controlled time accrues into a mirrored share bar, and rally-end ticks mark the scrubber with a running score.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import type {
  ChartTBattleReplayData,
  ChartTBattleReplayRally,
  ChartTBattleReplaySample,
} from "./chart-t-battle-replay.contract"

export interface ChartTBattleReplayProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
    ChartTBattleReplayData {

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chart-t-battle-replay.json

Prompt

Build a React + TypeScript + Tailwind "ChartTBattleReplay" card — a squash
T-control battle replayed over a scrub clock on a metre-true floor plan, in
hand-rolled SVG (no chart library) with zod for the contract.

Contract
- A zod schema is the single source of truth:
  { status: "loading" | "empty" | "error" | "ready";
    players: { a, b };
    samples: { t: seconds ascending, ax, ay, bx, by: court metres }[];
    rallies?: { endT: seconds, winner: "a" | "b" }[];
    controlRadius?: metres (default 1.2) }
  Component props = z.infer of that schema plus title?, onRetry?, emptyState?
  and className. No parallel interface.
- Coordinates are the WSF floor: x 0..6.4 across the court, y 0..9.75 from the
  front wall. Player a is var(--chart-1) and b var(--chart-2) everywhere —
  dots, trails, share bar, rally ticks; the chart never reorders them.

Behavior
- Four first-class branches in one bg-card panel: loading keeps the ready
  silhouette (legend, court block, bar, transport); empty draws the court
  glyph with the dashed T-zone ring, waiting; error offers "Try again" only
  when onRetry is supplied; ready is the replay.
- Ingest is one exported pure function: coordinates off the floor are clamped
  onto it, samples with an unreadable or non-ascending clock are dropped,
  rally marks outside the tracked window are pinned to its edge — every
  repair is counted and REPORTED under the chart, never swallowed.
- Control by exclusion, per sample interval, decided at the interval's start
  sample: exactly one player inside controlRadius of the T (the junction of
  the short line and the half-court line) owns that interval's duration; both
  inside or neither inside is neutral time. Prefix sums over the intervals
  make "controlled seconds up to any scrub time" a binary-search lookup, so
  scrubbing anywhere is O(log n) per frame.
- One timeline value `time` (seconds) drives everything: interpolated dot
  positions, ~2 s fading trails, the tint of the control ring, the mirrored
  share bar, per-player held seconds, a neutral-share readout, and the
  running rally score.
- Playback: NO autoplay — ready parks at the END of the clip (final shares
  visible, dots on their last positions) for a deterministic first paint;
  Play rewinds to 0. requestAnimationFrame with timestamp deltas (never
  setInterval), multiplied by a 1× / 2× / 4× speed radiogroup; the frame is
  cancelled on pause and unmount. The play button carries aria-pressed and an
  aria-label; a labeled native range scrubber over the clock seeks at any
  moment, playing or paused.
- prefers-reduced-motion (matchMedia, listener cleaned up on unmount):
  positions step sample-to-sample instead of tweening, and every CSS
  transition carries motion-reduce:transition-none. Playback still works —
  the replay steps rather than glides.
- A throttled polite live region announces rally ends while playing (at most
  one message per 1.5 s) and always announces pause and end-of-replay with
  the shares and score at that moment.
- Hover or keyboard focus on the T-zone circle (the whole radius is the hit
  target) shows who holds the T and both shares; each rally tick is a real
  button with a generous 28px hit — hover/focus tells rally number, winner
  and score after, click jumps the clock to that rally's end. A screen-reader
  summary sentence and an sr-only rally table carry the exact figures.

Rendering & styling
- The court is drawn to WSF measure in a metre viewBox, front wall at the
  top: 9.75 × 6.4 m floor, short line 5.44 m from the front wall, half-court
  line from the short line to the back wall, two 1.6 m service boxes in the
  short-line corners, the T emphasised as the one solid dot.
- The control ring is faint border ink, dashed, tinting toward the current
  controller's token at low fill opacity; player dots take var(--chart-1) /
  var(--chart-2) with 2px card-colored rings via vector-effect:
  non-scaling-stroke; trails are ~10 line segments fading to transparent.
- The share bar mirrors chart-t-dominance: the track is B's fill, A's segment
  is painted over it from the left, a 1px color-mix(var(--foreground) 55%,
  transparent) centre rule marks 50%, and the printed pair goes through
  largest-remainder rounding so the ends always sum to exactly 100.
- Semantic tokens only: bg-card, border, text-muted-foreground, bg-popover
  for tooltips, var(--chart-1) / var(--chart-2), accent-color var(--primary)
  on the range input. All text wears text tokens — color chips carry
  identity, never colored text. cn() merges className, remaining props spread
  on the root, decorative SVG groups are aria-hidden, every interactive
  element has a focus-visible ring.

Customization levers
- controlRadius is the analysis: tighten to 0.8 m for a strict "on the T"
  reading or widen for doubles; the ring, the legend label and the footnote
  all quote it, so nothing else needs editing.
- Rally marks are optional — drop `rallies` and the scrubber, score line and
  sr table quietly disappear while the control battle still replays.
- Playback grammar: SPEEDS ([1, 2, 4]) and the 0.1 s scrub step are one-line
  constants; TRAIL_S / TRAIL_SEGMENTS set how much recent movement the trails
  remember (0 segments removes them for a minimal dot view).
- Density: the court column is max-w-60 — widen it for a hero replay or drop
  the share-bar block for a compact floor-only variant; the transport row
  survives both.
- Palette: sides read var(--chart-1) / var(--chart-2); remap to any two chart
  tokens but keep both solid — the centre rule and the ring tint have to
  survive on top of each.
- Sport: the court constants are six numbers — swap them (and the T point)
  for racquetball or a padel half to replay any zone-control battle from the
  same contract.

Concepts

  • Control by exclusion — the T is owned only while exactly one player stands inside the radius; both inside is a contest and neither is open court, and both fall into a named neutral share instead of being silently split. The rule is decided per sample interval, so the accounting is exactly as good as the tracking cadence and no better.
  • Prefix-sum accrual — controlled seconds are pre-summed at every sample, so any scrub time costs one binary search plus a partial interval. The replay is derived from the clock, never accumulated by the animation — scrubbing backwards, jumping to a tick and replaying all land on identical numbers.
  • Parked at the end, never autoplaying — the first paint is the story's final frame: shares settled, dots on their last positions. Motion is the viewer's choice; Play rewinds to zero, and prefers-reduced-motion swaps the glide for honest sample-to-sample steps without losing the feature.
  • Timestamp-delta playback — the clock advances by real elapsed milliseconds times the chosen speed, not by frame counts, so a throttled tab or a 120 Hz screen replays the same game in the same seconds; the frame is cancelled on pause and unmount.
  • Largest-remainder pair — the two printed ends of the share bar partition controlled time, so they are rounded as a pair that sums to exactly 100; rounding each alone drifts to 99/101 and the bar stops being trusted.
  • Territory vs scoreboard — rally ticks colored by winner sit under the control clock, so the moment control flips before the score follows (or a point falls against the T) is visible as a mismatch between the bar and the tick row — the same coaching gap chart-t-dominance shows per game, replayed here as it happened.

On This Page