/* ============================================================ charts.jsx — quiet, precise SVG charts. Every series chart shows the personal-baseline / SWC band. No gradients, no chartjunk. Status lives in the dot, not the line. Exports to window. ============================================================ */ (function () { const { useId } = React; // map helpers const lerp = (a, b, t) => a + (b - a) * t; function extent(arr, pad = 0) { let lo = Math.min(...arr), hi = Math.max(...arr); if (lo === hi) { lo -= 1; hi += 1; } const p = (hi - lo) * pad; return [lo - p, hi + p]; } const STATUS_VAR = { good: "var(--good)", attn: "var(--attn)", within: "var(--c-line)", lowconf: "var(--ink-4)", acute: "var(--acute)", }; // --------------------------------------------------------- // Spark — sparkline with baseline + SWC band + status dot // --------------------------------------------------------- function Spark({ data, w = 132, h = 40, baseline, swc, status = "within", lowconf = false, dot = true, pad = 6 }) { const all = data.concat( baseline != null ? [baseline - (swc || 0) * 1.4, baseline + (swc || 0) * 1.4] : [] ); const [lo, hi] = extent(all, 0.12); const x = (i) => lerp(pad, w - pad, data.length < 2 ? 0.5 : i / (data.length - 1)); const y = (v) => lerp(h - pad, pad, (v - lo) / (hi - lo)); const pts = data.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" "); const last = data[data.length - 1]; const col = STATUS_VAR[status] || STATUS_VAR.within; return ( {baseline != null && swc != null && ( )} {baseline != null && ( )} {dot && ( <> )} ); } // --------------------------------------------------------- // SignalSpark — tiny directional spark for sub-acute signals // --------------------------------------------------------- function SignalSpark({ data, dir, higherBetter, w = 64, h = 26 }) { const [lo, hi] = extent(data, 0.15); const x = (i) => lerp(2, w - 2, i / (data.length - 1)); const y = (v) => lerp(h - 3, 3, (v - lo) / (hi - lo)); const pts = data.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" "); const bad = (dir === "up") !== higherBetter; // moving in the unhelpful direction const col = bad ? "var(--attn)" : "var(--good)"; return ( ); } // --------------------------------------------------------- // TrendChart — long range line + 7d rolling avg + SWC band // --------------------------------------------------------- function TrendChart({ raw, avg, baseline, swc, w = 320, h = 132, unit = "", label = "", threshold = null, status = "within", higherBetter = true }) { const padL = 8, padR = 30, padT = 10, padB = 18; const all = raw.concat(baseline != null ? [baseline - swc, baseline + swc] : [], threshold ? [threshold.value] : []); const [lo, hi] = extent(all, 0.08); const x = (i) => lerp(padL, w - padR, i / (raw.length - 1)); const y = (v) => lerp(h - padB, padT, (v - lo) / (hi - lo)); const rawPts = raw.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" "); const avgPts = avg.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" "); const ticks = [lo, (lo + hi) / 2, hi]; const col = STATUS_VAR[status]; return ( {ticks.map((t, i) => ( {t.toFixed(t < 10 ? 1 : 0)} ))} {baseline != null && ( )} {baseline != null && ( )} {threshold && ( {threshold.label} )} ); } // --------------------------------------------------------- // FFFChart — fitness / fatigue / form // --------------------------------------------------------- function FFF({ ctl, atl, form, w = 320, h = 150 }) { const padL = 8, padR = 8, padT = 10, padB = 20; const allTop = ctl.concat(atl); const [lo, hi] = extent(allTop, 0.08); const fext = extent(form, 0.2); const x = (i) => lerp(padL, w - padR, i / (ctl.length - 1)); const yTop = (v) => lerp(h - padB, padT, (v - lo) / (hi - lo)); const line = (arr) => arr.map((v, i) => `${x(i).toFixed(1)},${yTop(v).toFixed(1)}`).join(" "); // form as zero-baselined area at bottom band const fy = (v) => lerp(h - padB, h - padB - 34, (v - fext[0]) / (fext[1] - fext[0])); const zeroY = fy(0); const formArea = form.map((v, i) => `${x(i).toFixed(1)},${fy(v).toFixed(1)}`).join(" "); return ( ); } // --------------------------------------------------------- // Circadian — hourly activity bars, M10 / L5 windows marked // --------------------------------------------------------- function Circadian({ data, w = 320, h = 110 }) { const padB = 16, padT = 8, padX = 6; const hi = Math.max(...data); const bw = (w - padX * 2) / 24; // M10 = most-active 10h window; L5 = least-active 5h function bestWindow(len, most) { let bi = 0, bv = most ? -Infinity : Infinity; for (let i = 0; i + len <= 24; i++) { const s = data.slice(i, i + len).reduce((a, b) => a + b, 0); if (most ? s > bv : s < bv) { bv = s; bi = i; } } return [bi, bi + len]; } const m10 = bestWindow(10, true), l5 = bestWindow(5, false); return ( {data.map((v, i) => { const bh = Math.max(1, (v / hi) * (h - padB - padT)); return ; })} {[0, 6, 12, 18, 24].map((hh) => ( {String(hh).padStart(2, "0")} ))} ); } // --------------------------------------------------------- // StageBar — sleep stages, de-emphasized (lower confidence) // --------------------------------------------------------- function StageBar({ stages, w = 320, h = 24 }) { if (!stages) return null; const order = [["deep", "Deep"], ["rem", "REM"], ["light", "Light"], ["awake", "Awake"]]; const total = order.reduce((a, [k]) => a + stages[k], 0); let x = 0; const shades = { deep: "var(--ink-2)", rem: "var(--ink-3)", light: "var(--ink-4)", awake: "var(--line-strong)" }; return ( {order.map(([k]) => { const bw = (stages[k] / total) * w; const seg = ; x += bw; return seg; })} ); } // --------------------------------------------------------- // StrainBar — value within personal target band // --------------------------------------------------------- function StrainBar({ value, target, max = 21, w = 280, h = 30 }) { const x = (v) => (v / max) * w; return ( {value != null && ( <> )} ); } // --------------------------------------------------------- // RecoveryDist (Variation A) — probability density of the score // Communicates uncertainty as a distribution. Wider = less sure. // --------------------------------------------------------- function RecoveryDist({ value, band, w = 300, h = 116, status = "within" }) { const padX = 6, padB = 20, top = 8; const sigma = band; // ±band ~ 1 sigma const x = (v) => lerp(padX, w - padX, v / 100); const g = (v) => Math.exp(-0.5 * Math.pow((v - value) / sigma, 2)); const N = 120; const xs = Array.from({ length: N + 1 }, (_, i) => (i / N) * 100); const peak = 1; const yArea = h - padB; const yTop = top; const yy = (gv) => lerp(yArea, yTop, gv / peak); const curve = xs.map((v) => `${x(v).toFixed(1)},${yy(g(v)).toFixed(1)}`); const areaIn = xs.filter((v) => v >= value - band && v <= value + band); const inPath = `M ${x(value - band).toFixed(1)},${yArea} ` + areaIn.map((v) => `L ${x(v).toFixed(1)},${yy(g(v)).toFixed(1)}`).join(" ") + ` L ${x(value + band).toFixed(1)},${yArea} Z`; const fullArea = `M ${x(0)},${yArea} ` + curve.map((c) => `L ${c}`).join(" ") + ` L ${x(100)},${yArea} Z`; const col = STATUS_VAR[status]; return ( {[0, 25, 50, 75, 100].map((t) => ( {t} ))} {/* ± band ticks */} {[value - band, value + band].map((v, i) => ( ))} ); } // --------------------------------------------------------- // RecoveryBand (Variation B) — 0–100 track + ± interval + typical marker // --------------------------------------------------------- function RecoveryBand({ value, band, typical = 64, w = 300, h = 56, status = "within" }) { const padX = 8, trackY = 26; const x = (v) => lerp(padX, w - padX, v / 100); const col = STATUS_VAR[status]; return ( {[0, 25, 50, 75, 100].map((t) => ( {t} ))} {/* personal typical recovery marker */} {/* ± interval */} ); } // --------------------------------------------------------- // RecoveryArc (Variation C) — gauge with uncertainty arc // --------------------------------------------------------- function RecoveryArc({ value, band, w = 168, h = 110, status = "within" }) { const cx = w / 2, cy = h - 12, r = 70; const a0 = Math.PI, a1 = 0; // 180deg sweep const ang = (v) => lerp(a0, a1, v / 100); const pt = (v, rr = r) => [cx + rr * Math.cos(ang(v)), cy + rr * Math.sin(ang(v)) * -1]; function arc(v0, v1, rr) { const [x0, y0] = pt(v0, rr), [x1, y1] = pt(v1, rr); const large = Math.abs(ang(v1) - ang(v0)) > Math.PI ? 1 : 0; return `M ${x0.toFixed(1)} ${y0.toFixed(1)} A ${rr} ${rr} 0 ${large} 1 ${x1.toFixed(1)} ${y1.toFixed(1)}`; } const col = STATUS_VAR[status]; const [nx, ny] = pt(value); return ( ); } Object.assign(window, { Spark, SignalSpark, TrendChart, FFF, Circadian, StageBar, StrainBar, RecoveryDist, RecoveryBand, RecoveryArc, }); })();