/* ============================================================
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 (
);
}
// ---------------------------------------------------------
// 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 (
);
}
// ---------------------------------------------------------
// 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 (
);
}
// ---------------------------------------------------------
// 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 (
);
}
// ---------------------------------------------------------
// StrainBar — value within personal target band
// ---------------------------------------------------------
function StrainBar({ value, target, max = 21, w = 280, h = 30 }) {
const x = (v) => (v / max) * w;
return (
);
}
// ---------------------------------------------------------
// 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 (
);
}
// ---------------------------------------------------------
// 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 (
);
}
// ---------------------------------------------------------
// 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,
});
})();