Shuttle Trajectory
A four-state badminton flight-signature chart in hand-rolled SVG — the full 13.40 m court seen from the side with a to-scale net at centre, each stroke type aggregated into a median parabolic arc with a p25–p75 band, dash patterns as second identity, landing marks on the floor and physics guards that lift impossible arcs over the cord.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-shuttle-trajectory.jsonPrompt
Build a React + TypeScript + Tailwind "ChartShuttleTrajectory" chart — a
badminton flight-signature side elevation 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: { type: "clear" | "drop" | "smash" | "drive" | "net";
contactDepth: 0..6.7 (metres behind the net on the hitter's
side — rear-court strokes leave from around 4.5);
contactHeight: metres above the floor at contact;
apexHeight: the flight's highest point in metres;
landingDepth: metres past the net on the opponent's side }[];
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.
Court geometry (from the BWF book, not from pixels)
- The court is 13.40 m long, 6.70 m each side of the net. The elevation draws
the whole court, hitter's half on the left (negative x), one shared scale
constant on both axes so the picture is never stretched.
- The net is a band at centre drawn 1.55 m tall — its height at the posts;
the cord sags to 1.524 m at the centre strap, which the footnote states
because one elevation cannot draw two cords. The tape reads stronger than
the paint.
- The floor line carries a depth tick every metre; the opponent's short
service line (1.98 m past the net) and the doubles long service line
(5.94 m — 0.76 m inside the baseline) get labelled dotted risers. A height
axis in metres stands on the left.
Behavior
- Four first-class branches in one bg-card panel: loading (stat line, the
court frame with its net, pulsing marks), empty (faint side-on court +
copy, reused when a ready payload has nothing drawable), error (message +
a "Try again" button only when onRetry exists), ready.
- Ingest is a pure exported function that groups flights by stroke type in a
fixed order and reports everything it cannot honour: a non-finite number
or non-positive contact height is unreadable (dropped, counted); a landing
depth at or behind the net means the shuttle never crossed, so there is no
arc to draw (dropped by reason, counted); an apex below its own contact
height is impossible for a falling shuttle (raised to the contact height,
counted). Never drop or repair silently.
- Each type with two or more flights draws a median arc — the parabola
through its median contact point, median apex and median landing — plus a
translucent band between the arcs built from the p25 and p75 apex and
landing (the band's edges share the median contact: quartiles describe how
the flights spread, not where the player stood). A type with one flight
draws its single raw arc thinner, tagged "n=1", with no band.
- The parabola maths is exact, not sampled: solve y = a(x − h)² + k through
the contact and the landing with apex height k, then emit one quadratic
Bézier whose control point is the tangent intersection (control x is the
endpoint midpoint — a property of parabolas). Clamp k to at least the
contact height; if the implied path crosses the net plane below the cord,
lift k the minimum that clears it (bracket by doubling, then bisect) and
flag the lift — the shuttle landed past the net, so it crossed.
- Every flight leaves a landing mark on the floor line in its type's shape,
so the spread of landings reads even where the band is thin.
- Hover an arc, its band or a legend chip → that stroke emphasised, the rest
dimmed (guarded pointer-leave so crossing arcs never blinks), a tooltip at
the median apex with n, median apex and median landing, flipping below the
apex near the frame top and sliding near the baselines. Keyboard: the svg
is one role="listbox" tab stop whose options are the stroke types; arrow
keys walk them in fixed order, Home/End jump, aria-activedescendant
follows, and only :focus-visible arms the cursor so a mouse click doesn't
pin the tooltip. The tooltip is aria-hidden — each type's aria-label
already speaks a full sentence — and an sr-only summary + table repeat
every aggregate.
Rendering & styling
- Semantic tokens only. Identity is double-encoded and never re-assigned:
clear = var(--chart-1) solid, drop = var(--chart-2) dashed, smash =
var(--chart-3) dotted, drive = var(--chart-4) dash-dot, net shot =
var(--chart-5) fine-dot — five distinct dash patterns carry the stroke on
their own for a greyscale or colour-blind reader, and the landing shapes
(circle, triangle, square, diamond, inverted triangle) repeat the identity
on the floor.
- Bands are the type token at fillOpacity 0.13 with hairline quartile edges;
landing marks and the median contact dot wear a var(--card) ring so piles
stay separable. Court paint: gridlines stroke-border, floor and ticks
stroke-muted-foreground, tape stroke-foreground/70, net fill-muted. Every
label wears paintOrder="stroke" with a var(--card) halo so an arc can never
blot it out.
- Panel rounded-xl border bg-card p-4, numbers tabular-nums, cn() merges
className, root spreads remaining props and carries data-status; emphasis
transitions respect prefers-reduced-motion; the court group is aria-hidden.
Customization levers
- The frame's ceiling follows the tallest drawn apex (never below a stated
minimum), so towering clears get sky without code changes; adjust the
minimum for a card that only ever shows flat strokes.
- CLEAR_MARGIN is how far above the cord a lifted arc must pass; NET_W is
the drawn width of the net band — a drawing device, since a net has no
depth.
- TYPE_INK / TYPE_DASH / the landing-glyph switch are the only home of the
stroke identity — add a sixth stroke (lift, push) by extending the enum
and those three maps, and ingest, legend and table follow.
- Quartiles are two calls to one quantileSorted function; swap p25/p75 for
p10/p90 to widen the band's story.
- Drop the landing marks for a cleaner arc card, or the bands for a
median-only comparison — each reads from the same ingest.
- Sport swap: the same skeleton reads any "flight over a barrier" story —
volleyball serves over the net, football free kicks over a wall — by
renaming the strokes and swapping the four geometry constants.Concepts
- Arcs are aggregates, not spaghetti — every flight of a stroke type folds into one median arc with a quartile band, so five signatures stay readable where thirty raw arcs would weave into noise; a type with a single flight is drawn raw, thinner and tagged "n=1", because a median of one would be a costume.
- The parabola is exact — the arc is solved through contact, apex and landing as y = a(x − h)² + k and emitted as a single quadratic Bézier whose control point is the tangents' intersection (a parabola property: its x is the endpoint midpoint), so the drawn apex is the stated apex, never a sampling artefact.
- Physics is a rendering guard — a shuttle that landed past the net crossed it, so an arc whose supplied numbers imply a flight through the mesh is lifted the minimum that clears the cord, and an apex below its own contact height is raised to it; both repairs are counted and printed, never silently obeyed or hidden.
- Dash is the identity's second channel — on a chart whose marks are lines, colour alone dies in greyscale, so each stroke owns a distinct dash pattern and a distinct landing shape, assigned in a fixed order that never re-shuffles when a type is absent.
- The court is drawn from the book — 13.40 m end to end, the net band 1.55 m at the posts (the 1.524 m centre sag stated in the footnote, since one elevation can draw only one cord), the short service line at 1.98 m and the doubles long service line at 5.94 m — so "died before the service line" is read against the line itself, on one shared scale that never stretches the picture.
- One tab stop, every stroke reachable — the svg is a single listbox whose options are the stroke types; arrow keys walk them in fixed order with
aria-activedescendantfollowing the emphasis, and only:focus-visiblearms the cursor so a mouse click never pins a tooltip nobody asked for.
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.
Rally Replay
A playable top-down replay of one tennis rally in hand-rolled SVG — the ball tweens along each shot's curved path over a to-scale ITF singles court, landed shots persist as trails with walkable bounce dots, and play / scrub / speed controls drive a single timeline that starts parked at the finished point.