diff --git a/static/app.js b/static/app.js index b5d4fb7..f2f1857 100644 --- a/static/app.js +++ b/static/app.js @@ -7,7 +7,7 @@ import { initFindButton, setFindLabel } from "./mappicker.js"; import { W, PW, H, PL, PR, plotTop, plotBot, xLabY, setChartWidth, chartPalette, tempChart, precipChart, dryChart, attachChartHover } from "./chart.js"; import { track } from "./digest.js"; -import { TIER_COLORS, SCALE_TEMP, drynessColor, ord, esc, todayISO, placeLabel, +import { TIER_COLORS, SCALE_TEMP, drynessColor, pctOrd, esc, todayISO, placeLabel, fmtPrecip, fmtWind, fmtHumid } from "./shared.js"; let selected = null; // {lat, lon} @@ -124,7 +124,7 @@ function updateHero(data) { document.getElementById("hero-where").textContent = `Today in ${placeLabel(data)}`; document.getElementById("hero-band").textContent = g.grade; document.getElementById("hero-detail").textContent = - `${targetDay.tmax === g ? "High temp" : "Low temp"} in the ${ord(Math.round(g.percentile))} percentile`; + `${targetDay.tmax === g ? "High temp" : "Low temp"} in the ${pctOrd(g.percentile)} percentile`; // Band colour rides the same token set as everything else; the band word is // always present too, so colour is never the only signal. card.style.setProperty("--cat", TIER_COLORS[g.class] || ""); diff --git a/static/calendar.js b/static/calendar.js index 614ca1f..f984ed5 100644 --- a/static/calendar.js +++ b/static/calendar.js @@ -6,7 +6,7 @@ import { fmtTemp, onUnitChange } from "./units.js"; import "./account.js"; // header sign-in entry + notification bell import { TTL, chunkedFetch, prefetchViews } from "./cache.js"; import { initFindButton, setFindLabel } from "./mappicker.js"; -import { TIER_COLORS, PRECIP_COLORS, SCALE_TEMP, SCALE_RAIN, drynessColor, ord, +import { TIER_COLORS, PRECIP_COLORS, SCALE_TEMP, SCALE_RAIN, drynessColor, pctOrd, fmtPrecip, fmtWind, fmtHumid, MONTHS, isoOfDate, monthStart, monthEnd, CHUNK_MONTHS, buildChunks, clickOpensPicker, metricBuckets, distStrip, seasonFilterDropdown, seasonSummaryText, syncSeasonChecks, @@ -620,7 +620,7 @@ function attachHover(byDate) { const line = (key, name, g, fmt, color) => { const rc = key === activeKey ? " tt-r-active" : ""; if (!g) return `${name}`; - const pct = g.pct == null ? "" : ` · ${ord(g.pct)} pct`; + const pct = g.pct == null ? "" : ` · ${pctOrd(g.pct)} pct`; const catCls = key === activeKey ? "tt-cat tt-cat-active" : "tt-cat"; const style = color ? ` style="--cat:${color}"` : ""; return `${name}${fmt(g.v)}${pct}${g.g}`; diff --git a/static/chart.js b/static/chart.js index 225f26a..13939a9 100644 --- a/static/chart.js +++ b/static/chart.js @@ -3,7 +3,7 @@ // palette, and the pointer-driven hover. Extracted from app.js so any page can // plot a metric series; app.js keeps only its page orchestration (buildChart). import { fmtTemp, toUnit } from "./units.js"; -import { TIER_COLORS, drynessColor, ord } from "./shared.js"; +import { TIER_COLORS, drynessColor, pctOrd } from "./shared.js"; // #rrggbb -> rgba() with alpha, so band tints derive from the same site colors. const hexA = (hex, a) => { @@ -165,7 +165,7 @@ function lineChart(cfg) { const vy = (y - plotTop < 26 ? y + 15 : y - 15).toFixed(1); // flip below near the top const pv = getPct ? getPct(d) : null; const pct = pv == null ? "" : - `${ord(pv)}`; + `${pctOrd(pv)}`; return `${fmtLabel(v)}${pct}`; }).join(""); @@ -247,7 +247,7 @@ export function attachChartHover(wrap, days) { const C = chartPalette(); const pctLine = (label, g, unit, color) => { if (!g) return `
${label}
`; - const pct = g.percentile == null ? "" : ` · ${ord(g.percentile)} pct`; + const pct = g.percentile == null ? "" : ` · ${pctOrd(g.percentile)} pct`; const gc = TIER_COLORS[g.class] || ""; // "°" marks a temperature line — show it in the active unit; others are as-is. const val = unit === "°" ? Math.round(toUnit(g.value)) : g.value; diff --git a/static/day.js b/static/day.js index b1d3fc5..8a238b8 100644 --- a/static/day.js +++ b/static/day.js @@ -6,7 +6,7 @@ import { fmtTemp, onUnitChange } from "./units.js"; import "./account.js"; // header sign-in entry + notification bell import { getJSON, TTL, prefetchViews } from "./cache.js"; import { initFindButton, setFindLabel } from "./mappicker.js"; -import { TIER_COLORS, PRECIP_COLORS, DRY_COLOR, ord, fmtPrecip, fmtWind, fmtHumid, +import { TIER_COLORS, PRECIP_COLORS, DRY_COLOR, pctOrd, fmtPrecip, fmtWind, fmtHumid, todayISO, weatherType, placeLabel } from "./shared.js"; // Color a tier the same way the calendar cell would be colored for it. @@ -158,7 +158,7 @@ function ladderCard(title, m, fmt, kind) { let summary; if (obs) { - const pct = obs.percentile == null ? "" : ` · ${ord(obs.percentile)} pct`; + const pct = obs.percentile == null ? "" : ` · ${pctOrd(obs.percentile)} pct`; summary = `${fmt(obs.value)} ${obs.grade}${pct}`; } else { diff --git a/static/shared.js b/static/shared.js index 5819927..91bad7c 100644 --- a/static/shared.js +++ b/static/shared.js @@ -180,6 +180,19 @@ export 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]); }; + +// A percentile as a display ordinal. Mirrors grading.pct_ordinal() in the +// backend — floored into 1..99, because an empirical percentile is a rank +// against the sample and can round to 100 (or 0), and "100th percentile" reads +// as a measurement error rather than "as extreme as it has ever been". +// Every percentile shown anywhere goes through this, so the Day page, the +// calendar tooltip, the chart, the city pages and the homepage strip can't +// disagree about the same reading. +export const pctOrd = (n) => { + const v = Number(n); + if (!Number.isFinite(v)) return "—"; + return ord(Math.min(99, Math.max(1, Math.round(v)))); +}; export const todayISO = () => new Date().toISOString().slice(0, 10); export const esc = (s) => String(s).replace(/&/g, "&").replace(//g, ">"); // The heading label for a graded response: the resolved place name, or the