Charts

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import type {
  ChartShuttleTrajectoryData,
  ChartShuttleTrajectoryShot,
  ChartShuttleTrajectoryStroke,
} from "./chart-shuttle-trajectory.contract"

export interface ChartShuttleTrajectoryProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
    ChartShuttleTrajectoryData {

Installation

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

Prompt

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-activedescendant following the emphasis, and only :focus-visible arms the cursor so a mouse click never pins a tooltip nobody asked for.

On This Page