// AI-MONITOR · Reusable SVG chart primitives.
// All charts render from a numeric array (rolling history buffer).
const { useEffect, useMemo, useRef, useState, useLayoutEffect } = React;
// ── useElementSize ─────────────────────────────────────────────────────
function useElementSize() {
const ref = useRef(null);
const [size, setSize] = useState({ w: 0, h: 0 });
useLayoutEffect(() => {
if (!ref.current) return;
const ro = new ResizeObserver((entries) => {
const e = entries[0];
if (e) setSize({ w: e.contentRect.width, h: e.contentRect.height });
});
ro.observe(ref.current);
setSize({ w: ref.current.clientWidth, h: ref.current.clientHeight });
return () => ro.disconnect();
}, []);
return [ref, size];
}
// ── Sparkline (no axis, no labels — pure shape) ────────────────────────
function Sparkline({ data, height = 32, color = "var(--accent)", fill = true, area = true, smooth = true, style }) {
const [ref, { w, h }] = useElementSize();
const gradId = useMemo(() => "g" + Math.random().toString(36).slice(2, 9), []);
const H = h || height;
if (!data || data.length < 2 || !w) {
return
;
}
const min = Math.min(...data);
const max = Math.max(...data);
const span = max - min || 1;
const stepX = w / (data.length - 1);
const yOf = (v) => H - 6 - ((v - min) / span) * (H - 12);
// Build smooth path with catmull-rom-to-bezier
let d = "";
if (smooth) {
const pts = data.map((v, i) => [i * stepX, yOf(v)]);
d = `M ${pts[0][0]} ${pts[0][1]}`;
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[i - 1] || pts[i];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[i + 2] || p2;
const c1x = p1[0] + (p2[0] - p0[0]) / 6;
const c1y = p1[1] + (p2[1] - p0[1]) / 6;
const c2x = p2[0] - (p3[0] - p1[0]) / 6;
const c2y = p2[1] - (p3[1] - p1[1]) / 6;
d += ` C ${c1x} ${c1y}, ${c2x} ${c2y}, ${p2[0]} ${p2[1]}`;
}
} else {
d = data.map((v, i) => `${i ? "L" : "M"} ${i * stepX} ${yOf(v)}`).join(" ");
}
const last = data[data.length - 1];
const fillD = area ? `${d} L ${w} ${H} L 0 ${H} Z` : null;
return (
);
}
// ── Ring gauge ─────────────────────────────────────────────────────────
function Ring({ value = 0, max = 100, size = 70, stroke = 6, color, label, sub }) {
const v = Math.max(0, Math.min(max, value));
const r = (size - stroke) / 2;
const c = 2 * Math.PI * r;
const off = c * (1 - v / max);
const auto = color || (v / max > 0.85 ? "var(--bad)" : v / max > 0.7 ? "var(--hot)" : v / max > 0.4 ? "var(--accent)" : "var(--ok)");
return (