Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
// Thermograph climate-score subpage — for one location, shows how far the last 6
2026-07-20 05:45:57 +00:00
// years have drifted from the full 45-year record, per metric and season, with an
// equal-weighted overall score and a button-revealed summary. Talks to /api/v2/score.
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
import { loadLastLocation , saveLastLocation , locHash } from "./nav.js" ;
import { fmtTemp , onUnitChange } from "./units.js" ;
2026-07-22 18:50:00 +00:00
import { uv } from "./account.js" ; // header sign-in entry + notification bell, and the API-version resolver
2026-07-22 19:08:57 +00:00
import { loadView , TTL , prefetchViews } from "./cache.js" ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
import { initFindButton , setFindLabel } from "./mappicker.js" ;
2026-07-19 23:57:55 +00:00
import { TIER _COLORS , esc , placeLabel } from "./shared.js" ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
2026-07-19 23:57:55 +00:00
// Display order (site convention leads with Precip).
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
const METRIC _ORDER = [ "precip" , "tmax" , "tmin" , "feels" , "humid" , "wetbulb" , "wind" , "gust" ] ;
const SEASON _NAMES = { djf : "Winter" , mam : "Spring" , jja : "Summer" , son : "Fall" } ;
const SEASON _ORDER = [ "djf" , "mam" , "jja" , "son" ] ;
const tintColor = ( cls ) => TIER _COLORS [ cls ] || "" ;
2026-07-20 04:02:40 +00:00
const tintClass = ( cls ) => ` t- ${ cls } ` ;
// A signed-points chip, colored up (▲) or down (▼): "+3" / "-2".
const signedPts = ( d , suffix = "" ) =>
` <span class="pq-d ${ d >= 0 ? "pq-up" : "pq-down" } "> ${ d >= 0 ? "+" : "" } ${ d } ${ suffix } </span> ` ;
// One tinted score cell (an em-dash when the entry has no score). `strong` bolds
// the number for an Overall row/column; the tint + hover title come from the entry.
function scoreCell ( entry , strong = false ) {
if ( ! entry || entry . score == null ) return ` <td class="t-none">—</td> ` ;
const title = entry . label ? ` ${ entry . label } : ${ entry . grade } ` : entry . grade ;
const num = strong ? ` <strong> ${ entry . score } </strong> ` : ` ${ entry . score } ` ;
return ` <td class=" ${ tintClass ( entry . class ) } " title=" ${ esc ( title ) } "> ${ num } </td> ` ;
}
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
let selected = null ; // {lat, lon}
let lastData = null ; // most recent /score response, for repainting on a unit switch
const placeholder = document . getElementById ( "score-placeholder" ) ;
const scoreHead = document . getElementById ( "score-head" ) ;
const scoreBody = document . getElementById ( "score-body" ) ;
// ---- location picker ----
const findBtn = document . getElementById ( "find-btn" ) ;
const locLabel = document . getElementById ( "loc-label" ) ;
initFindButton ( findBtn , "Find a location" , ( ) => selected ,
( lat , lon ) => {
selected = { lat , lon } ;
locLabel . textContent = ` ${ lat . toFixed ( 3 ) } , ${ lon . toFixed ( 3 ) } ` ;
locLabel . hidden = false ;
fetchScore ( ) ;
} ) ;
// ---- fetch + render ----
let seqCounter = 0 ; // supersedes an in-flight load when the user picks a new spot
async function fetchScore ( ) {
if ( ! selected ) return ;
history . replaceState ( null , "" , locHash ( selected . lat , selected . lon ) ) ;
placeholder . hidden = true ;
scoreHead . hidden = false ;
2026-07-22 19:08:57 +00:00
await loadView ( {
url : uv ( ` score?lat= ${ selected . lat } &lon= ${ selected . lon } ` ) ,
ttl : TTL . score , seq : ++ seqCounter , current : ( ) => seqCounter ,
spinner : ( ) => {
scoreHead . innerHTML = ` <p class="spinner">Scoring 45 years of climate…</p> ` ;
scoreBody . innerHTML = "" ;
} ,
onData : finishScore ,
onError : ( err ) => {
scoreHead . innerHTML = ` <p class="error"> ${ esc ( err . message ) } </p> ` ;
scoreBody . innerHTML = "" ;
} ,
} ) ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
}
function finishScore ( data ) {
saveLastLocation ( selected . lat , selected . lon ) ;
render ( data ) ;
prefetchViews ( selected . lat , selected . lon , [ ] ) ; // warm the other views
}
onUnitChange ( ( ) => { if ( lastData ) render ( lastData ) ; } ) ;
function render ( data ) {
lastData = data ;
const place = placeLabel ( data ) ;
locLabel . textContent = place ;
locLabel . hidden = false ;
setFindLabel ( findBtn , "Change location" ) ;
const s = data . scores ;
if ( s . unavailable ) {
scoreHead . innerHTML = ` <div class="score-hero"><h2> ${ esc ( place ) } </h2>
< p class = "score-sub" > $ { esc ( s . unavailable ) } < / p > < / d i v > ` ;
scoreBody . innerHTML = "" ;
return ;
}
const annual = s . slices . annual ;
const ov = annual . overall ;
const rr = s . recent _range , br = s . baseline _range ;
const recentYrs = ` ${ rr [ 0 ] . slice ( 0 , 4 ) } – ${ rr [ 1 ] . slice ( 0 , 4 ) } ` ;
const baseYrs = ` ${ br [ 0 ] . slice ( 0 , 4 ) } – ${ br [ 1 ] . slice ( 0 , 4 ) } ` ;
// ---- hero: the overall annual score ----
scoreHead . innerHTML = `
< div class = "score-hero" style = "--cat:${tintColor(ov.class)}" >
2026-07-20 04:02:40 +00:00
< p class = "section-title score-eyebrow" > Climate - shift score · $ { esc ( place ) } < / p >
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
< div class = "score-hero-row" >
< span class = "score-num" > $ { ov . score } < / s p a n >
< div class = "score-hero-label" >
< span class = "score-grade" > $ { esc ( ov . grade ) } < / s p a n >
2026-07-19 23:37:42 +00:00
< span class = "score-sub" > Net change · $ { recentYrs } vs $ { baseYrs } · $ { s . n _recent . toLocaleString ( ) } recent days scored < / s p a n >
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
< / d i v >
< / d i v >
2026-07-19 23:37:42 +00:00
< p class = "score-note" > A net - change total across all metrics : 0 = no change from the long - term
2026-07-20 05:45:57 +00:00
normal · 100 = an extreme shift . Every metric counts equally . < / p >
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
< / d i v > ` ;
// ---- per-metric cards (annual) ----
const cards = METRIC _ORDER . map ( ( k ) => metricCard ( annual . metrics [ k ] ) ) . join ( "" ) ;
2026-07-19 23:37:42 +00:00
const wbNote = wetbulbNote ( annual . metrics . wetbulb , baseYrs ) ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
2026-07-19 23:41:03 +00:00
// ---- by-season table (seasons × metrics, columns aligned) ----
const seasonHead = ` <tr><th></th><th>Overall</th> ${ METRIC _ORDER . map ( ( k ) =>
` <th> ${ shortLabel ( k ) } </th> ` ) . join ( "" ) } < / t r > ` ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
const seasonRows = SEASON _ORDER . map ( ( sk ) => {
const sl = s . slices [ sk ] ;
2026-07-20 04:02:40 +00:00
const cells = METRIC _ORDER . map ( ( k ) => scoreCell ( sl . metrics [ k ] ) ) . join ( "" ) ;
return ` <tr><th> ${ SEASON _NAMES [ sk ] } </th> ${ scoreCell ( sl . overall , true ) } ${ cells } </tr> ` ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
} ) . join ( "" ) ;
scoreBody . innerHTML = `
< section class = "normals" > $ { cards } < / s e c t i o n >
2026-07-19 23:37:42 +00:00
$ { wbNote }
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
< section class = "score-section" >
< p class = "section-title" > By season < / p >
2026-07-20 04:02:40 +00:00
< div class = "table-wrap" > < table class = "score-table season-table" >
2026-07-19 23:41:03 +00:00
< thead > $ { seasonHead } < / t h e a d > < t b o d y > $ { s e a s o n R o w s } < / t b o d y > < / t a b l e > < / d i v >
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
< / s e c t i o n >
< button type = "button" class = "summary-toggle" aria - expanded = "false" aria - controls = "score-summary" >
Summary < span class = "summary-caret" aria - hidden = "true" > ▾ < / s p a n > < / b u t t o n >
< section id = "score-summary" hidden > $ { summaryHTML ( s ) } < / s e c t i o n > ` ;
wireSummaryToggle ( ) ;
}
2026-07-19 23:37:42 +00:00
// Explains what wet bulb is and reports the share of heat-stress ("wet-bulb")
// days vs normal days, recent vs baseline. Re-rendered on unit change.
function wetbulbNote ( m , baseYrs ) {
2026-07-20 00:22:38 +00:00
const def = ` <strong>Wet bulb</strong> is the lowest temperature that evaporating sweat can cool you to.
When it climbs , sweat can ' t shed heat fast enough and hot days turn dangerous . ` ;
2026-07-19 23:37:42 +00:00
if ( ! m || m . score == null || ! m . freq ) {
return ` <p class="score-callout"> ${ def } </p> ` ;
}
const thr = fmtTemp ( m . freq . threshold _f ) ;
const { f6 , f45 , d } = m . freq ;
2026-07-20 04:02:40 +00:00
const delta = d ? ` ( ${ signedPts ( d , " pts" ) } ) ` : "" ;
2026-07-19 23:37:42 +00:00
return ` <p class="score-callout"> ${ def }
Heat - stress days ( peak wet bulb ≥ $ { esc ( String ( thr ) ) } ) : < strong > $ { f6 } % < / s t r o n g > o f r e c e n t d a y s
Remove em-dashes from site copy and tighten the prose (#206)
Replace em-dashes in user-facing copy across the server-rendered pages,
static frontend views, and the strings the app injects at runtime, using
colons, commas, parentheses or full stops as the context wants. Data
placeholder glyphs (a lone "—" for a missing reading) are left alone,
since a hyphen there reads as a minus sign in temperature columns.
Also tighten the high-visibility surfaces (home hero and meta, about,
privacy, city and records ledes, glossary blurbs) toward a plainer,
more direct voice while keeping every factual claim intact.
Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
2026-07-20 01:48:33 +00:00
vs < strong > $ { f45 } % < / s t r o n g > a c r o s s $ { b a s e Y r s } $ { d e l t a } . T h e r e s t a r e n o r m a l . < / p > ` ;
2026-07-19 23:37:42 +00:00
}
2026-07-20 00:22:38 +00:00
// One metric score card. The tier and its direction stack under the score on
// their own lines (rather than crammed top-right), so a long grade like
// "Extreme shift — windier" never clusters in the corner on a narrow card.
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
function metricCard ( m ) {
if ( m . score == null ) {
2026-07-20 00:22:38 +00:00
return ` <section class="normal-card score-card">
< h3 > $ { esc ( m . label ) } < / h 3 >
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
< div class = "big" style = "--cat:var(--muted)" > — < / d i v >
< div class = "range" > $ { esc ( m . reason || "no data" ) } < / d i v >
< / s e c t i o n > ` ;
}
2026-07-20 00:22:38 +00:00
return ` <section class="normal-card score-card" style="--cat: ${ tintColor ( m . class ) } ">
< h3 > $ { esc ( m . label ) } < / h 3 >
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
< div class = "big" > $ { m . score } < / d i v >
2026-07-20 00:22:38 +00:00
< div class = "nc-tier" > $ { esc ( m . tier ) } < / d i v >
< div class = "range" > $ { scoreDetail ( m ) } < / d i v >
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
< / s e c t i o n > ` ;
}
2026-07-20 00:22:38 +00:00
// The muted detail line under a card's tier — the signed drift, or a steady note.
function scoreDetail ( m ) {
if ( m . score < 15 ) return "close to the long-term normal" ;
return ` ~ ${ Math . abs ( m . bias ) . toFixed ( 0 ) } pts ${ esc ( m . direction ) } ` ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
}
const shortLabel = ( k ) => ( { precip : "Precip" , tmax : "High" , tmin : "Low" , feels : "Feels" ,
humid : "Humid" , wetbulb : "Wet bulb" , wind : "Wind" , gust : "Gust" } ) [ k ] || k ;
// ---- summary (revealed by the button) ----
function summaryHTML ( s ) {
return sentencesHTML ( s . slices . annual ) + scoreTableHTML ( s ) + perQTableHTML ( s . slices . annual ) ;
}
// A plain-language sentence per metric that actually shifted.
function sentencesHTML ( annual ) {
const lines = METRIC _ORDER
. map ( ( k ) => annual . metrics [ k ] )
. filter ( ( m ) => m && m . score != null && m . score >= 15 )
. map ( ( m ) => {
const mid = m . per _q && m . per _q . find ( ( q ) => q . q === 50 ) ;
2026-07-19 23:57:55 +00:00
const where = mid ? ` At the median, recent readings sit about ${ Math . abs ( mid . d ) } points ${ mid . d >= 0 ? "higher" : "lower" } than the seasonal norm. ` : "" ;
Remove em-dashes from site copy and tighten the prose (#206)
Replace em-dashes in user-facing copy across the server-rendered pages,
static frontend views, and the strings the app injects at runtime, using
colons, commas, parentheses or full stops as the context wants. Data
placeholder glyphs (a lone "—" for a missing reading) are left alone,
since a hyphen there reads as a minus sign in temperature columns.
Also tighten the high-visibility surfaces (home hero and meta, about,
privacy, city and records ledes, glossary blurbs) toward a plainer,
more direct voice while keeping every factual claim intact.
Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
2026-07-20 01:48:33 +00:00
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> ` ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
} ) ;
Remove em-dashes from site copy and tighten the prose (#206)
Replace em-dashes in user-facing copy across the server-rendered pages,
static frontend views, and the strings the app injects at runtime, using
colons, commas, parentheses or full stops as the context wants. Data
placeholder glyphs (a lone "—" for a missing reading) are left alone,
since a hyphen there reads as a minus sign in temperature columns.
Also tighten the high-visibility surfaces (home hero and meta, about,
privacy, city and records ledes, glossary blurbs) toward a plainer,
more direct voice while keeping every factual claim intact.
Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
2026-07-20 01:48:33 +00:00
if ( ! lines . length ) return ` <p class="score-summary-lead">Every metric is close to its long-term normal here, with no notable recent drift.</p> ` ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
return ` <p class="score-summary-lead">What has shifted the most:</p><ul class="score-sentences"> ${ lines . join ( "" ) } </ul> ` ;
}
// Metrics × slices table of scores, each cell tinted by its tier.
function scoreTableHTML ( s ) {
const slices = [ "annual" , ... SEASON _ORDER ] ;
const head = ` <tr><th>Metric</th> ${ slices . map ( ( sk ) =>
` <th> ${ sk === "annual" ? "Annual" : SEASON _NAMES [ sk ] } </th> ` ) . join ( "" ) } < / t r > ` ;
const rows = METRIC _ORDER . map ( ( k ) => {
2026-07-20 04:02:40 +00:00
const cells = slices . map ( ( sk ) => scoreCell ( s . slices [ sk ] . metrics [ k ] ) ) . join ( "" ) ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
const label = s . slices . annual . metrics [ k ] ? . label || k ;
return ` <tr><th> ${ esc ( label ) } </th> ${ cells } </tr> ` ;
} ) . join ( "" ) ;
2026-07-20 04:02:40 +00:00
const overallCells = slices . map ( ( sk ) => scoreCell ( s . slices [ sk ] . overall , true ) ) . join ( "" ) ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
const overallRow = ` <tr class="score-table-overall"><th>Overall</th> ${ overallCells } </tr> ` ;
return ` <p class="section-title">All scores</p>
2026-07-20 04:02:40 +00:00
< div class = "table-wrap" > < table class = "score-table" >
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
< thead > $ { head } < / t h e a d > < t b o d y > $ { r o w s } $ { o v e r a l l R o w } < / t b o d y > < / t a b l e > < / d i v > ` ;
}
2026-07-19 23:57:55 +00:00
// 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.
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
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 ) => {
2026-07-20 04:02:40 +00:00
const cells = m . per _q . map ( ( q ) => ` <td>p ${ q . q } : ${ signedPts ( q . d ) } </td> ` ) . join ( "" ) ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
return ` <tr><th> ${ esc ( m . label ) } </th> ${ cells } </tr> ` ;
} ) . join ( "" ) ;
if ( ! rows ) return "" ;
2026-07-19 23:57:55 +00:00
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 >
2026-07-20 04:02:40 +00:00
< div class = "table-wrap" > < table class = "score-table pq-table" > < tbody > $ { rows } < / t b o d y > < / t a b l e > < / d i v > ` ;
Add a climate-score page from recent-vs-baseline percentile divergence (#196)
Score how far a location's last 6 years have drifted from its full 45-year
record. For each metric and percentile category (p10/p25/p50/p75/p90), the
recent-years value is placed on the baseline distribution and the gap from the
expected percentile is the divergence — unit-free, so metrics compare directly.
Scored per meteorological season plus annual, weighted into per-metric and
overall scores (temps, humidity and feels-like weighted heaviest).
- backend/scoring.py: divergence math, seasonal slicing, precip zero-inflation
split (wet-day frequency + amount), tier mapping onto the existing temp scale.
- climate.py: derive a wet-bulb column (Stull 2011) at the read boundary, before
the humidity column is converted to absolute — via a shared _derive_metrics
wrapper at all four read sites.
- api/v2/score endpoint + build_score payload, cached on the history token with
a scoring-version key.
- frontend score page: overall hero, per-metric cards, by-season chips, and a
button-revealed summary (sentences + metrics×season table + per-percentile
detail). Score nav link across all headers.
- Tests for the scoring math, wet-bulb formula, payload shape and route.
2026-07-19 23:02:33 +00:00
}
// Mirror of shared.js initSeasonExpand: flip [hidden] + aria-expanded.
function wireSummaryToggle ( ) {
const btn = scoreBody . querySelector ( ".summary-toggle" ) ;
const panel = scoreBody . querySelector ( "#score-summary" ) ;
if ( ! btn || ! panel ) return ;
btn . addEventListener ( "click" , ( ) => {
const show = panel . hidden ;
panel . hidden = ! show ;
btn . setAttribute ( "aria-expanded" , String ( show ) ) ;
} ) ;
}
// ---- restore (hash from a shared link, else the last place you looked at) ----
( function restore ( ) {
const loc = loadLastLocation ( ) ;
if ( loc ) {
selected = { lat : loc . lat , lon : loc . lon } ;
fetchScore ( ) ;
}
} ) ( ) ;