// Custom SVG charts
// Smooth path generator
function smoothPath(points) {
if (points.length < 2) return '';
let d = `M ${points[0].x} ${points[0].y}`;
for (let i = 0; i < points.length - 1; i++) {
const p0 = points[Math.max(0, i - 1)];
const p1 = points[i];
const p2 = points[i + 1];
const p3 = points[Math.min(points.length - 1, i + 2)];
const cp1x = p1.x + (p2.x - p0.x) / 6;
const cp1y = p1.y + (p2.y - p0.y) / 6;
const cp2x = p2.x - (p3.x - p1.x) / 6;
const cp2y = p2.y - (p3.y - p1.y) / 6;
d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2.x} ${p2.y}`;
}
return d;
}
// --- BarChart: monthly savings
function SavingsChart({ data = [], height = 240 }) {
const padL = 56, padR = 16, padT = 24, padB = 36;
const ref = useRef(null);
const [width, setW] = useState(640);
useLayoutEffect(() => {
if (!ref.current) return;
const ro = new ResizeObserver(entries => setW(Math.max(320, entries[0].contentRect.width)));
ro.observe(ref.current); return () => ro.disconnect();
}, []);
const max = Math.max(...data.map(d => d.value)) * 1.15;
const innerW = width - padL - padR;
const innerH = height - padT - padB;
const bw = innerW / data.length;
return (
);
}
// --- Donut chart: category share
function DonutChart({ data = [], size = 220, thick = 26, onArcClick, centerLabel = 'Total 30d' }) {
const total = data.reduce((s, d) => s + d.value, 0);
const r = (size - thick) / 2;
const cx = size / 2, cy = size / 2;
let acc = 0;
const arcs = data.map((d, i) => {
const start = acc;
const end = acc + d.value / total;
acc = end;
const a1 = start * 2 * Math.PI - Math.PI / 2;
const a2 = end * 2 * Math.PI - Math.PI / 2;
const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
const x2 = cx + r * Math.cos(a2), y2 = cy + r * Math.sin(a2);
const large = end - start > 0.5 ? 1 : 0;
return { d: `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`, tone: d.tone, label: d.label, value: d.value, raw: d };
});
const colorMap = {
emerald: 'oklch(0.78 0.16 160)',
coral: 'oklch(0.74 0.18 25)',
sky: 'oklch(0.78 0.13 230)',
amber: 'oklch(0.84 0.14 78)',
violet: 'oklch(0.74 0.14 285)',
};
const clickable = !!onArcClick;
return (
{centerLabel}
{window.SCP.BRLk(total)}
);
}
// --- Line chart: price evolution
function LineChart({ data = [], height = 180, color = 'oklch(0.78 0.16 160)' }) {
const padL = 44, padR = 16, padT = 16, padB = 28;
const ref = useRef(null);
const [width, setW] = useState(560);
useLayoutEffect(() => {
if (!ref.current) return;
const ro = new ResizeObserver(entries => setW(Math.max(280, entries[0].contentRect.width)));
ro.observe(ref.current); return () => ro.disconnect();
}, []);
const min = Math.min(...data.map(d => d.v));
const max = Math.max(...data.map(d => d.v));
const span = max - min || 1;
const innerW = width - padL - padR;
const innerH = height - padT - padB;
const pts = data.map((d, i) => ({
x: padL + (i / (data.length - 1)) * innerW,
y: padT + (1 - (d.v - min) / span) * innerH,
v: d.v, d: d.d
}));
const pathD = smoothPath(pts);
const areaD = pathD + ` L ${pts[pts.length - 1].x} ${padT + innerH} L ${pts[0].x} ${padT + innerH} Z`;
return (
);
}
// Mini sparkline
function Sparkline({ values = [], width = 80, height = 28, color = 'oklch(0.78 0.16 160)' }) {
const min = Math.min(...values);
const max = Math.max(...values);
const span = max - min || 1;
const pts = values.map((v, i) => ({
x: (i / (values.length - 1)) * width,
y: height - ((v - min) / span) * height * 0.8 - height * 0.1,
}));
return (
);
}
// inject chart css
(function() {
if (document.getElementById('scp-charts-css')) return;
const s = document.createElement('style'); s.id = 'scp-charts-css';
s.textContent = `
.scp-donut { position: relative; display: inline-block; }
.scp-donut-center {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; pointer-events: none;
}
.scp-donut-center .lbl { font-size: 10px; color: var(--fg-3); text-transform: uppercase; letter-spacing: .1em; font-weight: 600; }
.scp-donut-center .val { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; margin-top: 2px; font-family: var(--font-sans); font-feature-settings: "tnum"; }
.scp-donut-arc { cursor: pointer; transition: opacity .15s, stroke-width .15s; transform-origin: center; transform-box: fill-box; }
.scp-donut:hover .scp-donut-arc { opacity: 0.55; }
.scp-donut .scp-donut-arc:hover { opacity: 1; stroke-width: 30; }
`;
document.head.appendChild(s);
})();
Object.assign(window, { SavingsChart, DonutChart, LineChart, Sparkline, smoothPath });