From 29302624a71cc1b06ff3878d7cb571c4422161c7 Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Sat, 11 Jul 2026 14:48:21 -0700 Subject: [PATCH] =?UTF-8?q?Make=20the=20compare=20page=20unit-aware=20so?= =?UTF-8?q?=20the=20=C2=B0F/=C2=B0C=20toggle=20works=20there=20(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compare.js never imported units.js, so the header unit toggle was never built on the compare page and every temperature was stuck in °F. Import units.js (which builds the toggle) and route the comfort target, comfort band, and the colder/warmer/typical-miss figures through the unit formatters, re-rendering on unit change. State stays in °F (the API's unit); only the displayed numbers convert. Add fmtDelta for degree *differences* (band, average miss), which scale by 5/9 without the 32° offset that absolute temperatures use. --- static/compare.js | 27 +++++++++++++++++++-------- static/units.js | 12 ++++++++++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/static/compare.js b/static/compare.js index 9dbb5f9..f795db5 100644 --- a/static/compare.js +++ b/static/compare.js @@ -16,6 +16,7 @@ import { getJSON, TTL } from "./cache.js"; import { initFindButton } from "./mappicker.js"; import { MONTHS, pad, isoOfDate, monthStart, monthEnd, buildChunks, clickOpensPicker } from "./shared.js"; +import { fmtTemp, fmtDelta, onUnitChange } from "./units.js"; const CMP = { comfort: "thermograph:cmpComfort", @@ -243,7 +244,8 @@ function computeStats(series) { // ---- render ---- const pct = (v) => `${v < 10 ? v.toFixed(1) : Math.round(v)}%`; -const deg = (v) => `${Math.round(v)}°`; +// Temperature *differences* (avg miss, typical miss) in the active unit. +const deg = (v) => fmtDelta(v); function renderLocList() { locList.innerHTML = locations.map((l, i) => { @@ -269,7 +271,7 @@ function rankCard(loc, s, rank) { const stats = `
` + `
Colder${pct(s.belowPct)}${s.below ? `avg ${deg(s.avgBelow)} below` : "—"}
` + - `
In comfort${pct(s.comfortPct)}±${tol}° of ${comfort}°
` + + `
In comfort${pct(s.comfortPct)}±${fmtDelta(tol)} of ${fmtTemp(comfort)}
` + `
Warmer${pct(s.abovePct)}${s.above ? `avg ${deg(s.avgAbove)} above` : "—"}
` + `
`; return `
` + @@ -301,8 +303,8 @@ function renderResults() { const win = scored[0]; const span = `${monthLabel(range.start)} – ${monthLabel(range.end)}`; const lead = scored.length > 1 - ? `${win.loc.name} best matches ${comfort}° — ${pct(win.s.comfortPct)} of days within ±${tol}°` - : `${win.loc.name}: ${pct(win.s.comfortPct)} of days within ±${tol}° of ${comfort}°`; + ? `${win.loc.name} best matches ${fmtTemp(comfort)} — ${pct(win.s.comfortPct)} of days within ±${fmtDelta(tol)}` + : `${win.loc.name}: ${pct(win.s.comfortPct)} of days within ±${fmtDelta(tol)} of ${fmtTemp(comfort)}`; head.hidden = false; head.innerHTML = `

${lead}

` + `

By ${BASIS_LABEL[basis]} · ${span}${loading ? ` · loading ${loading} more…` : ""}

`; @@ -335,18 +337,27 @@ locList.addEventListener("click", (e) => { // Comfort / band / basis: instant re-rank over data already in hand — no refetch. comfortInput.addEventListener("input", () => { comfort = +comfortInput.value; - comfortVal.textContent = `${comfort}°`; + comfortVal.textContent = fmtTemp(comfort); lsSet(CMP.comfort, String(comfort)); writeHashState(); renderResults(); }); tolInput.addEventListener("input", () => { tol = +tolInput.value; - tolVal.textContent = `±${tol}°`; + tolVal.textContent = `±${fmtDelta(tol)}`; lsSet(CMP.tol, String(tol)); writeHashState(); renderResults(); }); + +// The °F/°C toggle only flips what's shown — comfort/tol/series all stay in °F +// (the API's unit), so there's nothing to refetch or re-rank, just re-render the +// numbers: the two slider labels and every card. +onUnitChange(() => { + comfortVal.textContent = fmtTemp(comfort); + tolVal.textContent = `±${fmtDelta(tol)}`; + renderResults(); +}); basisToggle.addEventListener("click", (e) => { const btn = e.target.closest("button[data-basis]"); if (!btn) return; @@ -373,8 +384,8 @@ clickOpensPicker(startInput, endInput); // whole-field tap opens the month pic if (hash.start && hash.end) range = { start: hash.start, end: hash.end }; } - comfortInput.value = comfort; comfortVal.textContent = `${comfort}°`; - tolInput.value = tol; tolVal.textContent = `±${tol}°`; + comfortInput.value = comfort; comfortVal.textContent = fmtTemp(comfort); + tolInput.value = tol; tolVal.textContent = `±${fmtDelta(tol)}`; basisToggle.querySelectorAll("button").forEach((b) => b.classList.toggle("active", b.dataset.basis === basis)); startInput.value = range.start; endInput.value = range.end; diff --git a/static/units.js b/static/units.js index d07b6d8..7e5980d 100644 --- a/static/units.js +++ b/static/units.js @@ -19,6 +19,15 @@ export function fmtTemp(vF, withLetter) { return `${Math.round(toUnit(vF))}°${withLetter ? _unit : ""}`; } +// A Fahrenheit-degree *difference* (a span or tolerance, not an absolute +// reading) formatted in the active unit. Differences scale by 5/9 with no 32° +// offset, so they can't go through toUnit/fmtTemp. +export function fmtDelta(dF, withLetter) { + if (dF == null || isNaN(dF)) return "—"; + const d = _unit === "C" ? dF * 5 / 9 : dF; + return `${Math.round(d)}°${withLetter ? _unit : ""}`; +} + export function onUnitChange(cb) { _unitCbs.push(cb); } export function setUnit(u) { @@ -39,8 +48,7 @@ function syncUnitToggle(wrap) { } // Drop a segmented °F/°C control into the header's top-right, ahead of the view -// switcher. A page opts out with data-no-unit-toggle on (Compare does: -// its comfort slider is inherently °F-based). +// switcher. A page opts out with data-no-unit-toggle on . (function buildUnitToggle() { const brand = document.querySelector(".brand"); if (!brand || brand.querySelector(".unit-toggle")) return;