"use strict";
// Thermograph single-day detail subpage — for one day + location, shows the value
// at every percentile tier boundary in that day-of-year's ±7-day climate window,
// and where the observed values land. Talks to /api/v2/day + /api/v2/geocode.
// Self-contained; re-declares the small shared tier constants (like calendar.js)
// so the page stays independent of app.js.
const TIER_COLORS = {
"rec-hot": "#7d1a49", "very-hot": "#db2777", "hot": "#f472b6", "warm": "#f9a8d4",
"normal": "#f0dce8",
"cool": "#cbb6f7", "cold": "#9b6ef0", "very-cold": "#6d28d9", "rec-cold": "#3b0764",
};
const PRECIP_COLORS = {
"wet-1": "#f3e8ff", "wet-2": "#e9d5ff", "wet-3": "#d8b4fe", "wet-4": "#c084fc", "wet-5": "#a855f7",
"wet-6": "#9333ea", "wet-7": "#7e22ce", "wet-8": "#6b21a8", "wet-9": "#4c1d95",
};
const DRY_COLOR = "#cbb26a";
const fmtTemp = (v) => (v == null ? "—" : `${Math.round(v)}°`);
const fmtPrecip = (v) => (v == null ? "—" : `${v.toFixed(2)}"`);
const fmtWind = (v) => (v == null ? "—" : `${Math.round(v)} mph`);
const fmtHumid = (v) => (v == null ? "—" : `${v.toFixed(1)} g/m³`);
const ord = (n) => {
const r = Math.round(n), s = ["th", "st", "nd", "rd"], v = r % 100;
return r + (s[(v - 20) % 10] || s[v] || s[0]);
};
// Color a tier the same way the calendar cell would be colored for it.
const tierColor = (c) => (c === "dry" ? DRY_COLOR : TIER_COLORS[c] || PRECIP_COLORS[c] || "");
// Monochrome line icons (currentColor) — no emoji, matching the site's dark look.
const WX = (paths) =>
``;
const WX_CLOUD = `
Building the percentile breakdown…
`; dayBody.innerHTML = ""; }, 150); let data; try { // Stale-while-revalidate: a stale cached copy renders immediately; the update // callback repaints if the background revalidation finds new data. data = await window.Thermograph.getJSON( `api/v2/day?lat=${selected.lat}&lon=${selected.lon}${dateQ}`, window.Thermograph.TTL.day, false, (upd) => { if (seq === daySeq) finishDay(upd); }); } catch (err) { clearTimeout(spin); if (seq !== daySeq) return; dayHead.innerHTML = `${err.message}
`; dayBody.innerHTML = ""; return; } clearTimeout(spin); if (seq !== daySeq) return; finishDay(data); } function finishDay(data) { latest = data.latest; curDate = data.detail.date; dateInput.value = curDate; // Allow navigating up to today (recent days beyond the archive come from the // forecast bundle), not just the latest fully-archived day. dateInput.max = todayISO(); document.getElementById("next-day").disabled = curDate >= todayISO(); window.Thermograph.saveLastLocation(selected.lat, selected.lon, curDate); render(data); window.Thermograph.prefetchViews(selected.lat, selected.lon, "day"); // warm weekly/calendar/forecast } function render(data) { const d = data.detail; const cell = data.cell; const place = data.place || `${cell.center_lat.toFixed(3)}, ${cell.center_lon.toFixed(3)}`; locLabel.textContent = place; locLabel.hidden = false; findBtn.innerHTML = `${window.LocationPicker.PIN_ICON} Change location`; const dt = new Date(d.date + "T00:00:00"); const nice = dt.toLocaleDateString(undefined, { weekday: "long", year: "numeric", month: "long", day: "numeric" }); const yrs = d.year_range ? `${d.year_range[0]}–${d.year_range[1]}` : "—"; // Weather summary from the observed high + precip (omitted for days with no obs yet). const th = d.metrics.tmax.obs ? d.metrics.tmax.obs.value : null; const pr = d.metrics.precip.obs ? d.metrics.precip.obs.value : null; const wt = th != null || pr != null ? weatherType(th, pr) : null; dayHead.innerHTML = `${wt.icon} ${wt.text}
` : ""} `; dayBody.innerHTML = [ ladderCard("Daily high", d.metrics.tmax, fmtTemp, "temp"), ladderCard("Daily low", d.metrics.tmin, fmtTemp, "temp"), ladderCard("Feels like", d.metrics.feels, fmtTemp, "temp"), ladderCard("Humidity", d.metrics.humid, fmtHumid, "temp"), ladderCard("Wind speed", d.metrics.wind, fmtWind, "temp"), ladderCard("Wind gust", d.metrics.gust, fmtWind, "temp"), ladderCard("Precipitation", d.metrics.precip, fmtPrecip, "precip"), ].join(""); } // One metric card: an observed summary line + the tier ladder (highest tier on // top). The tier the observed value falls into is highlighted with its value. function ladderCard(title, m, fmt, kind) { if (!m || !m.ladder) return ""; const obs = m.obs; const activeClass = obs ? obs.class : null; let summary; if (obs) { const pct = obs.percentile == null ? "" : ` · ${ord(obs.percentile)} pct`; summary = `${fmt(obs.value)} `; } else { summary = ``; } // Left: a per-metric summary. Right: the bold, right-aligned Historical Range. const noteLeft = kind === "temp" ? `median ${fmt(m.ladder.median)}` : `${m.ladder.rain_days.toLocaleString()} rain days · dry ${m.ladder.dry_pct}%`; const note = `${noteLeft}` + `Historical Range ${fmt(m.ladder.min)}–${fmt(m.ladder.max)}`; const rows = m.ladder.tiers.map((t) => { const active = t.c === activeClass ? " active" : ""; // Dry has no rain percentile — leave that column blank and show the threshold // as the value. Other tiers show their percentile range and value range. let val, pct = t.range; if (t.c === "dry") { val = t.range; pct = ""; } else if (t.hi == null) val = `> ${fmt(t.lo)}`; // top tier: strictly beyond p99 else if (t.lo == null) val = `< ${fmt(t.hi)}`; // bottom tier: strictly below p1 else val = `${fmt(t.lo)}–${fmt(t.hi)}`; const marker = t.c === activeClass && obs ? `◀ ${fmt(obs.value)}` : ""; return `${note}