Charts

Cricket Manhattan

A four-state cricket Manhattan chart in hand-rolled SVG — runs off every over as a skyline, wickets as drops under the axis, a cumulative worm on a second scale, two innings on one shared over axis, and a real transport that plays the match forward over by over.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import type {
  ChartCricketManhattanData,
  ChartCricketManhattanInnings,
} from "./chart-cricket-manhattan.contract"

/**
 * Which reading leads:
 * - `manhattan` — the skyline of runs off each over leads, the cumulative worm
 *   rides thinly over it (the default, and the chart's namesake);

Installation

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

Prompt

Build a React + TypeScript + Tailwind "ChartCricketManhattan" chart — a cricket
Manhattan in hand-rolled SVG (no chart library, no animation library), played
forward over by over, with zod.

Contract
- A zod schema is the single source of truth:
  { status: "loading" | "empty" | "error" | "ready";
    innings: { team: string;
               overs: { over: number; runs: number;
                        wickets?: number; note?: string }[];
               target?: number }[];
    maxOvers?: number;      // 20 for a T20, 50 for an ODI
    startedAt?: string }    // ISO instant of the first ball
- Props = z.infer of the schema plus title?, variant?, over?, defaultOver?,
  onOverChange?, onRetry?, emptyState? and className. No hand-written parallel
  interface.
- An over is the atom: a Manhattan bar is "runs off the over", extras included,
  so nothing is ever sliced finer than an over and no ball-by-ball detail is
  invented to make an animation smoother.
- One or two innings share ONE over axis — that shared axis is what makes "who
  was ahead at ten overs" answerable. A third innings and beyond is ignored and
  counted.
- Ingest repairs instead of trusting, and prints every repair in a visible
  line: non-finite over numbers or run figures drop the over; a repeated over
  keeps the first reading; negative runs are pulled up to zero; wicket figures
  outside 0–10, or a running total that would put an eleventh batter out, are
  pulled into range; overs numbered past the scheduled allotment are drawn
  anyway and the axis grows to fit; an unparseable startedAt is printed
  verbatim and flagged. A chart that quietly renumbers overs is describing a
  match nobody played.

Behavior
- Four first-class branches in one bg-card panel: loading (a skeleton that
  mirrors the ready silhouette — scoreline, gridded plot with pulsing bars,
  legend, transport — so the card keeps its height), empty (the bare axis plus
  copy, reused with a stated reason when a ready payload has nothing
  drawable), error (message + "Try again" only when onRetry exists), ready.
- The playhead is COMPLETED OVERS and nothing else. It is derived, never a wall
  clock: `over` / `defaultOver` / `onOverChange` are the controlled,
  uncontrolled and reporting sides of the same number. Uncontrolled it is held
  as a float so a bar can grow inside an over; controlled the parent owns whole
  overs and the loop reports each one as it comes due. Paused at over N the
  card renders byte-identically every time — that is what makes it
  screenshot-stable and SSR-safe.
- NO autoplay, and no Date.now or Math.random in the first render path. The
  first frame parks at the last over bowled, so a static preview shows the
  finished match with both worms and every wicket. Play from there rewinds to
  over 0.
- Transport is real UI, never a gesture-only scrubber: step back, play/pause
  (aria-pressed), step forward, a labelled <input type="range"> with one step
  per over and an aria-valuetext that speaks the clock, a 1×/2×/4× speed
  radiogroup, and a "12.0 / 20.0 ov" readout in cricket's own notation —
  overs.balls, with the ball slot honest at 0 because per-over data carries no
  ball count. Step buttons are aria-disabled with a handler guard at each end,
  NEVER natively disabled: a control that vanishes under the finger at the last
  over is exactly the bug that avoids.
- One requestAnimationFrame loop advances the playhead by the frame's timestamp
  delta × speed against a fixed ms-per-over, so a dropped frame skips forward
  instead of slowing the match down. It is cancelled on pause, on every commit
  and on unmount, and a visibilitychange listener pauses when the tab hides —
  never leave a loop running off-screen. Keep the onChange callback in a ref so
  an inline handler cannot restart the loop every frame and freeze it.
- prefers-reduced-motion (matchMedia, subscribed with cleanup — a
  useSyncExternalStore fits): playback still works, but the rendered position
  quantises to whole overs, so each bar appears complete instead of growing.
  Gate every CSS transition with motion-safe/motion-reduce.
- A polite sr-only live region announces the transport, not the frames: on a
  step, a seek, a pause and once at the finish, each time with the over and
  both scorelines — never once per animation frame.
- Completed bars are a keyboard walk: the svg is role="listbox" with
  aria-activedescendant (role="img" with a one-sentence label when nothing is
  walkable yet), arrow keys / Home / End move a cursor over the bars in bowling
  order across both innings, and only :focus-visible arms it so a stray click
  does not pin the tooltip. Hover and cursor share one tooltip (over, runs,
  wickets, score, note) anchored in viewBox percentages, with a transparent hit
  rect no wider than the gap to the neighbouring bar.

Rendering & styling
- All plot maths lives in exported pure functions beside the component —
  ingestCricketInnings, layoutCricketManhattan, cricketRunY, cricketTotalY,
  cricketScoreAt, cricketWormPath, cricketOverNotation, cricketRunRate,
  formatCricketScore — so a test can print the same numbers the picture is
  made of.
- One fixed viewBox, so the card keeps its aspect at 6 overs or 50. Each over
  owns a slot; the two innings split ~78% of it side by side; bar height is
  runs off the over against a left axis rounded up to a multiple of six — one
  gridline per six runs, the most one legal ball can yield.
- Exactly one horizontal rule per labelled runs tick. The cumulative axis on
  the right gets ticks but NO gridlines: two scales cannot both own the rules
  without one of them lying.
- Wickets hang under the axis as drops — a stem from the baseline into that
  innings' lane, one mark per wicket that fell in the over, circles for the
  first innings and squares for the second. A maiden (zero runs) is a hollow
  diamond on the baseline, because a zero-height bar reads as "never bowled".
- Colour never carries meaning alone: the first innings is var(--chart-1)
  solid, the second var(--chart-2) with a diagonal card-coloured hatch and a
  dashed worm, every mark is repeated in the tooltip, the legend and an
  sr-only table.
- Scorelines above the plot in the game's vocabulary: "176/7" (or "… all out"),
  "12.0 ov", run rate to two decimals, and for a side with a target "Needs 62
  off 48 balls · RRR 7.75" from the SCHEDULED allotment, not from the last over
  the feed happens to carry.
- Semantic tokens only — bg-card, border, muted, muted-foreground, popover,
  destructive, primary for the scrubber accent, var(--chart-1..2) for the
  series. No hex, rgb() or oklch() anywhere. cn() merges className and the root
  spreads remaining props with data-status.
- Long team names and notes elide with a title attribute or line-clamp; they
  never widen the card.

Customization levers
- Tempo: OVER_MS sets how long an over takes at 1×; SPEEDS is a plain array —
  add 0.5× for over-by-over analysis, or 8× for a fifty-over innings.
- Variant: `manhattan` (bars lead, thin worm), `worm` (worm leads, bars recede
  to a silhouette), `bars` (no worm, no cumulative axis) — one prop, three
  readings of the same data.
- Geometry: VIEW_W/VIEW_H, PAD_L/PAD_R, BASE_Y, LANE_Y/LANE_STEP and
  GROUP_FILL control aspect, axis room, the wicket lanes and how fat the bars
  are; drop the lanes entirely for a bars-only mini card.
- Scales: the runs axis steps in sixes (twelves for an avalanche) and the
  cumulative axis in 25/50/100 — re-key niceCeil for Test-match totals.
- Tokens: INNINGS_TOKEN is a two-entry array — swap in the sides' own chart
  tokens; the hatch and the dashed worm keep the pair legible without hue.
- Blocks: the scoreline row, legend, footnote, repair line and sr-only table
  are independent siblings — drop any of them for an embedded mini chart.
- Domain: the same engine draws any per-period skyline with a cumulative
  overlay — innings become periods, wickets become any discrete event — but
  relabel every string if you move it off a cricket field.

Concepts

  • The playhead is completed overs, not a clock — one integer (a float only while a bar is growing) drives bars, drops, worm, scorelines, scrubber and announcements. Because it is injected rather than read from Date.now, a card paused at over 11 paints the same pixels on the server, in the browser and in a screenshot.
  • Controlled and uncontrolled are the same playbackover / defaultOver / onOverChange are one value in three roles: hand the prop in and the parent owns the match, leave it out and the transport does, and either way playback reports every over it crosses.
  • Manhattan and worm are two readings of one number — the bar is what happened in the over, the worm is what it added up to. They share the plot but not the axis: only the runs scale owns the gridlines, because two scales that both claim the rules make every crossing a coincidence.
  • Wickets are a drop, a shape and a sentence — a wicket hangs below the axis in the over it fell in, circle for the first innings and square for the second, and is repeated in the tooltip, the summary and the table. Nothing here is legible only in colour.
  • Reduced motion changes the tween, not the feature — with motion off the same clock runs but each over appears complete instead of growing, and every transition carries a motion-reduce variant. Playback, stepping, scrubbing and announcements all keep working.
  • Repairs are counted out loud — dropped overs, repeated over numbers, negative runs, an eleventh wicket, overs past the scheduled allotment and an unreadable start time each print their count under the chart. Silently fixing a scorecard turns a chart into a story about a match that was never played.

On This Page