Scope calendar strip percentages to their bar group (#59)

Following the per-group bar heights, the percentage labels now match: a
multi-bar group (rain intensities on precip, streak lengths on dry streak)
shows each bar's share within that group, so its bars read as a distribution.
The lone opposing column (Dry on precip, Rain day on dry streak) keeps its
share of all shown days, which stays meaningful rather than trivially 100%.
This commit is contained in:
Emi Griffith 2026-07-11 15:11:21 -07:00 committed by GitHub
parent 79d755c16e
commit f4109f9ede

View file

@ -558,10 +558,24 @@ function renderTotals(visible) {
} }
const total = buckets.reduce((n, b) => n + b[2], 0); const total = buckets.reduce((n, b) => n + b[2], 0);
const pctText = (n) => { const p = 100 * n / total, r = Math.round(p); return n > 0 && r === 0 ? `${p.toFixed(1)}%` : `${r}%`; }; // Per-group subtotals + membership, so a bar's share can be scoped to its own group.
const groupTotal = {}, groupCount = {};
for (const b of buckets) {
groupTotal[b[3]] = (groupTotal[b[3]] || 0) + b[2];
groupCount[b[3]] = (groupCount[b[3]] || 0) + 1;
}
// A multi-bar group (rain intensities on precip, streak lengths on dry streak) shows
// each bar's share *within that group* — matching the per-group bar heights. A lone
// opposing column (Dry on precip, Rain day on dry streak) stays a share of ALL shown
// days, since a group-relative percentage there would trivially read 100%.
const pctText = (n, group) => {
const d = groupCount[group] > 1 ? (groupTotal[group] || 1) : total;
const p = 100 * n / d, r = Math.round(p);
return n > 0 && r === 0 ? `${p.toFixed(1)}%` : `${r}%`;
};
// The strip prints either each category's share (default) or its raw day count, // The strip prints either each category's share (default) or its raw day count,
// toggled by the "Show count" checkbox rendered in the header (state: showCount). // toggled by the "Show count" checkbox rendered in the header (state: showCount).
const valText = (n) => !n ? "0" : (showCount ? n.toLocaleString() : pctText(n)); const valText = (n, group) => !n ? "0" : (showCount ? n.toLocaleString() : pctText(n, group));
const head = wActive const head = wActive
? `${days.length.toLocaleString()} of ${visible.length.toLocaleString()} shown days match the weather filter — by ${title}:` ? `${days.length.toLocaleString()} of ${visible.length.toLocaleString()} shown days match the weather filter — by ${title}:`
: `${visible.length.toLocaleString()} days shown — by ${title}:`; : `${visible.length.toLocaleString()} days shown — by ${title}:`;
@ -592,9 +606,9 @@ function renderTotals(visible) {
// Caption stacks the category name over its value; the name reserves two lines // Caption stacks the category name over its value; the name reserves two lines
// so the value figures stay aligned across columns whether a name wraps or not. // so the value figures stay aligned across columns whether a name wraps or not.
return `<div class="ct-col" style="--c:${color}" ` + return `<div class="ct-col" style="--c:${color}" ` +
`title="${label} · ${pctText(n)} (${n})">` + `title="${label} · ${pctText(n, group)} (${n})">` +
`<span class="ct-cap"><span class="ct-label">${label}</span>` + `<span class="ct-cap"><span class="ct-label">${label}</span>` +
`<span class="ct-num${n ? "" : " ct-num-zero"}">${valText(n)}</span></span>` + `<span class="ct-num${n ? "" : " ct-num-zero"}">${valText(n, group)}</span></span>` +
`${line}</div>`; `${line}</div>`;
}).join(""); }).join("");
totEl.innerHTML = `${header} totEl.innerHTML = `${header}