nerd corner: add cumulative plot below the per-period chart
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user