1
2(function (root) {
3 function money(n) { return '$' + Math.round(n).toLocaleString(); }
4
5 function drawSpendingChart(canvas, points, dark) {
6 if (!canvas) return;
7 const ctx = canvas.getContext('2d');
8 const dpr = window.devicePixelRatio || 1;
9 const rect = canvas.getBoundingClientRect();
10 canvas.width = Math.max(320, rect.width) * dpr;
11 canvas.height = 260 * dpr;
12 ctx.scale(dpr, dpr);
13 const w = canvas.width / dpr, h = canvas.height / dpr;
14 const pad = 42;
15 ctx.clearRect(0, 0, w, h);
16 ctx.fillStyle = dark ? '#0f172a' : '#ffffff';
17 ctx.fillRect(0, 0, w, h);
18 ctx.strokeStyle = dark ? '#334155' : '#dbe3ef';
19 ctx.lineWidth = 1;
20 for (let i = 0; i < 5; i++) {
21 const y = pad + (h - pad * 1.6) * (i / 4);
22 ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(w - 16, y); ctx.stroke();
23 }
24 ctx.fillStyle = dark ? '#cbd5e1' : '#475569';
25 ctx.font = '12px system-ui, sans-serif';
26 if (!points.length) {
27 ctx.fillText('No spending data yet', pad, h / 2);
28 return;
29 }
30 const max = Math.max(...points.map(p => p.amount), 10);
31 const xFor = i => pad + (points.length === 1 ? 0 : i * (w - pad - 24) / (points.length - 1));
32 const yFor = v => h - pad - (v / max) * (h - pad * 1.8);
33 ctx.strokeStyle = '#6366f1';
34 ctx.lineWidth = 3;
35 ctx.beginPath();
36 points.forEach((p, i) => { const x = xFor(i), y = yFor(p.amount); i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); });
37 ctx.stroke();
38 ctx.fillStyle = '#22c55e';
39 points.forEach((p, i) => { ctx.beginPath(); ctx.arc(xFor(i), yFor(p.amount), 4, 0, Math.PI * 2); ctx.fill(); });
40 ctx.fillStyle = dark ? '#e2e8f0' : '#1e293b';
41 ctx.fillText(money(max), 6, yFor(max) + 4);
42 ctx.fillText('$0', 14, h - pad + 4);
43 ctx.fillText(points[0].date.slice(5), pad, h - 12);
44 ctx.fillText(points[points.length - 1].date.slice(5), w - 58, h - 12);
45 }
46
47 root.FinanceCharts = { drawSpendingChart };
48})(typeof globalThis !== 'undefined' ? globalThis : this);
49
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.