Make the compare page unit-aware so the °F/°C toggle works there (#55)

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.
This commit is contained in:
Emi Griffith 2026-07-11 14:48:21 -07:00 committed by GitHub
parent 94dde1ebc6
commit 29302624a7
2 changed files with 29 additions and 10 deletions

View file

@ -16,6 +16,7 @@ import { getJSON, TTL } from "./cache.js";
import { initFindButton } from "./mappicker.js"; import { initFindButton } from "./mappicker.js";
import { MONTHS, pad, isoOfDate, monthStart, monthEnd, buildChunks, import { MONTHS, pad, isoOfDate, monthStart, monthEnd, buildChunks,
clickOpensPicker } from "./shared.js"; clickOpensPicker } from "./shared.js";
import { fmtTemp, fmtDelta, onUnitChange } from "./units.js";
const CMP = { const CMP = {
comfort: "thermograph:cmpComfort", comfort: "thermograph:cmpComfort",
@ -243,7 +244,8 @@ function computeStats(series) {
// ---- render ---- // ---- render ----
const pct = (v) => `${v < 10 ? v.toFixed(1) : Math.round(v)}%`; 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() { function renderLocList() {
locList.innerHTML = locations.map((l, i) => { locList.innerHTML = locations.map((l, i) => {
@ -269,7 +271,7 @@ function rankCard(loc, s, rank) {
const stats = const stats =
`<div class="cmp-stats">` + `<div class="cmp-stats">` +
`<div class="cmp-stat cmp-s-below"><span class="cmp-k">Colder</span><span class="cmp-v">${pct(s.belowPct)}</span><span class="cmp-d">${s.below ? `avg ${deg(s.avgBelow)} below` : "—"}</span></div>` + `<div class="cmp-stat cmp-s-below"><span class="cmp-k">Colder</span><span class="cmp-v">${pct(s.belowPct)}</span><span class="cmp-d">${s.below ? `avg ${deg(s.avgBelow)} below` : "—"}</span></div>` +
`<div class="cmp-stat cmp-s-comfort"><span class="cmp-k">In comfort</span><span class="cmp-v">${pct(s.comfortPct)}</span><span class="cmp-d">±${tol}° of ${comfort}°</span></div>` + `<div class="cmp-stat cmp-s-comfort"><span class="cmp-k">In comfort</span><span class="cmp-v">${pct(s.comfortPct)}</span><span class="cmp-d">±${fmtDelta(tol)} of ${fmtTemp(comfort)}</span></div>` +
`<div class="cmp-stat cmp-s-above"><span class="cmp-k">Warmer</span><span class="cmp-v">${pct(s.abovePct)}</span><span class="cmp-d">${s.above ? `avg ${deg(s.avgAbove)} above` : "—"}</span></div>` + `<div class="cmp-stat cmp-s-above"><span class="cmp-k">Warmer</span><span class="cmp-v">${pct(s.abovePct)}</span><span class="cmp-d">${s.above ? `avg ${deg(s.avgAbove)} above` : "—"}</span></div>` +
`</div>`; `</div>`;
return `<div class="cmp-card">` + return `<div class="cmp-card">` +
@ -301,8 +303,8 @@ function renderResults() {
const win = scored[0]; const win = scored[0];
const span = `${monthLabel(range.start)} ${monthLabel(range.end)}`; const span = `${monthLabel(range.start)} ${monthLabel(range.end)}`;
const lead = scored.length > 1 const lead = scored.length > 1
? `<b>${win.loc.name}</b> best matches ${comfort}°${pct(win.s.comfortPct)} of days within ±${tol}°` ? `<b>${win.loc.name}</b> best matches ${fmtTemp(comfort)}${pct(win.s.comfortPct)} of days within ±${fmtDelta(tol)}`
: `<b>${win.loc.name}</b>: ${pct(win.s.comfortPct)} of days within ±${tol}° of ${comfort}°`; : `<b>${win.loc.name}</b>: ${pct(win.s.comfortPct)} of days within ±${fmtDelta(tol)} of ${fmtTemp(comfort)}`;
head.hidden = false; head.hidden = false;
head.innerHTML = `<h2>${lead}</h2>` + head.innerHTML = `<h2>${lead}</h2>` +
`<p class="meta">By ${BASIS_LABEL[basis]} · ${span}${loading ? ` · loading ${loading} more…` : ""}</p>`; `<p class="meta">By ${BASIS_LABEL[basis]} · ${span}${loading ? ` · loading ${loading} more…` : ""}</p>`;
@ -335,18 +337,27 @@ locList.addEventListener("click", (e) => {
// Comfort / band / basis: instant re-rank over data already in hand — no refetch. // Comfort / band / basis: instant re-rank over data already in hand — no refetch.
comfortInput.addEventListener("input", () => { comfortInput.addEventListener("input", () => {
comfort = +comfortInput.value; comfort = +comfortInput.value;
comfortVal.textContent = `${comfort}°`; comfortVal.textContent = fmtTemp(comfort);
lsSet(CMP.comfort, String(comfort)); lsSet(CMP.comfort, String(comfort));
writeHashState(); writeHashState();
renderResults(); renderResults();
}); });
tolInput.addEventListener("input", () => { tolInput.addEventListener("input", () => {
tol = +tolInput.value; tol = +tolInput.value;
tolVal.textContent = `±${tol}°`; tolVal.textContent = `±${fmtDelta(tol)}`;
lsSet(CMP.tol, String(tol)); lsSet(CMP.tol, String(tol));
writeHashState(); writeHashState();
renderResults(); 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) => { basisToggle.addEventListener("click", (e) => {
const btn = e.target.closest("button[data-basis]"); const btn = e.target.closest("button[data-basis]");
if (!btn) return; 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 }; if (hash.start && hash.end) range = { start: hash.start, end: hash.end };
} }
comfortInput.value = comfort; comfortVal.textContent = `${comfort}°`; comfortInput.value = comfort; comfortVal.textContent = fmtTemp(comfort);
tolInput.value = tol; tolVal.textContent = `±${tol}°`; tolInput.value = tol; tolVal.textContent = `±${fmtDelta(tol)}`;
basisToggle.querySelectorAll("button").forEach((b) => b.classList.toggle("active", b.dataset.basis === basis)); basisToggle.querySelectorAll("button").forEach((b) => b.classList.toggle("active", b.dataset.basis === basis));
startInput.value = range.start; endInput.value = range.end; startInput.value = range.start; endInput.value = range.end;

View file

@ -19,6 +19,15 @@ export function fmtTemp(vF, withLetter) {
return `${Math.round(toUnit(vF))}°${withLetter ? _unit : ""}`; 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 onUnitChange(cb) { _unitCbs.push(cb); }
export function setUnit(u) { 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 // 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 <body> (Compare does: // switcher. A page opts out with data-no-unit-toggle on <body>.
// its comfort slider is inherently °F-based).
(function buildUnitToggle() { (function buildUnitToggle() {
const brand = document.querySelector(".brand"); const brand = document.querySelector(".brand");
if (!brand || brand.querySelector(".unit-toggle")) return; if (!brand || brand.querySelector(".unit-toggle")) return;