/* Charts: small SVG components — sparkline, ring, bar, multi-line stream */
const SparkLine = ({ data, width = 120, height = 32, color = "var(--accent)", fill = true, dots = false, fluid = false }) => {
if (!data || data.length < 2) return null;
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;
// Coordinate space stays at `width`; cuando fluid=true el SVG se estira al
// 100% del contenedor vía CSS (preserveAspectRatio="none"). Eso evita el
// medio-sparkline en cards que crecen con el viewport.
const step = width / (data.length - 1);
const points = data.map((d, i) => [i * step, height - ((d - min) / range) * (height - 4) - 2]);
const path = points.map((p, i) => `${i === 0 ? "M" : "L"}${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(" ");
const area = `${path} L${width} ${height} L0 ${height} Z`;
const svgProps = fluid
? { width: "100%", height, viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "none", style: { display: "block" } }
: { width, height, viewBox: `0 0 ${width} ${height}`, style: { overflow: "visible" } };
return (
);
};
const Ring = ({ value = 0, max = 100, size = 64, stroke = 6, color = "var(--accent)", track = "var(--line)", label, sublabel }) => {
const r = (size - stroke) / 2;
const c = 2 * Math.PI * r;
const pct = Math.min(1, Math.max(0, value / max));
return (
{(label || sublabel) && (
{label &&
{label}
}
{sublabel &&
{sublabel}
}
)}
);
};
const Bars = ({ data, width = 280, height = 60, color = "var(--accent)", labels }) => {
if (!data) return null;
const max = Math.max(...data, 1);
const gap = 3;
const bw = (width - gap * (data.length - 1)) / data.length;
return (
);
};
/* Larger time-series area chart with grid */
const AreaChart = ({ data, width = 600, height = 200, color = "var(--accent)", yLabel, xLabels = [], yMin, yMax, secondary }) => {
if (!data || data.length < 2) return null;
const padL = 32, padR = 8, padT = 12, padB = 24;
const w = width - padL - padR;
const h = height - padT - padB;
const min = yMin !== undefined ? yMin : Math.min(...data);
const max = yMax !== undefined ? yMax : Math.max(...data);
const range = max - min || 1;
const step = w / (data.length - 1);
const pts = data.map((d, i) => [padL + i * step, padT + h - ((d - min) / range) * h]);
const path = pts.map((p, i) => `${i === 0 ? "M" : "L"}${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(" ");
const area = `${path} L${padL + w} ${padT + h} L${padL} ${padT + h} Z`;
const yTicks = 4;
const ticks = Array.from({ length: yTicks + 1 }, (_, i) => min + (range * i) / yTicks);
return (
);
};
/* Multi-line stream (HR, pace, elevation).
*
* Cada serie acepta `{ data, color, fill?, label?, axis?, format? }`:
* - `axis`: "left" | "right" — decide a qué costado va la escala (min/max).
* - `format(v)`: opcional — cómo formatear el min/max para esa serie.
* (ej. pace negativo se invierte para mostrar pace real).
* - `label`: corto, mostrado debajo del valor (ej. "FC", "Pace").
*
* Si una serie no declara `axis`, no aparece etiquetada (compat hacia atrás).
*/
const StreamChart = ({ series, width = 800, height = 220, xMax = 100 }) => {
const padL = 56, padR = 56, padT = 14, padB = 22;
const w = width - padL - padR;
const h = height - padT - padB;
const axisSeries = (side) => series.filter((s) => s.axis === side);
const fmtNum = (v) => {
if (v == null || !isFinite(v)) return "—";
if (Math.abs(v) >= 100) return Math.round(v).toString();
if (Math.abs(v) >= 10) return v.toFixed(1);
return v.toFixed(2);
};
const renderAxis = (side) => {
const ss = axisSeries(side);
if (!ss.length) return null;
const x = side === "left" ? padL - 8 : padL + w + 8;
const anchor = side === "left" ? "end" : "start";
return ss.map((s, idx) => {
const min = Math.min(...s.data);
const max = Math.max(...s.data);
const fmt = s.format || fmtNum;
// Bloque vertical por serie: max arriba, label, min abajo.
const blockTop = padT + idx * 30;
return (
{fmt(max)}
{s.label && (
{s.label}
)}
{fmt(min)}
);
});
};
return (
);
};
/* Heatmap-ish weekly grid */
const WeekGrid = ({ data, max, color = "var(--accent)", labels = ["L","M","X","J","V","S","D"] }) => {
return (
{data.map((d, i) => {
const o = Math.max(0.08, d / max);
return (
);
})}
);
};
Object.assign(window, { SparkLine, Ring, Bars, AreaChart, StreamChart, WeekGrid });