From ef61308a9effa9e03f8f087bfbdf3f4c392d811a Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Sun, 19 Jul 2026 15:30:31 -0700 Subject: [PATCH] Show a metric's unit once atop its distribution strip (#195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The distribution strip printed each bar's average with its full unit inline, so humidity read "4.0 g/m³" in a ~38px phone column and clipped on every bar — temperature never did, because it shows a bare "40°". Wind ("5 mph") and precip were heading the same way. The unit now appears once, top-right of the strip, and every bar value is bare: "4.0" under a "g/m³" label, "5" under "mph", "0.00" under "in". It reacts to the unit toggle like the values do (°F↔°C, mm↔in, mph↔km/h). Temperature keeps its degree glyph on the value since it is iconic and never clipped. With the unit no longer on the humidity label, its calendar title drops the "(g/m³ of water vapor)" parenthetical that would otherwise repeat it — every metric's title is unitless now, the strip's own label carries it. Also fixes a pre-existing crash: calendar.js referenced DRY_CAP without importing it from shared.js, so selecting the dry-streak metric threw and left its legend unrendered. Exported it. Verified at 390 and 1440 in light and dark, imperial and metric, across all five calendar metrics and the compare strips: no label clips, no console errors. Claude-Session: https://claude.ai/code/session_013dRZmX9D3JEntfMKWMTWZ8 --- static/calendar.js | 6 ++++-- static/shared.js | 39 +++++++++++++++++++++++++++++++++++---- static/style.css | 10 ++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/static/calendar.js b/static/calendar.js index f984ed5..468620e 100644 --- a/static/calendar.js +++ b/static/calendar.js @@ -11,7 +11,7 @@ import { TIER_COLORS, PRECIP_COLORS, SCALE_TEMP, SCALE_RAIN, drynessColor, pctOr CHUNK_MONTHS, buildChunks, clickOpensPicker, metricBuckets, distStrip, seasonFilterDropdown, seasonSummaryText, syncSeasonChecks, applySeasonMonthChange, initSeasonExpand, allMonths, monthsToMask, maskToMonths, - weatherType as wxType } from "./shared.js"; + weatherType as wxType, DRY_CAP } from "./shared.js"; import { initFilterSheet } from "./filtersheet.js"; // The diverging-temperature-scale metrics (colored exactly like High/Low), with a @@ -20,7 +20,9 @@ const TEMP_METRIC_INFO = { tmax: { name: "High", label: "daily high", fmt: fmtTemp }, tmin: { name: "Low", label: "daily low", fmt: fmtTemp }, feels: { name: "Feels", label: "feels like (apparent temperature)", fmt: fmtTemp }, - humid: { name: "Humid", label: "absolute humidity (g/m³ of water vapor)", fmt: fmtHumid }, + // The unit lives in the strip's own top-right label now, so the metric name + // doesn't repeat it (every other metric's title is unitless too). + humid: { name: "Humid", label: "absolute humidity", fmt: fmtHumid }, wind: { name: "Wind", label: "wind speed", fmt: fmtWind }, gust: { name: "Gust", label: "wind gust", fmt: fmtWind }, }; diff --git a/static/shared.js b/static/shared.js index 1f8008a..6d7f16d 100644 --- a/static/shared.js +++ b/static/shared.js @@ -2,7 +2,7 @@ // constant and helper here previously existed as a copy in app.js, calendar.js, // day.js, compare.js and/or legend.html; pages import what they need so a tier // color, scale label or formatter has exactly one home. -import { fmtTemp, fmtPrecip, fmtWind, fmtHumid, getUnit, +import { fmtTemp, fmtPrecip, fmtWind, fmtHumid, getUnit, windUnit, precipUnit, toUnit, toWind, toPrecip } from "./units.js"; // ---- tier colors ---- // style.css's :root custom properties are the source of truth (they're not @@ -59,7 +59,8 @@ export const SCALE_RAIN = [ // ---- dry-streak ramp ---- // "Days since last rain": rain days are blue; each dry day warms from a light // base toward dark red, saturating at 14 consecutive dry days. -const DRY_BASE = [247, 217, 176], DRY_MAX = [122, 13, 26], DRY_CAP = 14; +const DRY_BASE = [247, 217, 176], DRY_MAX = [122, 13, 26]; +export const DRY_CAP = 14; const lerpRgb = (a, b, t) => `rgb(${a.map((x, i) => Math.round(x + (b[i] - x) * t)).join(",")})`; export function drynessColor(dsr) { @@ -128,6 +129,31 @@ function fmtMetricVal(unit, v) { return `${Math.round(v)}`; } +// The value of a bucket's average with no unit — the unit is shown once at the top +// of the strip (stripUnitLabel), not repeated on every bar. Temperature keeps its +// degree glyph, which is iconic and narrow; the wordy units (" g/m³", " mph") are +// exactly what made these labels clip on a phone. +function fmtMetricBare(unit, v) { + if (v == null || isNaN(v)) return ""; + if (unit === "temp") return fmtTemp(v); // "40°" + if (unit === "humid") return v.toFixed(1); // "4.0" + if (unit === "wind") return fmtWind(v, false); // "5" + if (unit === "precip") return fmtPrecip(v, false);// "0.00" / "5" + if (unit === "dsr") return `${Math.round(v)}`; // "9", header supplies "days" + return `${Math.round(v)}`; +} + +// The unit label for a whole strip, shown once at its top. Reacts to the active +// unit system, so it flips with the toggle alongside the values. +function stripUnitLabel(unit) { + if (unit === "temp") return `°${getUnit()}`; + if (unit === "humid") return "g/m³"; + if (unit === "wind") return windUnit(); + if (unit === "precip") return precipUnit(); + if (unit === "dsr") return "days"; + return ""; +} + // The low–high span for a tier, as bare numbers. No unit: this line sits directly // under the average, which carries it, and a column is ~38px wide on a phone — // repeating " g/m³" on both ends is what made the old in-bar label clip. @@ -192,13 +218,18 @@ export function distStrip(buckets, showCount) { return `
` + - `${avg != null ? fmtMetricVal(b.unit, avg) : ""}` + + `${avg != null ? fmtMetricBare(b.unit, avg) : ""}` + `${range}` + bar + `${valText(b.n, b.group)}` + `${b.label}
`; }).join(""); - return `
${cols}
`; + // The unit once, at the top of the strip — not repeated on every bar, where the + // wordy ones ("g/m³", "km/h") clipped a ~38px phone column. Bare on dry-streak + // and any unitless metric. + const unit = stripUnitLabel(buckets[0] ? buckets[0].unit : ""); + const cap = unit ? `
${unit}
` : ""; + return `
${cap}
${cols}
`; } // ---- formatters & tiny utils ---- diff --git a/static/style.css b/static/style.css index adc5c96..6e51e54 100644 --- a/static/style.css +++ b/static/style.css @@ -1050,6 +1050,16 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; } baseline and nearly touching so they read as one chart. The tier's average value sits above its bar, the min–max range inside it, and the share + name below. Bar height encodes frequency (within the metric's scale group). */ +/* Wraps the unit label + the scrolling strip. The label sits outside .ct-strip's + overflow so it stays put when a wide (precip) strip scrolls. */ +.ct-plot { position: relative; } +/* The metric's unit, shown once at the top-right of the strip — the values on the + bars are bare so they don't clip a narrow phone column. */ +.ct-unit { + text-align: right; margin: 6px 2px -2px; height: 1em; + font-size: 11px; font-weight: 700; line-height: 1; + color: var(--muted); font-variant-numeric: tabular-nums; +} .ct-strip { display: flex; align-items: flex-end; gap: 1px; margin: 10px 0 6px; overflow-x: auto; }