Charts

Cricket Wagon Wheel

A playable cricket wagon wheel in hand-rolled SVG — every scoring shot drawn from the striker's stumps out to where the ball finished, coloured and shaped by the runs it brought, with the eight region totals riding the rope and a ball-by-ball transport clocked in overs.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import type {
  ChartCricketWagonWheelData,
  ChartCricketWagonWheelHand,
  ChartCricketWagonWheelRuns,
  ChartCricketWagonWheelShot,
} from "./chart-cricket-wagon-wheel.contract"

export interface ChartCricketWagonWheelProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue" | "onChange" | "title">,

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chart-cricket-wagon-wheel.json

Prompt

Build a React + TypeScript + Tailwind "ChartCricketWagonWheel" chart — a
playable cricket wagon wheel in hand-rolled SVG (no chart library, no animation
library), with zod.

Contract
- A zod schema is the single source of truth:
  { status: "loading" | "empty" | "error" | "ready";
    batter: { name: string; hand: "left" | "right" };
    innings?: string;
    shots: { over: number;      // 1-based over number
             ball: number;      // 1..6, the legal delivery within that over
             runs: 1 | 2 | 3 | 4 | 6;
             angle: number;     // degrees clockwise from straight
             distance: number;  // metres from the striker's stumps
             shot?: string }[] }.
- Props = z.infer of the schema plus title?, value?, defaultValue?,
  onValueChange?, onRetry?, emptyState? and className. No hand-written
  parallel interface.
- A wagon wheel plots SCORING shots only: a dot ball is simply absent, extras
  belong on the scorecard, and five-run overthrows are out of scope. `runs` is
  a literal union so every branch that colours or shapes a mark is exhaustive.
- Angles are measured at the striker's stumps: 0 is dead straight past the
  bowler, 90 square on the clockwise side, 180 straight behind the keeper.
  Sector edges are half-open, so a ball hit dead straight books to the first
  sector.
- Handedness renames the ground, it never moves a ball: for a right-hander the
  clockwise half is the leg side, for a left-hander the anticlockwise half. Mirror
  the region NAMES, draw the shot where it was actually hit.

Behavior
- Four first-class branches in one bg-card panel: loading (a skeleton that
  mirrors the ready silhouette — stat line, the ground itself, readout,
  transport, ball rail — so the card keeps its height), empty (a faint ground
  plus copy, reused with a stated reason when a ready payload has nothing
  drawable), error (message plus "Try again" only when onRetry exists), ready.
- Ingest repairs instead of trusting, and counts every repair in a visible
  line: shots with no readable over, ball, line, length or run value are
  dropped; a delivery numbered outside the over is snapped back into 1..6; a
  distance past the rope on its own line is clamped onto the rope; angles are
  normalised (lossless); the feed is sorted by (over - 1) * 6 + ball and says so
  when it arrived out of order. A wagon wheel that quietly loses or moves a
  shot is claiming an innings that was never played.
- ONE playhead — how many recorded scoring shots have been played — is the only
  playback state, and it supports the full triple: `value` (controlled),
  `defaultValue` (uncontrolled) and `onValueChange`, which fires once per ball
  from the transport, the scrubber, the ball rail and the loop alike. Everything
  on the card derives from it: runs, boundary share, leg/off split, region
  totals, the clock, the scrubber thumb and the sr-only table.
- NO autoplay, and no wall clock anywhere in the first render path. The
  uncontrolled playhead starts parked at the last ball, so a screenshot, an SSR
  paint or a docs thumbnail shows the complete wheel; pressing play from there
  rewinds to 0.0 overs. A card paused at ball N renders identically every time.
- Playback is one requestAnimationFrame loop that advances by the frame's
  timestamp delta times the selected speed (1x / 2x / 4x), against a per-ball
  dwell derived from how far the ball travelled (about 12 ms per metre, clamped
  420–1100 ms) — so a six hangs and a push for one is over quickly. Leftover
  milliseconds carry across balls in a ref, so 2x is exactly twice 1x. Cancel
  the loop on pause, on every seek, on unmount, and pause playback on
  visibilitychange when the document hides.
- The transport is real UI, all keyboard operable with focus-visible rings:
  back-to-start, step back one ball, play/pause (aria-pressed with a
  state-specific aria-label), step forward one ball, a labelled native
  <input type="range"> scrubber with one step per ball and an aria-valuetext
  that speaks the overs, and a 1x/2x/4x speed radiogroup. Buttons at the ends of
  the innings use aria-disabled plus a handler guard, NEVER the native disabled
  attribute — a control that stops taking focus vanishes under the finger using
  it.
- The clock reads in cricket's own units: overs bowled as over.ball, so 12
  completed overs and 3 balls is "12.3 ov". Frame 0 is "0.0 ov", the start of
  the innings.
- A ball-by-ball rail under the transport is the keyboard equivalent of the
  lines on the ground: one button per recorded shot showing its runs, roving
  tabindex (arrows / Home / End move focus, Enter jumps the replay there),
  aria-current on the playhead ball. Every ball keeps its button whatever the
  playhead is doing, so a rewind can never drop focus onto <body>.
- prefers-reduced-motion (via matchMedia, subscribed with cleanup —
  useSyncExternalStore fits): playback still works, but the ball in flight is
  never drawn, so each shot simply appears whole when its dwell is up. CSS
  transitions carry motion-reduce: variants.
- Hover on a line and focus on a rail button share ONE readout row under the
  wheel — runs, region, stroke name, overs, metres — which shows the playhead's
  own ball when nothing is being inspected, so the card never grows or shrinks.
- A sr-only aria-live region announces on pause, on every step or seek and on
  reaching the last ball; it is emptied while the loop runs, so playback never
  spams a screen reader.

Rendering & styling
- Ground geometry in metres, from the laws and never from pixel guesses: a 70 m
  boundary measured from the CENTRE of the 22-yard (20.12 m) pitch, the 30-yard
  (27.43 m) fielding circle as two semicircles about the middle stumps joined by
  straight lines, a 10-foot-wide pitch with popping creases 4 feet in front of
  each set of stumps. Because the origin is the striker's stumps and the ground
  centre is half a pitch up-field, the straight boundary is ~80 m and the square
  boundary ~69 m — the reason a straight six is the biggest hit on the ground.
- Export the maths as pure functions beside the component so a test can print
  the same numbers the picture is made of: angle normalisation, rope distance
  along a line (t = halfPitch·cos θ + √(R² − halfPitch²·sin²θ)), polar-to-screen,
  region index for a hand, the rope arc between two lines, overs formatting,
  ingest and region totals.
- Runs carry three channels, never colour alone: var(--chart-1..5) by run value,
  a stroke width that grows with the runs, and an end mark whose SHAPE is the
  category — disc for a run (radius grows 1 to 3), square for a four, hexagon
  for a six. The legend repeats shape plus colour plus a count.
- Region totals ride the rope: an arc per sector whose stroke opacity is that
  region's share of the runs so far (foreground ink, so it never competes with
  the run scale), with the region's cricket name and its running total set
  outside it, text-anchored by the sector's own direction so nothing overflows.
- The outfield is a bg-muted wash inside a muted-foreground rope, the fielding
  circle and creases are border strokes, the pitch is background, and the
  striker's stumps are a muted-foreground hub every line radiates from.
- The figure is role="img" with a one-sentence aria-label; its marks are pointer
  affordances only (a transparent fat stroke over each line, starting a quarter
  of the way out so the crowded hub is not one big target), and the accessible
  equivalents are the ball rail plus an sr-only summary and a table of every
  ball with overs, runs, region, distance, stroke and whether the replay has
  reached it.
- Colour only ever comes from semantic tokens (border, muted, card, background,
  popover, primary for the scrubber accent, destructive for the error branch,
  var(--chart-*) for runs); ramps are opacity or color-mix on those tokens; no
  hex, rgb() or oklch() anywhere, ground markings included. cn() merges
  className and the root spreads remaining props with data-status.

Customization levers
- Ground size: BOUNDARY_R is the only number that decides how big the ground
  reads — drop it to 62 m for a small city ground and the straight/square
  asymmetry follows automatically. PITCH_LENGTH, INNER_R and CREASE are the
  laws; leave them alone unless you are drawing a different format.
- Tempo: MS_PER_METRE plus the 420–1100 ms clamp set how an innings feels, and
  SPEEDS is just an array — swap in 0.5x for coaching study, or drop the
  radiogroup entirely for an embedded card.
- Encoding: RUN_TOKEN, RUN_WIDTH and RUN_MARK are three independent maps.
  Re-key RUN_TOKEN to team colours, flatten RUN_WIDTH for a hairline wheel, or
  give twos and threes their own shapes if your audience needs them apart.
- Regions: WAGON_REGIONS is an eight-name array driving sectors, labels, splits
  and the mirror. Six or twelve sectors work as long as 360 divides evenly;
  rename them for a broadcast partner's vocabulary.
- Blocks: the stat header, the boundary/side line, the readout row, the rail,
  the legend and the ground footnote are independent siblings — drop any of
  them for a compact card, or keep only the wheel plus the transport.
- Playhead: pass `value` and own time from the parent (jump to the first six,
  drive it from a video player's currentTime); pass `defaultValue` for a card
  that opens paused mid-innings; pass neither for the complete wheel.

Concepts

  • Wagon wheel — the cricket picture this chart is: every run off the bat drawn as a line from the striker's stumps to where the ball finished, so the shape of an innings (all through cover, or everything square on the leg side) is legible before a single number is read.
  • The clock is over.ball, not seconds — cricket measures time in deliveries, so the transport reads "12.3 ov": twelve completed overs and three balls. The playhead is a ball index the component is handed, never a wall clock, which is what makes a paused card render identically on every machine and every reload.
  • Parked complete, never autoplaying — the first render shows the whole wheel with its region totals, so a screenshot or an SSR paint is already the full innings; play from there means rewind and watch. Controlled and uncontrolled playback are the same code path, so a parent can own the playhead without losing the transport.
  • Reduced motion changes the tween, not the feature — with motion off the ball in flight is never drawn: each shot appears whole when its dwell is up. Play, step, scrub, speed and the announcements all keep working, and no mark depends on an in-flight animation to become readable.
  • The rope is not a circle around the batter — the boundary is 70 m from the centre of the pitch, so from the striker it is about 80 m straight and 69 m square. Clamping shots to that curve, rather than to a constant radius, is what stops a lofted straight drive being drawn shorter than a square cut of the same carry.
  • Repairs are counted out loud — unreadable shots dropped, deliveries snapped back into a six-ball over, carries past the rope drawn at it, out-of-order feeds re-sorted: each count is printed under the chart, because a silent repair turns a scorecard into fiction.

On This Page