// SVG chart primitives — no external libs. // Uses CSS vars for colors so light/dark works automatically. const Sparkline = ({ data, w = 120, h = 36, stroke = "var(--primary)", fill = "none", area = false, strokeWidth = 1.5 }) => { if (!data || data.length < 2) return null; const min = Math.min(...data), max = Math.max(...data), range = max - min || 1; const stepX = w / (data.length - 1); const pts = data.map((v, i) => [i * stepX, h - ((v - min) / range) * (h - 4) - 2]); const d = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(" "); const areaD = area ? `${d} L ${w} ${h} L 0 ${h} Z` : null; return ( {areaD && } ); }; const LineChart = ({ series, w = 600, h = 220, labels, colors = ["var(--primary)", "var(--accent)"], showGrid = true, showAxes = true, yFormat = (v) => v }) => { // series: [{name, data:[..]}, ...] const pad = { t: 12, r: 12, b: 24, l: 40 }; const iw = w - pad.l - pad.r, ih = h - pad.t - pad.b; const allVals = series.flatMap(s => s.data); const min = Math.min(...allVals, 0), max = Math.max(...allVals); const range = max - min || 1; const len = series[0].data.length; const xStep = iw / (len - 1); const yTicks = 4; return ( {showGrid && Array.from({ length: yTicks + 1 }).map((_, i) => { const y = pad.t + (ih / yTicks) * i; return ; })} {showAxes && Array.from({ length: yTicks + 1 }).map((_, i) => { const y = pad.t + (ih / yTicks) * i; const v = max - (range / yTicks) * i; return {yFormat(Math.round(v))}; })} {showAxes && labels && labels.map((l, i) => { if (labels.length > 12 && i % 2 !== 0) return null; const x = pad.l + i * xStep; return {l}; })} {series.map((s, si) => { const color = colors[si % colors.length]; const pts = s.data.map((v, i) => [pad.l + i * xStep, pad.t + ih - ((v - min) / range) * ih]); const d = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(" "); const areaD = `${d} L ${pad.l + iw} ${pad.t + ih} L ${pad.l} ${pad.t + ih} Z`; return ( {si === 0 && } {pts.map((p, i) => )} ); })} ); }; const BarChart = ({ data, w = 600, h = 220, labels, color = "var(--primary)", accent, horizontal = false, showValue = false, format = (v) => v }) => { const pad = horizontal ? { t: 8, r: 40, b: 8, l: 120 } : { t: 12, r: 12, b: 26, l: 40 }; const iw = w - pad.l - pad.r, ih = h - pad.t - pad.b; const max = Math.max(...data); if (horizontal) { const barH = Math.min(22, (ih / data.length) - 6); return ( {data.map((v, i) => { const y = pad.t + i * (barH + 8); const barW = (v / max) * iw; return ( {labels[i]} {format(v)} ); })} ); } const barW = iw / data.length * 0.7; const gap = iw / data.length * 0.3; return ( {Array.from({ length: 5 }).map((_, i) => { const y = pad.t + (ih / 4) * i; return ; })} {data.map((v, i) => { const x = pad.l + i * (barW + gap) + gap / 2; const barHeight = (v / max) * ih; const y = pad.t + ih - barHeight; return ( {labels && {labels[i]}} {showValue && {format(v)}} ); })} ); }; const Donut = ({ data, w = 160, colors = ["var(--primary)", "var(--accent)", "var(--info)", "var(--ok)", "oklch(0.55 0.14 300)"], thickness = 22, centerLabel, centerSub }) => { const r = w / 2 - 4; const inner = r - thickness; const cx = w / 2, cy = w / 2; const total = data.reduce((s, d) => s + d.value, 0); let angle = -Math.PI / 2; const arcs = data.map((d, i) => { const frac = d.value / total; const nextAngle = angle + frac * Math.PI * 2; const large = frac > 0.5 ? 1 : 0; const x1 = cx + r * Math.cos(angle), y1 = cy + r * Math.sin(angle); const x2 = cx + r * Math.cos(nextAngle), y2 = cy + r * Math.sin(nextAngle); const x3 = cx + inner * Math.cos(nextAngle), y3 = cy + inner * Math.sin(nextAngle); const x4 = cx + inner * Math.cos(angle), y4 = cy + inner * Math.sin(angle); const path = `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2} L ${x3} ${y3} A ${inner} ${inner} 0 ${large} 0 ${x4} ${y4} Z`; angle = nextAngle; return { path, color: colors[i % colors.length], label: d.label, value: d.value }; }); return (
{arcs.map((a, i) => )} {centerLabel && {centerLabel}} {centerSub && {centerSub}}
{arcs.map((a, i) => (
{a.label} {((a.value / total) * 100).toFixed(1)}%
))}
); }; const Gauge = ({ value, max = 100, w = 160, label, sub, color = "var(--primary)" }) => { const r = 60; const cx = w / 2, cy = 80; const startA = Math.PI, endA = 2 * Math.PI; const arcAngle = startA + (value / max) * Math.PI; const p = (a) => [cx + r * Math.cos(a), cy + r * Math.sin(a)]; const [x1, y1] = p(startA); const [x2, y2] = p(endA); const [xv, yv] = p(arcAngle); return ( {value}{typeof value === 'number' ? '%' : ''} {sub} ); }; const Heatmap = ({ rows, cols, data, w = 600, cellH = 22, rowLabels, colLabels, format = (v) => v }) => { // data: 2d array [rows][cols] const max = Math.max(...data.flat()); const min = Math.min(...data.flat()); const cellW = (w - 90) / cols; return ( {colLabels && colLabels.map((l, i) => ( {l} ))} {data.map((row, ri) => ( {rowLabels && {rowLabels[ri]}} {row.map((v, ci) => { const intensity = (v - min) / (max - min || 1); const bg = `oklch(${0.98 - intensity * 0.55} ${0.04 + intensity * 0.12} 258)`; const txtColor = intensity > 0.55 ? 'white' : 'var(--ink-2)'; return ( {format(v)} ); })} ))} ); }; Object.assign(window, { Sparkline, LineChart, BarChart, Donut, Gauge, Heatmap });