diff --git a/web/charts.js b/web/charts.js index ad297dd..2a9e026 100644 --- a/web/charts.js +++ b/web/charts.js @@ -37,6 +37,8 @@ // months:[ym], series:[{key,color,values:{ym:amount}}] function monthlyBars(container, months, series, opts) { opts = opts || {}; + const axisLab = opts.labelFn || (m => window.MT.MONTH_LABEL(m).split(" '")[0]); + const fullLab = opts.labelFn || window.MT.MONTH_LABEL; clear(container); const W = container.clientWidth || 800, H = opts.height || 240; const padL = 52, padR = 12, padT = 14, padB = 26; @@ -61,11 +63,11 @@ const x = gx - totalW / 2 + si * (bw + gap); const y = padT + innerH - h; const r = el('rect', { x, y, width: bw, height: Math.max(0, h), rx: 2, fill: s.color, opacity: 0.92, class: 'mbar' }); - r.addEventListener('mousemove', e => window.MTtip && window.MTtip.raw(e, s.label + ' · ' + window.MT.MONTH_LABEL(m), fmtFull(v), s.color)); + r.addEventListener('mousemove', e => window.MTtip && window.MTtip.raw(e, s.label + ' · ' + fullLab(m), fmtFull(v), s.color)); r.addEventListener('mouseleave', () => window.MTtip && window.MTtip.hide()); svg.appendChild(r); }); - svg.appendChild(el('text', { x: gx, y: H - 8, 'text-anchor': 'middle', class: 'chart-axis' }, window.MT.MONTH_LABEL(m).split(" '")[0])); + svg.appendChild(el('text', { x: gx, y: H - 8, 'text-anchor': 'middle', class: 'chart-axis' }, axisLab(m))); }); container.appendChild(svg); } @@ -73,6 +75,8 @@ // ---------- line / area (cumulative net) ---------- function lineChart(container, months, valuesByMonth, opts) { opts = opts || {}; + const axisLab = opts.labelFn || (m => window.MT.MONTH_LABEL(m).split(" '")[0]); + const fullLab = opts.labelFn || window.MT.MONTH_LABEL; clear(container); const W = container.clientWidth || 800, H = opts.height || 200; const padL = 56, padR = 14, padT = 14, padB = 26; @@ -105,10 +109,10 @@ svg.appendChild(el('path', { d: dPath, fill: 'none', stroke: col, 'stroke-width': 2.4, 'stroke-linejoin': 'round' })); pts.forEach((v, i) => { const dot = el('circle', { cx: x(i), cy: y(v), r: 3.4, fill: cssv('--bg'), stroke: col, 'stroke-width': 2 }); - dot.addEventListener('mousemove', e => window.MTtip && window.MTtip.raw(e, window.MT.MONTH_LABEL(months[i]), 'running ' + fmtFull(v), col)); + dot.addEventListener('mousemove', e => window.MTtip && window.MTtip.raw(e, fullLab(months[i]), 'running ' + fmtFull(v), col)); dot.addEventListener('mouseleave', () => window.MTtip && window.MTtip.hide()); svg.appendChild(dot); - svg.appendChild(el('text', { x: x(i), y: H - 8, 'text-anchor': 'middle', class: 'chart-axis' }, window.MT.MONTH_LABEL(months[i]).split(" '")[0])); + svg.appendChild(el('text', { x: x(i), y: H - 8, 'text-anchor': 'middle', class: 'chart-axis' }, axisLab(months[i]))); }); container.appendChild(svg); } diff --git a/web/dashboard.html b/web/dashboard.html index b64e8d7..965aa7e 100644 --- a/web/dashboard.html +++ b/web/dashboard.html @@ -20,6 +20,11 @@
725 txns · 12 banks · 10 months
+
+ + + +
-

Income vs spending by monthmonthly, CLP

+

Income vs spending${granPill()}

Money inMoney out

Real vs internal

@@ -110,15 +126,15 @@

Cumulative net positionrunning income − spending

`; - C.monthlyBars($('#ov-bars'), months, [ + C.monthlyBars($('#ov-bars'), keys, [ { key: 'in', label: 'Money in', color: col.income(), values: inByM }, { key: 'out', label: 'Money out', color: col.spend(), values: outByM } - ], { height: 230 }); + ], { height: 230, labelFn: granLabelFn() }); C.donut($('#ov-donut'), [ { label: 'Real money', value: TOT.realIn + TOT.realOut, color: col.income() }, { label: 'Internal shuffle', value: TOT.internal, color: col.internal() } ], { size: 176, centerTop: 'x' + mult.toFixed(1), centerBot: 'multiplier' }); - C.lineChart($('#ov-line'), months, netByM, { height: 200, color: col.income() }); + C.lineChart($('#ov-line'), keys, netByM, { height: 200, color: col.income(), labelFn: granLabelFn() }); } // ============================================================ TAB 2: SPENDING @@ -131,7 +147,7 @@ const palette = ['#ff5f73', '#ff7a6b', '#ff9460', '#ffae57', '#ffc24b', '#d98cff', '#8c9eff', '#6fd0ff', '#5fd0a0', '#7a8699']; const catColor = i => i < palette.length ? palette[i] : col.mute(); const months = window.MT.months; - const spendByM = {}; months.forEach(m => spendByM[m] = sum(spend.filter(t => t.ym === m))); + const spKeys = periodKeys(); const spendByM = sumByPeriod(spend, () => true); // top merchants const byMerch = groupSum(tx.filter(isExpense), t => t.description || 'Unknown'); const merch = sortEntries(byMerch).slice(0, 10); @@ -147,7 +163,7 @@

Where it actually goesby category

Top merchantssingle payees

-

Spending by monthCLP / month

+

Spending${granPill()}

Categories & merchantsclick to expand

`; C.hbars($('#sp-cats'), cats.map(([label, d], i) => ({ @@ -164,7 +180,7 @@ row.addEventListener('mouseleave', () => window.MTtip.hide()); ml.appendChild(row); }); - C.monthlyBars($('#sp-month'), months, [{ key: 'sp', label: 'Spending', color: col.spend(), values: spendByM }], { height: 200 }); + C.monthlyBars($('#sp-month'), spKeys, [{ key: 'sp', label: 'Spending', color: col.spend(), values: spendByM }], { height: 200, labelFn: granLabelFn() }); renderCategoryBreakdown($('#sp-breakdown'), cats, catColor, total); } @@ -221,7 +237,7 @@ const srcs = sortEntries(bySrc); const palette = ['#2ee6a6', '#3ad6b0', '#46c6ba', '#52b6c4', '#5ea6ce', '#b98cff', '#8c9eff', '#7a8699']; const months = window.MT.months; - const incByM = {}; months.forEach(m => incByM[m] = sum(inc.filter(t => t.ym === m))); + const inKeys = periodKeys(); const incByM = sumByPeriod(inc, () => true); const top3 = srcs.slice(0, 3).reduce((a, [, d]) => a + d.value, 0); const conc = Math.round(top3 / total * 100); @@ -238,7 +254,7 @@
-

Income by monthCLP / month

+

Income${granPill()}

`; C.hbars($('#in-src'), srcs.map(([label, d], i) => ({ label, value: d.value, color: i < palette.length ? palette[i] : col.mute(), @@ -252,7 +268,7 @@ if (otherV > 0) donutData.push({ label: 'Other sources', value: otherV, color: col.mute() }); C.donut($('#in-donut'), donutData, { size: 176, centerTop: window.MT.CLPk(total).replace('$', '$'), centerBot: 'total in' }); $('#in-chips').innerHTML = donutData.map(d => `${esc(d.label)} ${Math.round(d.value / total * 100)}%`).join(''); - C.monthlyBars($('#in-month'), months, [{ key: 'in', label: 'Income', color: col.income(), values: incByM }], { height: 200 }); + C.monthlyBars($('#in-month'), inKeys, [{ key: 'in', label: 'Income', color: col.income(), values: incByM }], { height: 200, labelFn: granLabelFn() }); } // ============================================================ TAB 4: CYCLES @@ -357,6 +373,17 @@ } document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => show(t.dataset.tab))); + // granularity toggle (Month / Year / 5-Year) + function rerenderActive() { const cur = document.querySelector('.tab.on'); if (cur && TOT) RENDERERS[cur.dataset.tab]($('#host-' + cur.dataset.tab)); } + document.querySelectorAll('#gran-seg button').forEach(b => { + b.classList.toggle('on', b.dataset.g === GRAN); + b.addEventListener('click', () => { + GRAN = b.dataset.g; localStorage.setItem('mt-gran', GRAN); + document.querySelectorAll('#gran-seg button').forEach(x => x.classList.toggle('on', x === b)); + rerenderActive(); + }); + }); + let rt; addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(() => { const cur = document.querySelector('.tab.on'); if (cur && TOT) RENDERERS[cur.dataset.tab]($('#host-' + cur.dataset.tab)); }, 160); }); // expose for tweaks re-render (no-op until data is loaded)