Premium, data-driven muscle heatmaps for the web. Render the human body and color each muscle group by a 0–100 score — as a clean flat-vector figure or layered over a photorealistic body. Headless core + React.
MuscleMap is a standalone visualization library for showing human bodies and muscle groups as an interactive heatmap. You feed it normalized scores per muscle (0–100); it paints the body.
It is intentionally app-agnostic — no database, no backend, no auth, no framework lock-in in the core. It was built for fitness analytics products (e.g. TrainPilot / RepMap) but ships free of any of them.
Not an analytics or tracking tool — purely the rendering layer. Your app calculates the scores; MuscleMap draws them.
Status — early / pre-release. Included: the core API, color engine, region rules, male & female front/back bodies, bundled demo body photos, the React component, unit tests and CI. The public API may still change before
1.0.
- 🧠 Headless core — color scales, region/visibility rules and scoring helpers with zero UI, usable from any framework.
- 🖼️ Two looks, one component — flat vector silhouette, or a greyscale photoreal body with the colored muscles laid on top, pixel-aligned.
- 🎯 Each muscle surface individually addressable — color a whole group (bundled) or a single surface like
TRAPEZIUS_LEFT(e.g. left/right balance). - 🔍 Region views — full body, upper body, lower body — actually cropped, not just zoomed.
- 🎨 Multiple color models — Load, Frequency, Balance, Recovery Risk — body fills and legend always in sync.
- 💎 Premium dark aesthetic — per-muscle gradient shading, soft glow, hover/tap tooltips.
- ✏️ Bring your own body — trace any anatomy in Inkscape and drop it in (see Assets).
| Flat vector | Photoreal hybrid | Upper-body view | Lower-body view |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Host app training data
→ host app aggregation / API
→ normalized MuscleMap scores (0–100 per muscle)
→ @musclemap/core headless types, color scales, visibility rules
→ @musclemap/assets body diagrams (SVG path data + region boxes)
→ @musclemap/react <MuscleMap /> component
→ SVG heatmap| Package | Responsibility |
|---|---|
@musclemap/core |
Domain types (MuscleGroup, MuscleMapValues, …), color scales (getMuscleHeatColor, getColorScaleCss), region/visibility rules (getVisibleMuscleGroups). UI-free. |
@musclemap/assets |
Per-sex, per-view BodyDiagrams: muscle path data with semantic IDs + cropped region viewBoxes. Framework-free. |
@musclemap/react |
The <MuscleMap /> and <MuscleMapLegend /> components. |
apps/playground |
Vite demo for visual QA (not published). |
pnpm add @musclemap/react @musclemap/core @musclemap/assets react react-domFeed values keyed by MuscleGroup, each with a score from 0–100. That's the whole contract.
import { MuscleMap } from "@musclemap/react";
import type { MuscleMapValues } from "@musclemap/core";
const values: MuscleMapValues = {
CHEST: { score: 88, volumeKg: 7200, sets: 24, trend: "UP" },
TRAPEZIUS: { score: 90 },
LATS: { score: 84 },
QUADS: { score: 66 },
CALVES: { score: 26, trend: "DOWN" },
// …any subset of muscle groups
};
export function Analytics() {
return (
<MuscleMap
values={values}
view="BOTH" // FRONT | BACK | BOTH
colorModel="LOAD" // LOAD | FREQUENCY | BALANCE | RECOVERY_RISK
/>
);
}Your app owns aggregation; MuscleMap just renders. volumeKg / sets / trend are optional and only used by the tooltip.
Color a whole group via values, or override an individual surface via partValues (keyed by the path's id, e.g. HAMSTRINGS_LEFT). Perfect for left/right balance.
<MuscleMap
values={{ HAMSTRINGS: { score: 70 } }} // both sides, bundled
partValues={{
HAMSTRINGS_LEFT: { score: 90 }, // overrides just the left
HAMSTRINGS_RIGHT: { score: 50 },
}}
/>The selection callback reports the surface too, so you can react to a specific left/right click — partId is undefined when a bundled (whole-group) path is tapped:
<MuscleMap
values={{ HAMSTRINGS: { score: 70 } }}
partValues={{
HAMSTRINGS_LEFT: { score: 90 },
HAMSTRINGS_RIGHT: { score: 50 },
}}
onSelectMuscle={({ group, partId, value }) => {
console.log(group, partId, value?.score); // "HAMSTRINGS" "HAMSTRINGS_LEFT" 90
}}
/>Pass a body photo as the background; it's clipped to the silhouette and (optionally) desaturated, with the colored muscles on top — pixel-aligned because both come from the same trace.
Four ready-to-use body photos ship with @musclemap/assets (import them as asset URLs), or pass any URL of your own:
import maleFront from "@musclemap/assets/bodies/male-front.webp";
import maleBack from "@musclemap/assets/bodies/male-back.webp";
<MuscleMap
values={values}
backgroundImageFront={maleFront}
backgroundImageBack={maleBack}
backgroundGrayscale // strip the photo's colors → neutral grey body
backgroundBrightness={1.2}
backgroundOpacity={0.45}
/>The bundled photos are AI-generated demo bodies (MIT). For production/marketing, swap in your own branded imagery —
backgroundImage*takes any URL.
Set region and turn on cropToRegion to get a real cropped view, not just a zoom — the figure only occupies the space of its region and out-of-region muscles are inactive.
// Full body
<MuscleMap values={values} view="FRONT" region="FULL_BODY" />
// Upper body — only chest/back/shoulders/arms/abs active, cropped to the torso
<MuscleMap values={values} view="FRONT" region="UPPER_BODY" cropToRegion />
// Lower body — only legs active, cropped to hips→feet
<MuscleMap values={values} view="FRONT" region="LOWER_BODY" cropToRegion />| Region | Active muscles | Frame |
|---|---|---|
FULL_BODY |
everything | head → feet |
UPPER_BODY |
chest, back (traps/lats/rhomboids), shoulders, arms, abs, obliques, lower back | head → waist |
LOWER_BODY |
glutes, quads, hamstrings, calves, adductors, abductors, hip flexors | hips → feet |
There's also a CORE region (abs, obliques, lower back) for isolating the midsection — abs belong to both UPPER_BODY and CORE. The playground demos the three main views; the API supports all four regions.
Region viewBoxes are precomputed per body and stored on the
BodyDiagram, so cropping is exact for whatever geometry you ship.
getMuscleHeatColor(score, model) (continuous) drives the body; getColorScaleCss(model) drives the legend, so they never drift apart.
| Model | Scale |
|---|---|
LOAD |
training load — blue → cyan → green → yellow → orange → red |
FREQUENCY |
how often trained |
BALANCE |
under-represented (violet) → balanced (green) → over (red) |
RECOVERY_RISK |
ready (green) → risky (red) |
import { getMuscleHeatColor, getColorScaleCss } from "@musclemap/core";
getMuscleHeatColor(72, "LOAD"); // "#f9a..." smooth interpolation
getColorScaleCss("LOAD", "90deg"); // "linear-gradient(90deg, …)"Prefer one brand color over the multi-hue ramps? Set monochromeColor — the body and legend then run a single grey → color scale (0–100). colorModel is ignored while it's set. Monochrome colors accept #RGB and #RRGGBB hex values.
// 0 = grey → 100 = your blue
<MuscleMap values={values} monochromeColor="#2f7bff" />
// optional: override the score-0 base color
<MuscleMap values={values} monochromeColor="#2f7bff" monochromeBaseColor="#e5e7eb" />import { getMonochromeColor, getMonochromeScaleCss } from "@musclemap/core";
getMonochromeColor(0, "#2f7bff"); // "#6b7280" (grey base)
getMonochromeColor(100, "#2f7bff"); // "#2f7bff" (full color)
getMonochromeColor(50, "#2f7bff"); // halfway blend
getMonochromeScaleCss("#2f7bff"); // "linear-gradient(90deg, #6b7280 0%, #2f7bff 100%)"| Prop | Type | Default | Description |
|---|---|---|---|
values |
MuscleMapValues |
— | Score per muscle group (0–100). |
partValues |
Partial<Record<string, MuscleMapValue>> |
— | Per-surface overrides keyed by path id. |
sex |
"MALE" | "FEMALE" |
"MALE" |
Which body — male and female front/back are both included. |
view |
"FRONT" | "BACK" | "BOTH" |
"BOTH" |
Which side(s). |
region |
"FULL_BODY" | "UPPER_BODY" | "LOWER_BODY" | "CORE" |
"FULL_BODY" |
Active region. |
cropToRegion |
boolean |
false |
Crop the figure to region. |
colorModel |
"LOAD" | "FREQUENCY" | "BALANCE" | "RECOVERY_RISK" |
"LOAD" |
Heatmap scale. |
monochromeColor |
string |
— | Single-color override using #RGB or #RRGGBB; overrides colorModel. |
monochromeBaseColor |
string |
"#6b7280" |
Hex base color at score 0 for the monochrome scale. |
glow |
boolean |
true |
Glow halo behind active muscles. |
showLegend |
boolean |
true |
Render the gradient legend. |
legendMinLabel |
string |
— | Custom label for the low end of the legend. |
legendMaxLabel |
string |
— | Custom label for the high end of the legend. |
tooltipFields |
TooltipField[] |
["group","score"] |
What the hover/tap tooltip shows. |
labels |
Partial<Record<MuscleGroup,string>> |
— | Localized tooltip labels (defaults to the raw enum). |
backgroundImageFront / backgroundImageBack |
string |
— | Body photo behind the figure. |
backgroundGrayscale / backgroundBrightness / backgroundOpacity |
boolean / number / number |
false / 1 / 1 |
Background image treatment. |
figureWidth |
number |
200 |
Per-figure SVG width (px). |
onSelectMuscle |
(selection: { group: MuscleGroup; partId?: string; value?: MuscleMapValue }) => void |
— | Fired on tap/click with the selected muscle group, the optional surface partId (e.g. HAMSTRINGS_LEFT), and the resolved value (respecting partValues). |
className |
string |
— | Optional CSS class applied to the root container. |
style |
CSSProperties |
— | Inline styles applied to the root container. |
A BodyDiagram is just SVG path data with semantic IDs. To add or replace a body:
- Trace the muscles in Inkscape over a reference image (front + back). Use the pen tool (BSpline mode for smooth curves).
- Name each path in the Objects panel with a semantic label —
CHEST_LEFT,QUADS_LEFT,TRAPEZIUS_LEFT, … plus aBODYsilhouette. Inkscape stores these asinkscape:label. - Keep one viewBox for the body and its reference photo so the colored vector and the photo stay aligned.
- Import — flatten the labelled paths to absolute coordinates and emit a
BodyDiagram. The region viewBoxes are computed automatically from the muscle positions.
Each labelled surface becomes individually addressable; the coarse group (mapped from the label) enables region filtering and bundled coloring.
Licensing: Everything is MIT — free to use, modify and redistribute. The traced SVG path data in
@musclemap/assetsis original work. The four body photographs used for the hybrid look were generated with OpenAI image tooling from the maintainer's own prompts and then manually edited/traced; they ship in@musclemap/assets(bodies/*.webp, importable as asset URLs) and are MIT too. They're AI-generated demo bodies — swap in your own for production. Full breakdown:ASSET_PROVENANCE.md.
pnpm install
pnpm build # build packages (core → assets → react)
pnpm dev # run the playground
pnpm typecheck
pnpm lint # ESLint + SonarJS rules
pnpm test # vitestThe three packages depend on each other via the workspace:* protocol. Publish
with pnpm (pnpm -r publish / pnpm pack) — it rewrites those to the
concrete package version in the published tarball; plain npm publish
does not. Each package sets publishConfig.access: "public" so the scoped
packages publish publicly. CI packs the tarballs and fails if any still contains
a workspace: dependency.
MIT — and it covers everything in this repository: all source code, the
traced SVG muscle path data in @musclemap/assets, and the four body photos
bundled in @musclemap/assets (bodies/*.webp, generated with OpenAI image
tooling from the maintainer's own prompts, then manually edited). All of it is
free to use, modify and redistribute. See LICENSE and
ASSET_PROVENANCE.md.


