// Charts.jsx — Lightweight SVG chart components (no external deps) 'use strict'; // ── Area / Line Chart ───────────────────────────────────────────────────── function SimpleAreaChart({ data, lines, height = 200, xKey = 'month', formatY, formatTooltip }) { const [tooltip, setTooltip] = React.useState(null); const W = 560, H = height, PAD = { top: 10, right: 10, bottom: 28, left: 52 }; const innerW = W - PAD.left - PAD.right; const innerH = H - PAD.top - PAD.bottom; const allVals = lines.flatMap(l => data.map(d => d[l.key] || 0)); const maxVal = Math.max(...allVals, 1); const minVal = 0; const range = maxVal - minVal || 1; const scaleX = i => PAD.left + (i / (data.length - 1)) * innerW; const scaleY = v => PAD.top + innerH - ((v - minVal) / range) * innerH; const makePath = key => { if (data.length < 2) return ''; const pts = data.map((d, i) => [scaleX(i), scaleY(d[key] || 0)]); let d = `M ${pts[0][0]} ${pts[0][1]}`; for (let i = 1; i < pts.length; i++) { const cp1x = (pts[i - 1][0] + pts[i][0]) / 2; d += ` C ${cp1x} ${pts[i - 1][1]}, ${cp1x} ${pts[i][1]}, ${pts[i][0]} ${pts[i][1]}`; } return d; }; const makeArea = key => { const line = makePath(key); if (!line) return ''; const lastX = scaleX(data.length - 1); const baseY = PAD.top + innerH; return `${line} L ${lastX} ${baseY} L ${PAD.left} ${baseY} Z`; }; const yTicks = 4; const yTickVals = Array.from({ length: yTicks + 1 }, (_, i) => minVal + (i / yTicks) * range); const handleMouseMove = (e, svgRef) => { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); const svgX = (e.clientX - rect.left) * (W / rect.width) - PAD.left; const idx = Math.round((svgX / innerW) * (data.length - 1)); const clamped = Math.max(0, Math.min(data.length - 1, idx)); setTooltip({ idx: clamped, x: e.clientX - rect.left, y: e.clientY - rect.top }); }; const svgRef = React.useRef(null); return (
handleMouseMove(e, svgRef)} onMouseLeave={() => setTooltip(null)}> {lines.map(l => ( ))} {/* Grid lines */} {yTickVals.map((v, i) => { const y = scaleY(v); return ( {formatY ? formatY(v) : Math.round(v)} ); })} {/* X axis labels */} {data.map((d, i) => ( {d[xKey]} ))} {/* Area fills */} {lines.map(l => ( ))} {/* Lines */} {lines.map(l => ( ))} {/* Hover indicator */} {tooltip && data[tooltip.idx] && ( {lines.map(l => ( ))} )} {/* Tooltip */} {tooltip && data[tooltip.idx] && (

{data[tooltip.idx][xKey]}

{lines.map(l => (
{l.label}: {formatTooltip ? formatTooltip(data[tooltip.idx][l.key] || 0) : data[tooltip.idx][l.key]}
))}
)}
); } // ── Donut Chart ─────────────────────────────────────────────────────────── function SimpleDonutChart({ data, size = 180 }) { const [hovered, setHovered] = React.useState(null); const cx = size / 2, cy = size / 2; const R = size * 0.38, r = size * 0.24; const total = data.reduce((s, d) => s + (d.value || 0), 0) || 1; let angle = -Math.PI / 2; const arcs = data.map(d => { const sweep = (d.value / total) * 2 * Math.PI; const start = angle; const end = angle + sweep - 0.03; angle += sweep; const x1 = cx + R * Math.cos(start), y1 = cy + R * Math.sin(start); const x2 = cx + R * Math.cos(end), y2 = cy + R * Math.sin(end); const x3 = cx + r * Math.cos(end), y3 = cy + r * Math.sin(end); const x4 = cx + r * Math.cos(start), y4 = cy + r * Math.sin(start); const large = sweep > Math.PI ? 1 : 0; const path = `M ${x1} ${y1} A ${R} ${R} 0 ${large} 1 ${x2} ${y2} L ${x3} ${y3} A ${r} ${r} 0 ${large} 0 ${x4} ${y4} Z`; return { ...d, path, midAngle: start + sweep / 2 }; }); const hovItem = hovered !== null ? data[hovered] : null; return ( {arcs.map((arc, i) => ( setHovered(i)} onMouseLeave={() => setHovered(null)} transform={hovered === i ? `translate(${Math.cos(arc.midAngle) * 3}, ${Math.sin(arc.midAngle) * 3})` : ''}/> ))} {/* Center text */} {hovItem ? hovItem.value : total} {hovItem ? hovItem.name : 'total'} ); } // ── Vertical Bar Chart ──────────────────────────────────────────────────── function SimpleBarChart({ data, xKey, bars, height = 180, formatY, formatTooltip }) { const [tooltip, setTooltip] = React.useState(null); const W = 560, H = height, PAD = { top: 10, right: 10, bottom: 28, left: 52 }; const innerW = W - PAD.left - PAD.right; const innerH = H - PAD.top - PAD.bottom; const allVals = bars.flatMap(b => data.map(d => d[b.key] || 0)); const maxVal = Math.max(...allVals, 1); const scaleY = v => PAD.top + innerH - (v / maxVal) * innerH; const barWidth = (innerW / data.length) / (bars.length + 1); const groupWidth = barWidth * bars.length + barWidth * 0.3; const yTicks = 4; return (
{/* Grid */} {Array.from({ length: yTicks + 1 }, (_, i) => { const v = (i / yTicks) * maxVal; const y = scaleY(v); return ( {formatY ? formatY(v) : Math.round(v)} ); })} {/* Bars */} {data.map((d, gi) => { const groupX = PAD.left + (gi / data.length) * innerW + (innerW / data.length - groupWidth) / 2; return ( setTooltip(gi)} onMouseLeave={() => setTooltip(null)}> {bars.map((b, bi) => { const val = d[b.key] || 0; const bx = groupX + bi * (barWidth + 2); const bh = Math.max(((val / maxVal) * innerH), 2); const by = PAD.top + innerH - bh; return ; })} {d[xKey]} {tooltip === gi && ( )} ); })} {tooltip !== null && data[tooltip] && (

{data[tooltip][xKey]}

{bars.map(b => (
{b.label}: {formatTooltip ? formatTooltip(data[tooltip][b.key] || 0) : data[tooltip][b.key]}
))}
)}
); } // ── Horizontal Bar Chart ────────────────────────────────────────────────── function HorizontalBarChart({ data, valueKey = 'value', labelKey = 'name', color = '#3B82F6', height = 220, formatValue }) { const [hovered, setHovered] = React.useState(null); const maxVal = Math.max(...data.map(d => d[valueKey] || 0), 1); const barH = Math.min(18, (height - data.length * 8) / data.length); return (
{data.map((d, i) => { const pct = ((d[valueKey] || 0) / maxVal) * 100; return (
setHovered(i)} onMouseLeave={() => setHovered(null)}>
{d[labelKey]}
{formatValue ? formatValue(d[valueKey]) : d[valueKey]}
); })}
); } Object.assign(window, { SimpleAreaChart, SimpleDonutChart, SimpleBarChart, HorizontalBarChart });