Kitchen Dink Map
A four-state pickleball dink placement map in hand-rolled SVG — the opponent's kitchen to true scale with the net on the bottom edge, every tracked dink a dot coloured by outcome (in, too deep, net, wide), a count-and-share column apportioned by largest remainder, per-dot tooltips and arrow-key walking.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-kitchen-dink-map.jsonPrompt
Build a React + TypeScript + Tailwind "ChartKitchenDinkMap" chart — a
pickleball dink PLACEMENT map in hand-rolled SVG (no chart library), with zod.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready";
dinks: { x: number (-3.05..3.05, metres across the width, negative left);
y: number (0..3.2, metres past the net);
outcome: "in" | "tooDeep" | "net" | "wide" }[];
meta: { player: string; context?: string } }.
- Props = z.infer of the schema plus title?, onRetry?, emptyState? and
className; forwardRef to the card div. No hand-written parallel interface.
- One dot per dink, order meaningless. A wide dink's x is capped at the
sideline it crossed — the sign is the side, the value is not exact, because
a miss past the line has no bounded landing area. A netted dink's y is
ignored: it never crossed, so it has no depth.
Behavior
- Four first-class branches in one bg-card panel: loading (the ready layout
piece for piece — stat line, pulsing court beside a four-row column,
footnote — so the card keeps its height), empty (faint court + copy, reused
when a ready payload has nothing placeable), error (message + "Try again"
only when onRetry exists), ready.
- Ingest is a pure exported function: drop dinks with unreadable coordinates
and report the count in a visible line — a placement map that quietly loses
shots flatters the player it audits. The outcome is the scorer's call and it
wins: a coordinate that contradicts it (an "in" logged past the kitchen
line) is clamped into the region the outcome names.
- The four outcome shares are apportioned together by largest remainder so
they always print a clean 100: floor each share, hand leftover points to the
largest fractions, ties to the bigger count then the earlier outcome. One
pre-rounded set feeds the stat column, the aria summary and the sr table; a
non-zero count too small for a whole percent prints "<1%", never "0%".
- Hover a dot for a tooltip (outcome + metres across / past the net); Tab
focuses the map and arrow keys walk the dinks as a listbox with
aria-activedescendant, Home/End included. hovered wins over the keyboard
cursor; both are derived against the current count so a shorter payload can
never leave either pointing past the end. The active dot gets a foreground
halo, the rest dim to 0.35; each dot carries a transparent hit circle much
bigger than the painted one.
- Headline: the in-the-kitchen share big, then counts for too deep / net /
wide. The side column doubles as the legend — outcome dot, label, count,
share per row, a Tracked total under a border.
Rendering & styling
- Geometry in metres from the USA Pickleball court, never pixel guesses:
court 6.10 m wide (13.41 m long — context only), the kitchen (non-volley
zone) 2.13 m from the net, service courts 4.57 m beyond the kitchen line,
painted lines 0.051 m (2 in) at true scale. Net: 0.864 m at the centre
strap, 0.914 m at posts 0.305 m outside the sidelines (22 ft apart).
- The net sits on the BOTTOM edge — the reader stands where the dinker stands
— so screen y is the negated metres-past-net. A netted dink is drawn on the
face of the net itself, halfway up the cord at its x, because it owns no
ground; the cord's rise from strap to posts makes the band a shallow taper,
not a rectangle.
- Beyond the kitchen line, only the first half-kitchen (1.065 m) of the
service courts is drawn, as the too-deep strip, closed by a dashed crop
line; the centreline is drawn there and stops at the kitchen line, because
the kitchen has no centreline. Outside each sideline a dashed 0.61 m label
band holds the wide dots: real depth, surrendered width.
- Colour from tokens only: in var(--chart-2), tooDeep var(--chart-4), net
var(--chart-5), wide var(--chart-3); court lines stroke-border, the net
stroke-muted-foreground, the kitchen washed fill-muted at 0.4. The regions
repeat the encoding — an "in" dot is inside the kitchen, a wide dot outside
the sideline — so colour is never the only channel.
- Every dot wears a 2px var(--card) ring (vectorEffect non-scaling-stroke) so
an overlapping cluster reads as N rings, not one blob.
- Tooltips are aria-hidden echoes anchored in percentages of the viewBox (no
measuring, no observer), sliding near the side edges and flipping below the
dot near the top edge; a sentence-long aria-label per option and one sr-only
table repeat everything they say. Transitions respect
prefers-reduced-motion; skeleton pulses carry motion-reduce:animate-none.
- Panel rounded-xl border bg-card p-4, numbers tabular-nums, cn() merges
className, the root spreads remaining props and carries data-status.
Customization levers
- Regions: MARGIN is half a kitchen — deepen it to show more of the service
court, or drop the too-deep strip entirely for a kitchen-only card;
WIDE_BAND (0.61 m) widens for fat out-of-bounds gutters.
- Encoding: the four outcome tokens live in one OUTCOME_TOKEN map — retheme
or re-bucket there; DOT_R / HIT_R trade dot legibility against cluster
density on heavy sessions.
- Stat column: it is the legend — reorder OUTCOMES to change both at once, or
drop the share column for a counts-only card.
- Orientation: the net-at-bottom view is one sign flip (screen y = -y); flip
it to put the net on top if the host draws serve maps that way.
- Copy: outcome labels and the tooltip phrases are two small records
(OUTCOME_LABEL, placePhrase) — relabel for padel or short tennis; only the
metre constants are pickleball's.Concepts
- Outcome owns the region — a dot's colour and its region are both decided by the outcome alone, so the map stays readable in greyscale: an "in" dot can only be inside the kitchen, a too-deep dot only in the strip past the line, a wide dot only in the out-of-bounds band. A coordinate that disagrees with its outcome is clamped into the outcome's region, because the scorer's call is the datum and a dot in the wrong region would contradict every count on the card.
- A netted dink has no landing point — it never crossed, so it cannot own ground in the kitchen. Its dot hangs on the face of the net itself, halfway up the cord at its x, with the cord drawn at its real heights (0.864 m strap, 0.914 m posts): the one honest place for a shot with no coordinate on the far side.
- Label bands, not landing areas — a wide miss is unbounded, so the dashed band outside each sideline is a labelled region of stated width, not to-scale court; the wide dot keeps its true depth and gives up its exact width. The same honesty crops the service court to half a kitchen: a "dink" deeper than that belongs on a different chart.
- Shares are apportioned, never rounded one by one — the four outcome percentages are cut by largest remainder so they add to exactly 100 in every payload; 27 / 6 / 5 / 3 of 41 rounded separately would drift between 99 and 101 and read as a data error, and a real count too small for a whole percent prints "<1%" rather than a lying "0%".
- Hover is an echo, the keyboard is a cursor — the tooltip is an aria-hidden repeat of the option's own aria-label; Tab plus arrow keys walk the same dots through aria-activedescendant, hovering always wins over the cursor, and both indices are derived against the current dink count so a reload can never strand them.
- The court is a measurement, not a backdrop — every line comes from USA Pickleball numbers (6.10 m width, 2.13 m kitchen, 0.051 m lines, posts 0.305 m outside the sidelines), including the detail maps get wrong: the centreline splits only the service courts and stops at the kitchen line, because the kitchen has none.
Court Zone Win Rate
A four-state tennis choropleth in hand-rolled SVG — the player's own half cut into a 3×3 grid derived from ITF court lines (deuce / middle / ad lanes × net / mid / baseline depths), each zone tinted by how far its point win rate sits from 50%, thin samples hatched, every zone hover- and keyboard-readable.
Third Shot Outcome
A four-state pickleball third-shot report in hand-rolled SVG — one row per choice (drop, drive, lob) with its usage share, a 100% stacked won / neutral / lost bar apportioned by largest remainder, per-row n and point-win labels, a totals row, segment tooltips and arrow-key walking.