Body Load Map
Front and back human silhouettes in hand-rolled SVG whose muscle areas shade by reported load or soreness — one chart token at stepped opacity, hatching for areas nobody reported, leader-lined labels on the worst areas and a ranked list underneath.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-body-map.jsonPrompt
Build a React + TypeScript + Tailwind "Body Load Map" component (zod, lucide-react, a cn() class
merger). Two human silhouettes — front and back — drawn as hand-rolled SVG paths, where every
muscle area is a separately fillable region whose colour intensity encodes one reported number:
soreness 0-10, weekly load, session count.
Contract
- zod schema, one source of truth:
status: "loading" | "empty" | "error" | "ready"
regions: { id: string; label: string; side: "front" | "back"; value: number }[] // sparse
scale: { measure: string; unit?: string; max?: number; flagAt?: number }
athlete?: { name: string; session?: string; date?: string }
- side is part of the KEY, not decoration: the lookup is `${side}:${id}`, so "shoulders" on the
front is the anterior deltoid and on the back the posterior one, each with its own shape. A pair
the map draws no shape for is refused and counted — never guessed onto the other figure.
- The 17 areas drawn: front — shoulders, chest, biceps, forearms, core, obliques, quads, shins;
back — traps, shoulders, lats, triceps, forearms, lower-back, glutes, hamstrings, calves.
- Props on top of the contract: title, description, steps (2-6, default 4), topCount (list rows,
default 5), labelCount (on-map labels, 0-4, default 3), onRetry, emptyState, className,
...div props. forwardRef onto the card.
Figure geometry (hand-rolled SVG, viewBox "0 0 300 228")
- ONE silhouette, drawn twice: a body seen from the front and from behind has the same outline,
so only the muscle areas on top of it differ. Head circle, neck, torso, plus arm / leg / foot.
- Every shape is authored on the right half (x > 50) of a 100 x 212 box and mirrored with
translate(100,0) scale(-1,1), i.e. x → 100 - x. That is what makes the figure symmetric by
construction rather than by careful typing. Midline shapes (core, traps, lower back) are single
symmetric paths; bilateral ones are drawn twice inside one <g>, so both halves are one target.
- Layout: 46 gutter · 100 front · 8 gap · 100 back · 46 gutter. The body only spans x 19-81 inside
its box, so label text may reach past the box edge without ever touching a figure.
- Keep the shapes simple and flat — a stylised diagram, never anatomical illustration.
Behavior
- Ingest (pure, exported, testable): payload rows → placed readings, with three refusals counted by
reason and printed under the map: an area the map has no shape for, a negative / non-finite
value, and a repeat of an area already placed. Repeats are REFUSED, not summed: a soreness score
is ordinal, 6 this morning plus 5 tonight is not 11, and silently picking one would hide that two
answers arrived. Aggregate upstream where the rule is known.
- The ladder: levelOf(value) = floor(value / ceiling * steps), clamped onto the top step, cut on
(0, ceiling]. ceiling = scale.max when stated (a 0-10 questionnaire tops out at 10 even on a
quiet week), otherwise the busiest reported area. Values over a stated ceiling land on the top
step and are counted in a note rather than stretching the declared scale. If the ceiling was
derived AND every positive reading is equal, the ladder collapses to one mid-ramp step.
- Three distinct marks, never two: a tinted area is a reported value, a FLAT area is a measured
zero ("asked, nothing there"), a HATCHED area was never reported. Zero is excluded from the
ladder on purpose — missing data must not read as the palest shade of sore.
- Ranking is one rule used everywhere: value descending, ties by label, then anatomical order.
The ranked list, the on-map labels and the summary all read that one array, so the list order can
never disagree with the shading.
- On-map labels: the top ranked areas get a leader line from a point inside their own shape out to
a two-line block (name + value) in that figure's gutter. Blocks that want the same band are
pushed down, and a stack that runs past the frame slides back up — the LABEL moves, the anchor
never does, so a displaced label cannot rename a muscle. Front anchors are mirrored (x → 100 - x)
so every leader exits toward its own gutter instead of crossing the card.
- Hover / focus raises an SVG tooltip: area name plus value and unit (or "not reported"), measured
from its own text and clamped inside the frame, flipping below the anchor when there is no
headroom above.
- Keyboard: the svg is ONE tab stop (role listbox, tabIndex 0, focus-visible outline) whose areas
are role option groups driven by aria-activedescendant. Arrow keys walk head to foot, front
figure then back; Home / End jump to the ends. Unreported areas stay in the walk and answer
"not reported", so what is missing is discoverable, not invisible.
- flagAt draws a destructive outline on qualifying areas — on its own layer, so a neighbour's fill
can never paint over half a ring — plus a marked row in the list and a count in the summary.
- Four states are first-class branches: loading is a pulsing pair of silhouettes
(motion-reduce:animate-none) behind an sr-only status; empty keeps the silhouettes; error shows
the retry button only when onRetry exists. A ready payload where nothing could be placed renders
the empty branch and says how many entries arrived — the model decides the branch, not the caller.
Rendering & styling
- Semantic tokens only. Bodies are fill-muted; intensity is ONE chart token at stepped opacity
(fill var(--chart-1), fillOpacity 0.34 → 0.95). The floor is deliberately high: a 10%-alpha tint
that reads fine on a white card disappears on a near-black one. Areas carry a var(--card)
hairline so two adjacent hot muscles never merge into one blob.
- The hatch is a <pattern> of muted-foreground strokes over fill-muted, never a paler chart tint —
and its strokes ride on muted-foreground because --border is only 10% white in dark mode.
- Colour is never the only channel: the legend prints every step bound, the labels print names and
values, the ranked list carries an exact-length bar beside each tinted swatch, and an sr-only
table lists every reading plus the areas nobody reported.
- A measured zero is painted opaque muted rather than the chart token at zero alpha: identical on
screen, but an unpainted fill drops out of SVG hit-testing and the area would stop answering.
- cn() merges className; the root spreads remaining div props and forwards its ref; the retry
button and the svg carry focus-visible rings; the only animation is the skeleton pulse.
Customization levers
- Palette: swap var(--chart-1) for another single chart token (a second card on --chart-2 makes a
before / after pair). Keep ONE token per map — a multi-hue ramp stops reading as intensity.
- Ramp: the 0.34 / 0.95 floor and ceiling, and steps 2-6 (3 reads as light / moderate / heavy,
6 as a near-gradient). Raise the floor for projector decks.
- Scale: scale.max fixes the ceiling for bounded questionnaires, omit it for open-ended counts;
scale.flagAt sets the action line, or drop it for measures with no threshold.
- Density: labelCount 0 turns the on-map labels off for a thumbnail, topCount trims or extends the
ranked list (the sr-only table always carries everything).
- Vocabulary: scale.measure and scale.unit rename the numbers ("sessions", "AU", "/10"); the
fallback area names live in one AREAS table beside their paths.
- Anatomy: add an area by adding one row to AREAS — key, path (right half), whether it mirrors,
and an anchor point inside the shape. Split "core" into upper and lower abs, or add adductors,
without touching the ingest, the ladder or the ranking.
- Layout: move the ranked list beside the map on wide cards with a container query; keep the two
figures side by side, because front / back comparison is the whole point of the card.Concepts
sideis part of the key — the map looks upside:id, so the same word can name a different muscle on each silhouette (frontshouldersis the anterior deltoid, backshouldersthe posterior one). A pair with no shape is refused and counted, never quietly moved to the other figure.- Three marks, not two — a tint is a reported value, a flat area is a measured zero, a hatched area was never reported. Zero is excluded from the intensity ladder on purpose: "asked, nothing there" and "nobody asked" are different statements, and neither may be drawn as a faint version of the other.
- Mirror by construction — every shape is authored on the right half and reflected through
x → 100 - x, so the figure is symmetric because of the transform, not because two path strings were typed carefully. One silhouette serves both figures; only the muscle areas on top of it change. - One ranking, three consumers — value descending with a name tie-break feeds the ranked list, the on-map labels and the summary line alike, so the list can never disagree with the shading, and two areas reporting the same number never swap places between renders.
- The label moves, the anchor doesn't — leader-line labels that want the same band are pushed apart and a stack that overruns the frame slides back up, but every line still ends on a point inside the area it names.
- Repeats are refused, not summed — touches accumulate, a soreness score does not: 6 this morning plus 5 tonight is not 11. A second reading for an area is counted in a visible note so the disagreement surfaces where the aggregation rule actually lives.
- A ramp that survives dark mode — one chart token whose opacity climbs from 0.34, composited over the muted silhouette. The floor is high on purpose: a 10%-alpha tint that looks fine on a white card is invisible on a near-black one, and every area also carries a
var(--card)hairline so two hot neighbours never merge.
Learning Curve
A four-state learning curve: training and validation score against training-set size, each inside a ±1 SD band computed from the per-fold scores, with the gap at the largest size bracketed and read as high variance, still converging, still learning, high bias or converged.
HRV Frequency Spectrum
A four-state HRV power spectrum in plain SVG: the density estimate drawn as an area, cut and shaded at the VLF / LF / HF edges, each band integrated by the trapezoid rule into its own power and share, with an LF:HF readout and a normalised-units toggle.