// Lightweight data viz primitives (pure SVG, no deps) function Sparkline({ data, width = 110, height = 30, color = "var(--brand)", fill = true }) { if (!data || !data.length) return null; const min = Math.min(...data); const max = Math.max(...data); const range = max - min || 1; const pts = data.map((v, i) => { const x = (i / (data.length - 1)) * (width - 2) + 1; const y = height - 2 - ((v - min) / range) * (height - 4); return [x, y]; }); const d = pts.map(([x, y], i) => (i === 0 ? `M${x} ${y}` : `L${x} ${y}`)).join(" "); const area = `${d} L${width - 1} ${height - 1} L1 ${height - 1} Z`; return ( {fill && ( )} ); } function Bars({ data, width = 520, height = 140, color = "var(--brand)", labels }) { const max = Math.max(...data); const bw = (width - 20) / data.length; const gap = 3; return ( {[0.25, 0.5, 0.75, 1].map((p) => ( ))} {data.map((v, i) => { const h = ((v / max) * (height - 30)); const x = 10 + i * bw + gap / 2; const y = height - 20 - h; return ( ); })} {labels && labels.map((l, i) => ( {l} ))} ); } function StackedArea({ seriesA, seriesB, width = 900, height = 220, labels }) { const combined = seriesA.map((a, i) => a + (seriesB[i] || 0)); const max = Math.max(...combined) * 1.08; const w = width; const h = height; const pad = { t: 12, r: 12, b: 22, l: 40 }; const iw = w - pad.l - pad.r; const ih = h - pad.t - pad.b; const pt = (v, i, offset = 0) => { const x = pad.l + (i / (seriesA.length - 1)) * iw; const y = pad.t + ih - ((v + offset) / max) * ih; return [x, y]; }; const areaPath = (arr, offsetArr = null) => { const top = arr.map((v, i) => pt(v, i, offsetArr ? offsetArr[i] : 0)); const bottomOffset = offsetArr || new Array(arr.length).fill(0); const bottom = bottomOffset.map((v, i) => pt(0, i, v)).reverse(); return [...top, ...bottom].map(([x, y], i) => (i === 0 ? `M${x} ${y}` : `L${x} ${y}`)).join(" ") + " Z"; }; const linePath = (arr, offsetArr = null) => arr .map((v, i) => pt(v, i, offsetArr ? offsetArr[i] : 0)) .map(([x, y], i) => (i === 0 ? `M${x} ${y}` : `L${x} ${y}`)) .join(" "); const gridTicks = 4; return ( {Array.from({ length: gridTicks + 1 }).map((_, i) => { const y = pad.t + (i / gridTicks) * ih; const v = max - (i / gridTicks) * max; return ( {v >= 1_000_000 ? (v / 1_000_000).toFixed(1) + "M" : v >= 1_000 ? (v / 1_000).toFixed(0) + "k" : v.toFixed(0)} ); })} v + seriesA[i]))} stroke="var(--violet)" strokeWidth="1.6" fill="none" /> {labels && labels.map((l, i) => { if (i % Math.ceil(labels.length / 8) !== 0 && i !== labels.length - 1) return null; const x = pad.l + (i / (labels.length - 1)) * iw; return {l}; })} ); } function Donut({ segments, size = 140, thickness = 18 }) { const total = segments.reduce((a, s) => a + s.value, 0); const r = (size - thickness) / 2; const c = size / 2; const circ = 2 * Math.PI * r; let offset = 0; return ( {segments.map((s, i) => { const frac = s.value / total; const len = circ * frac; const el = ( ); offset += len; return el; })} ); } window.Sparkline = Sparkline; window.Bars = Bars; window.StackedArea = StackedArea; window.Donut = Donut;