Score each metric on the average of its seasonal differentials (#200)
Annual scores were computed from an all-year pooled distribution, which widens the reference spread and hides a shift confined to one season — Seattle's daily high read 16 despite a summer high of 65. Build each metric's annual score (and the overall) as the mean of its four seasonal divergences instead, so a real seasonal shift shows through (that high now reads 28). Frequency read-outs (precip wet days, wet-bulb heat-stress days) stay pooled over the year. Also lock the by-season table to fixed, uniform columns (min-width to scroll on a phone) so each metric lines up vertically across the seasons, and show the per-percentile detail as the season-averaged shift. Bumps the score cache version.
This commit is contained in:
parent
c6b2e55ff2
commit
c008ece671
2 changed files with 14 additions and 17 deletions
|
|
@ -6,15 +6,10 @@ 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, pctOrd, fmtPrecip, fmtWind, fmtHumid, esc, placeLabel } from "./shared.js";
|
||||
import { TIER_COLORS, esc, placeLabel } from "./shared.js";
|
||||
|
||||
// Display order (site convention leads with Precip), and how each metric's raw
|
||||
// per-percentile value is formatted in the detail table.
|
||||
// Display order (site convention leads with Precip).
|
||||
const METRIC_ORDER = ["precip", "tmax", "tmin", "feels", "humid", "wetbulb", "wind", "gust"];
|
||||
const FMT = {
|
||||
tmax: fmtTemp, tmin: fmtTemp, feels: fmtTemp, wetbulb: fmtTemp,
|
||||
humid: fmtHumid, wind: fmtWind, gust: fmtWind, precip: fmtPrecip,
|
||||
};
|
||||
const SEASON_NAMES = { djf: "Winter", mam: "Spring", jja: "Summer", son: "Fall" };
|
||||
const SEASON_ORDER = ["djf", "mam", "jja", "son"];
|
||||
|
||||
|
|
@ -210,8 +205,8 @@ function sentencesHTML(annual) {
|
|||
.filter((m) => m && m.score != null && m.score >= 15)
|
||||
.map((m) => {
|
||||
const mid = m.per_q && m.per_q.find((q) => q.q === 50);
|
||||
const where = mid ? ` Its recent median now lands near the ${pctOrd(mid.pct)} percentile of the full record.` : "";
|
||||
return `<li><strong>${esc(m.label)}:</strong> ${esc(m.grade)} — about ${Math.abs(m.bias).toFixed(0)} percentile points ${esc(m.direction)}.${where}</li>`;
|
||||
const where = mid ? ` At the median, recent readings sit about ${Math.abs(mid.d)} points ${mid.d >= 0 ? "higher" : "lower"} than the seasonal norm.` : "";
|
||||
return `<li><strong>${esc(m.label)}:</strong> ${esc(m.grade)} — about ${Math.abs(m.bias).toFixed(0)} percentile points ${esc(m.direction)} on average.${where}</li>`;
|
||||
});
|
||||
if (!lines.length) return `<p class="score-summary-lead">Every metric is close to its long-term normal here — no notable recent drift.</p>`;
|
||||
return `<p class="score-summary-lead">What has shifted the most:</p><ul class="score-sentences">${lines.join("")}</ul>`;
|
||||
|
|
@ -242,21 +237,20 @@ function scoreTableHTML(s) {
|
|||
<thead>${head}</thead><tbody>${rows}${overallRow}</tbody></table></div>`;
|
||||
}
|
||||
|
||||
// The annual per-percentile detail: for each category, the recent value and where
|
||||
// it lands in the 45-year record.
|
||||
// The per-percentile detail: for each band, the mean shift (in percentile points)
|
||||
// averaged across the four seasons — the differentials the score is built from.
|
||||
function perQTableHTML(annual) {
|
||||
const rows = METRIC_ORDER
|
||||
.map((k) => annual.metrics[k])
|
||||
.filter((m) => m && m.score != null && m.per_q && m.per_q.length)
|
||||
.map((m) => {
|
||||
const fmt = FMT[m.key] || ((v) => v);
|
||||
const cells = m.per_q.map((q) =>
|
||||
`<td>p${q.q}: ${esc(String(fmt(q.v6)))} → ${pctOrd(q.pct)} <span class="pq-d ${q.d >= 0 ? "pq-up" : "pq-down"}">(${q.d >= 0 ? "+" : ""}${q.d})</span></td>`).join("");
|
||||
`<td>p${q.q}: <span class="pq-d ${q.d >= 0 ? "pq-up" : "pq-down"}">${q.d >= 0 ? "+" : ""}${q.d}</span></td>`).join("");
|
||||
return `<tr><th>${esc(m.label)}</th>${cells}</tr>`;
|
||||
}).join("");
|
||||
if (!rows) return "";
|
||||
return `<p class="section-title">Annual percentile detail</p>
|
||||
<p class="score-note">Each recent-years percentile value, and where it lands in the full record (▲/▼ = shift in percentile points).</p>
|
||||
return `<p class="section-title">Shift by percentile band</p>
|
||||
<p class="score-note">The mean shift at each percentile band, averaged across all four seasons (percentile points; ▲ higher / ▼ lower than the seasonal norm).</p>
|
||||
<div class="score-table-wrap"><table class="score-table pq-table"><tbody>${rows}</tbody></table></div>`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1737,8 +1737,11 @@ td.rec-date { color: var(--muted); font-size: 13px; }
|
|||
padding: 12px 14px; margin: 14px 0 4px; font-size: 13px; line-height: 1.5; color: var(--text);
|
||||
}
|
||||
|
||||
/* By-season scores: seasons as rows, one aligned column per metric. */
|
||||
.season-table th:first-child, .season-table td:first-child { text-align: left; }
|
||||
/* By-season scores: seasons as rows, one uniform column per metric so every
|
||||
metric lines up vertically across the seasons. Fixed layout keeps the columns
|
||||
equal width; the min-width lets it scroll (not squash) on a phone. */
|
||||
.season-table { table-layout: fixed; width: 100%; min-width: 620px; max-width: 740px; }
|
||||
.season-table th:first-child, .season-table td:first-child { width: 62px; text-align: left; }
|
||||
|
||||
/* Summary reveal — pill button + caret, mirrors the season-expand pattern. */
|
||||
.summary-toggle {
|
||||
|
|
|
|||
Loading…
Reference in a new issue