1
2(function (global) {
3 'use strict';
4
5 function cumulativeSeries(transactions) {
6 let running = 0;
7 return [...transactions]
8 .sort((a, b) => a.date.localeCompare(b.date))
9 .map(tx => ({ date: tx.date, value: Math.round((running += Number(tx.amount || 0)) * 100) / 100 }));
10 }
11
12 function scalePoints(series, width, height, padding) {
13 const pad = padding == null ? 28 : padding;
14 if (!series.length) return [];
15 const values = series.map(p => p.value);
16 const min = Math.min(...values, 0);
17 const max = Math.max(...values, 0);
18 const range = max - min || 1;
19 return series.map((p, i) => ({
20 ...p,
21 x: pad + (series.length === 1 ? 0 : (i / (series.length - 1)) * (width - pad * 2)),
22 y: height - pad - ((p.value - min) / range) * (height - pad * 2)
23 }));
24 }
25
26 function drawSpendingChart(canvas, transactions, options) {
27 if (!canvas || !canvas.getContext) return;
28 const opts = options || {};
29 const ctx = canvas.getContext('2d');
30 const dpr = global.devicePixelRatio || 1;
31 const rect = canvas.getBoundingClientRect();
32 const width = Math.max(320, rect.width || canvas.clientWidth || 640);
33 const height = Math.max(220, rect.height || canvas.clientHeight || 260);
34 canvas.width = width * dpr;
35 canvas.height = height * dpr;
36 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
37 ctx.clearRect(0, 0, width, height);
38
39 const dark = opts.dark;
40 const fg = dark ? '#dbeafe' : '#172033';
41 const muted = dark ? '#64748b' : '#94a3b8';
42 const line = dark ? '#38bdf8' : '#2563eb';
43 const fill = dark ? 'rgba(56, 189, 248, .16)' : 'rgba(37, 99, 235, .12)';
44 const series = cumulativeSeries(transactions);
45 const points = scalePoints(series, width, height, 34);
46
47 ctx.strokeStyle = muted;
48 ctx.lineWidth = 1;
49 ctx.beginPath();
50 ctx.moveTo(34, height - 34);
51 ctx.lineTo(width - 16, height - 34);
52 ctx.moveTo(34, 14);
53 ctx.lineTo(34, height - 34);
54 ctx.stroke();
55
56 if (!points.length) {
57 ctx.fillStyle = fg;
58 ctx.font = '14px system-ui';
59 ctx.fillText('No transactions to chart', 48, 64);
60 return;
61 }
62
63 ctx.beginPath();
64 points.forEach((p, i) => i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y));
65 ctx.strokeStyle = line;
66 ctx.lineWidth = 3;
67 ctx.stroke();
68
69 ctx.lineTo(points[points.length - 1].x, height - 34);
70 ctx.lineTo(points[0].x, height - 34);
71 ctx.closePath();
72 ctx.fillStyle = fill;
73 ctx.fill();
74
75 ctx.fillStyle = fg;
76 ctx.font = '12px system-ui';
77 ctx.fillText('Balance over time', 42, 28);
78 ctx.fillText('$' + points[points.length - 1].value.toLocaleString(), width - 110, 28);
79 }
80
81 const api = { cumulativeSeries, scalePoints, drawSpendingChart };
82 if (typeof module !== 'undefined' && module.exports) module.exports = api;
83 global.FinanceCharts = api;
84})(typeof window !== 'undefined' ? window : globalThis);
85
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.