/* global React */ /* Charts: Sparkline, Line, Bar, Area, KPI metrics — all SVG, no deps */ const { useMemo, useState, useRef, useEffect } = React; function useMeasure() { const ref = useRef(null); const [size, setSize] = useState({ w: 0, h: 0 }); useEffect(() => { if (!ref.current) return; const ro = new ResizeObserver((entries) => { for (const e of entries) { const cr = e.contentRect; setSize({ w: Math.round(cr.width), h: Math.round(cr.height) }); } }); ro.observe(ref.current); return () => ro.disconnect(); }, []); return [ref, size]; } function _scale(values, w, h, pad = 0) { const min = Math.min(...values); const max = Math.max(...values); const r = max - min || 1; return values.map((v, i) => [ (i / (values.length - 1)) * (w - pad * 2) + pad, h - pad - ((v - min) / r) * (h - pad * 2), ]); } function Sparkline({ data, w = 120, h = 36, color = "currentColor", area = true, stroke = 1.5 }) { const pts = useMemo(() => _scale(data, w, h, 2), [data, w, h]); const d = pts.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`).join(" "); const areaD = `${d} L${w - 2},${h - 2} L2,${h - 2} Z`; return ( {area && } ); } /* ---------- LineChart ---------- */ function LineChart({ series = [], labels = [], w: wIn = 720, h: hIn = 280, gradient = true, yTicks = 4, fill = false }) { const [ref, size] = useMeasure(); const w = fill && size.w ? size.w : wIn; const h = fill && size.h ? size.h : hIn; const pad = { t: 16, r: 16, b: 28, l: 40 }; const cw = w - pad.l - pad.r; const ch = h - pad.t - pad.b; const all = series.flatMap(s => s.data); const min = Math.min(0, ...all); const max = Math.max(...all); const r = max - min || 1; const x = (i, n) => pad.l + (i / (n - 1)) * cw; const y = (v) => pad.t + ch - ((v - min) / r) * ch; const ticks = useMemo(() => { const arr = []; for (let i = 0; i <= yTicks; i++) { const v = min + (r * i / yTicks); arr.push({ y: y(v), v }); } return arr; }, [min, max, ch, yTicks]); const colors = ["var(--ink)", "var(--grad-4)", "var(--grad-1)", "var(--grad-2)"]; const svg = ( {/* y grid */} {ticks.map((t, i) => ( {t.v >= 1000 ? (t.v / 1000).toFixed(0) + "K" : Math.round(t.v)} ))} {/* x labels */} {labels.map((l, i) => ( {l} ))} {/* series */} {series.map((s, si) => { const pts = s.data.map((v, i) => [x(i, s.data.length), y(v)]); const d = pts.map(([px, py], i) => `${i === 0 ? "M" : "L"}${px.toFixed(1)},${py.toFixed(1)}`).join(" "); const fillD = `${d} L${pts[pts.length - 1][0].toFixed(1)},${pad.t + ch} L${pts[0][0].toFixed(1)},${pad.t + ch} Z`; const stroke = si === 0 && gradient ? "url(#lineGrad)" : (s.color || colors[si % colors.length]); return ( {si === 0 && gradient && } ); })} ); if (!fill) return svg; return
{size.w ? svg : null}
; } /* ---------- BarChart ---------- */ function BarChart({ data = [], labels = [], w: wIn = 720, h: hIn = 240, color = "var(--ink)", stacked, fill = false }) { const [ref, size] = useMeasure(); const w = fill && size.w ? size.w : wIn; const h = fill && size.h ? size.h : hIn; const pad = { t: 12, r: 12, b: 28, l: 36 }; const cw = w - pad.l - pad.r; const ch = h - pad.t - pad.b; const max = Math.max(...data.flat()); const groupW = cw / data.length; const barW = Math.min(groupW * 0.55, 32); const svg = ( {/* baseline */} {data.map((v, i) => { const cx = pad.l + groupW * i + groupW / 2; const bh = (v / max) * ch; const isLast = i === data.length - 1; return ( {labels[i]} ); })} ); if (!fill) return svg; return
{size.w ? svg : null}
; } /* ---------- AreaChart ---------- */ function AreaChart({ data = [], labels = [], w = 720, h = 200 }) { const pad = { t: 8, r: 8, b: 24, l: 36 }; const cw = w - pad.l - pad.r; const ch = h - pad.t - pad.b; const max = Math.max(...data); const min = Math.min(0, ...data); const r = max - min || 1; const pts = data.map((v, i) => [ pad.l + (i / (data.length - 1)) * cw, pad.t + ch - ((v - min) / r) * ch, ]); const d = pts.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)},${y.toFixed(1)}`).join(" "); const fillD = `${d} L${pad.l + cw},${pad.t + ch} L${pad.l},${pad.t + ch} Z`; return ( {labels.map((l, i) => ( {i % Math.ceil(labels.length / 6) === 0 ? l : ""} ))} ); } /* ---------- KPI Card ---------- */ function KPICard({ label, value, delta, deltaLabel = "vs last 7d", spark, gradient, big }) { const positive = delta != null && delta >= 0; return (
{label} {delta != null && ( {positive ? "▲" : "▼"} {Math.abs(delta).toFixed(1)}% )}
{gradient ? {value} : {value}}
{(spark || deltaLabel) && (
{deltaLabel && {deltaLabel}} {spark && }
)}
); } /* ---------- Stat Block (editorial, bordered top) ---------- */ function StatBlock({ label, value, sub, gradient }) { return (
{gradient ? {value} : value}
{label}
{sub &&
{sub}
}
); } /* ---------- Donut ---------- */ function Donut({ value = 60, size = 120, stroke = 12, color = "var(--brand-grad)", label, sub }) { const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const off = c - (value / 100) * c; return (
{label || `${value}%`} {sub && {sub}}
); } Object.assign(window, { Sparkline, LineChart, BarChart, AreaChart, KPICard, StatBlock, Donut });