From d4272e57dae6d8b9322a87dde9ea80904eb69ebb Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Fri, 10 Jul 2026 18:28:24 -0700 Subject: [PATCH] Calendar totals: compact per-category status lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the stacked share bar + labeled percentage chips above the calendar grid with a compact deviation strip: one thin status-colored line per category (height scaled to the tallest non-median category), with each share printed above it in that category's own status color. The median tier — the neutral "Normal" center of the diverging temperature scale — draws no line, just a faint baseline tick, so it reads as the reference the other categories deviate from. Precip and dry-streak have no natural median, so every category there keeps a line. Percentage text is lifted toward the theme text color (color-mix) so even the darkest/lightest tiers stay legible in both light and dark. Co-Authored-By: Claude Opus 4.8 --- static/calendar.js | 28 ++++++++++++++++++++-------- static/style.css | 29 +++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/static/calendar.js b/static/calendar.js index aa5e360..a79fe4a 100644 --- a/static/calendar.js +++ b/static/calendar.js @@ -673,7 +673,7 @@ function renderTotals(visible) { .map((r) => { const g = metric === "feels" ? feelsPick(r) : r[metric]; return g && g.c; }) .filter(Boolean); buckets = SCALE_TEMP.map((t) => - [t.label, TIER_COLORS[t.c], classes.filter((c) => c === t.c).length]); + [t.label, TIER_COLORS[t.c], classes.filter((c) => c === t.c).length, t.c === "normal"]); title = (TEMP_METRIC_INFO[metric] || {}).label || "value"; } @@ -687,14 +687,26 @@ function renderTotals(visible) { totEl.hidden = false; return; } - const bar = buckets.filter((b) => b[2] > 0).map(([label, color, n]) => - ``).join(""); - const chips = buckets.filter((b) => b[2] > 0).map(([label, color, n]) => - `
- ${label}${pctText(n)}
`).join(""); + // A compact deviation strip: one thin status-colored line per category, ordered + // low→high like the color scale, with each share printed above it in that + // category's own color. The median tier (the neutral "Normal" center of the + // diverging temperature scale) is the reference — its share is shown but it draws + // no line, just a faint baseline tick. Precip/dry-streak have no median, so every + // category there gets a line. Category names live in the color key below the grid, + // so this stays terse. Line heights scale to the tallest non-median category. + const maxLine = Math.max(1, ...buckets.filter((b) => !b[3]).map((b) => b[2])); + const LINE_MAX = 30; // px — height of the most common (non-median) category + const cols = buckets.map(([label, color, n, isMed]) => { + const h = isMed || !n ? 0 : Math.max(2, Math.round(LINE_MAX * n / maxLine)); + const num = n ? pctText(n) : "0"; + const line = h ? `` : ""; + const mid = isMed ? `` : ""; + return `
` + + `${num}${line}${mid}
`; + }).join(""); totEl.innerHTML = `
${head}
-
${bar}
-
${chips}
`; +
${cols}
`; totEl.hidden = false; } diff --git a/static/style.css b/static/style.css index b5edc16..d3baed5 100644 --- a/static/style.css +++ b/static/style.css @@ -736,14 +736,31 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } } .comfort-row .hint { flex: 1 1 100%; margin: 0; font-size: 12px; color: var(--muted); } -/* Category totals above the grid: stacked share bar + per-category chips. */ +/* Category totals above the grid: a compact deviation strip. Each category is a + column with its share printed at the top (in that category's own status color) + and a thin status-colored line rising from a shared baseline (height ∝ share). + The median tier draws no line — just a faint baseline tick as the neutral anchor. */ .cal-totals { max-width: 560px; margin: 0 0 18px; } -.ct-bar { - display: flex; height: 14px; border-radius: 7px; overflow: hidden; - margin: 6px 0 8px; background: var(--surface-2); +.ct-strip { + display: flex; align-items: flex-end; gap: 6px; + height: calc(var(--line-max, 30px) + 22px); margin: 8px 0 6px; +} +.ct-col { position: relative; flex: 1 1 0; min-width: 0; height: 100%; } +.ct-num { + position: absolute; top: 0; left: 0; right: 0; text-align: center; + font-size: 11.5px; font-weight: 700; line-height: 1; + color: color-mix(in oklab, var(--c, var(--muted)), var(--text) 40%); +} +.ct-num-zero { font-weight: 600; opacity: .38; } +.ct-line { + position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); + width: 3px; border-radius: 2px 2px 0 0; background: var(--c); +} +.ct-col-median .ct-num { color: var(--muted); font-weight: 800; } +.ct-mid { + position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); + width: 7px; height: 3px; border-radius: 2px; background: var(--border); } -.ct-bar span { display: block; height: 100%; } -.ct-pct { color: var(--muted); font-size: 12px; } /* Days filtered out by the weather filter fade back so matches pop. */ .cal-cell.dim { opacity: .15; }