// 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 (
{/* 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 (
);
}
// ── 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 (
{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 });