Scale wet and dry calendar bars independently (#57)

In the calendar category strip, precip (Dry + rain intensity) and dry-streak
(Rain day + dry buckets) mix wet and dry categories. Heights scaled to the
single tallest bucket across both, so a dominant dry-day count flattened the
rain-intensity bars to ~2px and vice versa, hiding the opposing distribution.

Give each bucket a scale group (wet/dry) and size bars to the tallest bucket
within their own group. Temperature tiers share one group, so they scale
together as before.
This commit is contained in:
Emi Griffith 2026-07-11 14:59:15 -07:00 committed by GitHub
parent 29302624a7
commit 79d755c16e

View file

@ -534,20 +534,26 @@ function renderTotals(visible) {
["14+ dry", drynessColor(14), (d) => d >= 14], ["14+ dry", drynessColor(14), (d) => d >= 14],
]; ];
const vals = days.map((r) => r.dsr).filter((d) => d != null); const vals = days.map((r) => r.dsr).filter((d) => d != null);
buckets = B.map(([label, color, fn]) => [label, color, vals.filter(fn).length]); // Group (index 3) sets which categories share a height scale: the lone "Rain day"
// (wet) is scaled apart from the dry-streak buckets so it can't flatten them.
buckets = B.map(([label, color, fn], i) =>
[label, color, vals.filter(fn).length, i === 0 ? "wet" : "dry"]);
title = "dry streak"; title = "dry streak";
} else if (metric === "precip") { } else if (metric === "precip") {
const classes = days.map((r) => r.precip && r.precip.c).filter(Boolean); const classes = days.map((r) => r.precip && r.precip.c).filter(Boolean);
buckets = [["Dry", drynessColor(7), classes.filter((c) => c === "dry").length]] // Dry days scale independently of the rain-intensity buckets (wet) so a big dry
// count doesn't crush the rain bars — and vice versa.
buckets = [["Dry", drynessColor(7), classes.filter((c) => c === "dry").length, "dry"]]
.concat(SCALE_RAIN.map((t) => .concat(SCALE_RAIN.map((t) =>
[t.label, PRECIP_COLORS[t.c], classes.filter((c) => c === t.c).length])); [t.label, PRECIP_COLORS[t.c], classes.filter((c) => c === t.c).length, "wet"]));
title = "rain intensity"; title = "rain intensity";
} else { } else {
const classes = days const classes = days
.map((r) => { const g = r[metric]; return g && g.c; }) .map((r) => { const g = r[metric]; return g && g.c; })
.filter(Boolean); .filter(Boolean);
// Temperature tiers all share one scale group, so they scale together as before.
buckets = SCALE_TEMP.map((t) => buckets = SCALE_TEMP.map((t) =>
[t.label, TIER_COLORS[t.c], classes.filter((c) => c === t.c).length, t.c === "normal"]); [t.label, TIER_COLORS[t.c], classes.filter((c) => c === t.c).length, "temp"]);
title = (TEMP_METRIC_INFO[metric] || {}).label || "value"; title = (TEMP_METRIC_INFO[metric] || {}).label || "value";
} }
@ -573,11 +579,15 @@ function renderTotals(visible) {
// own color. Every category — including the neutral "Normal" center of the diverging // own color. Every category — including the neutral "Normal" center of the diverging
// temperature scale — draws a line, so the strip reads as a full distribution. // temperature scale — draws a line, so the strip reads as a full distribution.
// Category names live in the color key below the grid too, so this stays terse. // Category names live in the color key below the grid too, so this stays terse.
// Line heights scale to the tallest category. // Line heights scale to the tallest category *within each scale group* (index 3):
const maxLine = Math.max(1, ...buckets.map((b) => b[2])); // for precip/dry-streak, wet and dry days scale independently, so a lopsided count
const LINE_MAX = 30; // px — height of the most common category // on one side doesn't shrink the opposing side's bars into invisibility. Temperature
const cols = buckets.map(([label, color, n]) => { // tiers all sit in one group, so they still scale against each other as before.
const h = !n ? 0 : Math.max(2, Math.round(LINE_MAX * n / maxLine)); const maxByGroup = {};
for (const b of buckets) maxByGroup[b[3]] = Math.max(maxByGroup[b[3]] || 1, b[2]);
const LINE_MAX = 30; // px — height of the tallest category in its group
const cols = buckets.map(([label, color, n, group]) => {
const h = !n ? 0 : Math.max(2, Math.round(LINE_MAX * n / maxByGroup[group]));
const line = h ? `<span class="ct-line" style="height:${h}px"></span>` : ""; const line = h ? `<span class="ct-line" style="height:${h}px"></span>` : "";
// 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.