thermograph/static/units.js
Emi Griffith 29302624a7 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.
2026-07-11 21:48:21 +00:00

69 lines
2.8 KiB
JavaScript

// Temperature units — the shared °C/°F toggle and unit-aware formatting.
// The API always speaks Fahrenheit; the unit is a pure display concern. Pages
// format temps through `fmtTemp`/`toUnit` and re-render on `onUnitChange`, so the
// stored/plotted values stay in °F and only the numbers shown flip.
const UNIT_KEY = "thermograph:unit";
let _unit = (localStorage.getItem(UNIT_KEY) === "C") ? "C" : "F";
const _unitCbs = [];
export function getUnit() { return _unit; }
// A Fahrenheit value as a number in the active unit.
export function toUnit(vF) { return _unit === "C" ? (vF - 32) * 5 / 9 : vF; }
// A Fahrenheit value formatted as a rounded temperature in the active unit.
// `withLetter` appends the C/F letter (off by default — the nav toggle shows it).
export function fmtTemp(vF, withLetter) {
if (vF == null || isNaN(vF)) return "—";
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) {
const nu = (u === "C") ? "C" : "F";
if (nu === _unit) return;
_unit = nu;
try { localStorage.setItem(UNIT_KEY, _unit); } catch (e) {}
document.querySelectorAll(".unit-toggle").forEach(syncUnitToggle);
_unitCbs.forEach((cb) => { try { cb(_unit); } catch (e) {} });
}
function syncUnitToggle(wrap) {
wrap.querySelectorAll("button[data-unit]").forEach((b) => {
const on = b.dataset.unit === _unit;
b.classList.toggle("active", on);
b.setAttribute("aria-pressed", on ? "true" : "false");
});
}
// 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>.
(function buildUnitToggle() {
const brand = document.querySelector(".brand");
if (!brand || brand.querySelector(".unit-toggle")) return;
if (document.body.dataset.noUnitToggle != null) return;
const wrap = document.createElement("div");
wrap.className = "unit-toggle";
wrap.setAttribute("role", "group");
wrap.setAttribute("aria-label", "Temperature units");
wrap.innerHTML = '<button type="button" data-unit="F">°F</button>'
+ '<button type="button" data-unit="C">°C</button>';
wrap.addEventListener("click", (e) => {
const b = e.target.closest("button[data-unit]");
if (b) setUnit(b.dataset.unit);
});
const nav = brand.querySelector(".view-nav");
brand.insertBefore(wrap, nav); // sits just left of the view tabs (top-right)
syncUnitToggle(wrap);
})();