// SPDX-License-Identifier: GPL-3.0-or-later
/* === SVG charts: score sparkline + severity bars — premium, compact, diagnostic === */
function fmtWhen(at) {
const d = new Date(at);
if (isNaN(d.getTime())) return "";
const p = (n) => String(n).padStart(2, "0");
return (d.getMonth() + 1) + "/" + d.getDate() + " " + p(d.getHours()) + ":" + p(d.getMinutes());
}
/* Tiny inline sparkline for the hero meta row */
function miniSpark(data) {
const W = 80, H = 18, pad = 2;
const scores = data.map((r) => r.score);
if (scores.length < 2) return "";
const min = Math.min(...scores), max = Math.max(...scores);
const span = Math.max(1, max - min);
const X = (i) => pad + (i * (W - 2 * pad)) / (scores.length - 1);
const Y = (s) => H - pad - ((s - min) / span) * (H - 2 * pad);
const line = scores.map((s, i) => X(i).toFixed(1) + "," + Y(s).toFixed(1)).join(" ");
const last = scores[scores.length - 1], first = scores[0];
const color = last > first ? "var(--green)" : last < first ? "var(--red)" : "var(--muted2)";
return '';
}
function scoreChart(data) {
const W = 240, H = 34, pad = 4;
const scores = data.map((r) => r.score);
const min = Math.min(...scores), max = Math.max(...scores);
const span = Math.max(1, max - min);
/* A flat series has no shape to show; pin it to the middle of the trace
instead of drawing a hairline along the floor, which reads as a border.
"Middle" is the score's own height though: a flat 100 belongs at the top
of the plot, a flat 60 just above the floor. */
const flat = max === min;
const X = (i) => pad + (i * (W - 2 * pad)) / (scores.length - 1);
const Y = (s) => (flat
? H - pad - (Math.max(0, Math.min(100, s)) / 100) * (H - 2 * pad)
: H - pad - ((s - min) / span) * (H - 2 * pad));
const pts = scores.map((s, i) => [X(i).toFixed(1), Y(s).toFixed(1)]);
const last = scores[scores.length - 1];
const first = scores[0];
const dir = last > first ? { txt: "Improving", icon: "↗", cls: "up", col: "var(--green)" }
: last < first ? { txt: "Declining", icon: "↘", cls: "down", col: "var(--red)" }
: { txt: "Stable", icon: "→", cls: "flat", col: "var(--muted2)" };
const range = scores.length + " runs · " + first + " → " + last;
const line = pts.map((p) => p.join(",")).join(" ");
const area = "M" + line.replace(/ /g, " L") + " L" + (W - pad) + "," + (H - pad) + " L" + pad + "," + (H - pad) + " Z";
const dots = pts.map((p, i) =>
// A flat series has no shape, so each run gets a visible mark: twelve
// readings in a row read as twelve readings, not as a stray line. The
// mark is decorative; the transparent circle on top is the control.
(flat ? '