nerd corner: add cumulative plot below the per-period chart

This commit is contained in:
Davide Scaini
2026-05-14 18:43:05 +02:00
parent 5167f2a988
commit 653db2428f
+53 -13
View File
@@ -76,31 +76,48 @@
$: ({ rows, colorDomain, colorRange } = buildData(activities, metric, granularity)); $: ({ rows, colorDomain, colorRange } = buildData(activities, metric, granularity));
let chartEl: HTMLElement; let chartEl: HTMLElement;
let chartCumEl: HTMLElement;
function renderChart( function renderChartInto(
el: HTMLElement,
rows: { year: string; period: number; value: number }[], rows: { year: string; period: number; value: number }[],
colorDomain: string[], colorDomain: string[],
colorRange: string[], colorRange: string[],
m: Metric, m: Metric,
g: Granularity, g: Granularity,
cumulative: boolean,
) { ) {
if (!chartEl) return; if (!el) return;
chartEl.innerHTML = ''; el.innerHTML = '';
if (!rows.length) return; if (!rows.length) return;
// For the cumulative chart, convert per-period rows to running sums.
// rows are ordered: for each year (sorted asc), periods 1..limit in order.
let displayRows = rows;
if (cumulative) {
const acc = new Map<string, number>();
displayRows = rows.map(r => {
const prev = acc.get(r.year) ?? 0;
const cum = prev + r.value;
acc.set(r.year, cum);
return { ...r, value: cum };
});
}
const maxPer = g === 'week' ? 52 : 12; const maxPer = g === 'week' ? 52 : 12;
const xLabel = g === 'week' ? 'Week' : 'Month'; const xLabel = g === 'week' ? 'Week' : 'Month';
const axColor = document.documentElement.getAttribute('data-theme') === 'light' ? '#52525b' : '#a1a1aa'; const axColor = document.documentElement.getAttribute('data-theme') === 'light' ? '#52525b' : '#a1a1aa';
const fmt = METRIC_FMT[m]; const fmt = METRIC_FMT[m];
const curYear = String(_currentYear); const curYear = String(_currentYear);
const pastRows = rows.filter(r => r.year !== curYear); const pastRows = displayRows.filter(r => r.year !== curYear);
const curRows = rows.filter(r => r.year === curYear); const curRows = displayRows.filter(r => r.year === curYear);
const yLabel = cumulative ? `Cumulative ${METRIC_LABEL[m]}` : METRIC_LABEL[m];
const MONTH_LABELS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const MONTH_LABELS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const chart = Plot.plot({ const chart = Plot.plot({
width: chartEl.clientWidth || 700, width: el.clientWidth || 700,
height: 320, height: 320,
marginLeft: 60, marginLeft: 60,
marginBottom: 40, marginBottom: 40,
@@ -113,8 +130,8 @@
? (d: number) => MONTH_LABELS[d - 1] ? (d: number) => MONTH_LABELS[d - 1]
: (d: number) => String(d), : (d: number) => String(d),
}, },
y: { label: METRIC_LABEL[m], grid: true, zero: true }, y: { label: yLabel, grid: true, zero: true },
color: { domain: colorDomain, range: colorRange, legend: true }, color: { domain: colorDomain, range: colorRange, legend: !cumulative },
marks: [ marks: [
...(pastRows.length ? [ ...(pastRows.length ? [
Plot.line(pastRows, { Plot.line(pastRows, {
@@ -140,19 +157,31 @@
] : []), ] : []),
], ],
}); });
chartEl.appendChild(chart); el.appendChild(chart);
} }
$: renderChart(rows, colorDomain, colorRange, metric, granularity); function renderBoth(
rows: { year: string; period: number; value: number }[],
colorDomain: string[],
colorRange: string[],
m: Metric,
g: Granularity,
) {
renderChartInto(chartEl, rows, colorDomain, colorRange, m, g, false);
renderChartInto(chartCumEl, rows, colorDomain, colorRange, m, g, true);
}
$: renderBoth(rows, colorDomain, colorRange, metric, granularity);
// Keep current values for resize / theme callbacks // Keep current values for resize / theme callbacks
let _r = rows, _cd = colorDomain, _cr = colorRange, _m = metric, _g = granularity; let _r = rows, _cd = colorDomain, _cr = colorRange, _m = metric, _g = granularity;
$: _r = rows; $: _cd = colorDomain; $: _cr = colorRange; $: _m = metric; $: _g = granularity; $: _r = rows; $: _cd = colorDomain; $: _cr = colorRange; $: _m = metric; $: _g = granularity;
onMount(() => { onMount(() => {
const ro = new ResizeObserver(() => renderChart(_r, _cd, _cr, _m, _g)); const redraw = () => renderBoth(_r, _cd, _cr, _m, _g);
const ro = new ResizeObserver(redraw);
ro.observe(chartEl); ro.observe(chartEl);
const mo = new MutationObserver(() => renderChart(_r, _cd, _cr, _m, _g)); const mo = new MutationObserver(redraw);
mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
return () => { ro.disconnect(); mo.disconnect(); }; return () => { ro.disconnect(); mo.disconnect(); };
}); });
@@ -175,6 +204,14 @@
} }
.pill.active { background: #1d4ed822; border-color: #60a5fa; color: #60a5fa; } .pill.active { background: #1d4ed822; border-color: #60a5fa; color: #60a5fa; }
.pill:hover:not(.active) { border-color: #a1a1aa; color: #d4d4d8; } .pill:hover:not(.active) { border-color: #a1a1aa; color: #d4d4d8; }
.section-label {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #52525b;
margin: 2rem 0 0.5rem;
}
</style> </style>
<div class="controls"> <div class="controls">
@@ -191,6 +228,9 @@
<div bind:this={chartEl} class="w-full min-h-[320px]"></div> <div bind:this={chartEl} class="w-full min-h-[320px]"></div>
<p class="section-label">Cumulative</p>
<div bind:this={chartCumEl} class="w-full min-h-[320px]"></div>
{#if !rows.length} {#if !rows.length}
<p class="text-zinc-500 text-sm mt-4">No activity data to display.</p> <p class="text-zinc-500 text-sm mt-4">No activity data to display.</p>
{/if} {/if}