2026-07-11 00:29:47 +00:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "utf-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1" / >
< title > Thermograph — 2-year calendar< / title >
2026-07-11 15:59:14 +00:00
< meta name = "description" content = "Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." / >
< meta name = "theme-color" content = "#f0803c" / >
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
og:image origins in from this request's scheme://host + base path —
crawlers need absolute URLs and don't run JS. -->
< meta property = "og:type" content = "website" / >
< meta property = "og:site_name" content = "Thermograph" / >
< meta property = "og:title" content = "Thermograph — 2-year calendar" / >
< meta property = "og:description" content = "Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." / >
< meta property = "og:url" content = "__ORIGIN__/calendar" / >
< meta property = "og:image" content = "__ORIGIN__/logo.png" / >
< meta property = "og:image:width" content = "512" / >
< meta property = "og:image:height" content = "512" / >
< meta property = "og:image:alt" content = "Thermograph logo" / >
< meta name = "twitter:card" content = "summary" / >
< link rel = "icon" href = "logo.svg" type = "image/svg+xml" / >
< link rel = "apple-touch-icon" href = "logo.png" / >
2026-07-11 00:29:47 +00:00
< link rel = "stylesheet" href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" / >
< link rel = "stylesheet" href = "style.css" / >
< / head >
< body >
2026-07-11 07:20:13 +00:00
< header class = "compact" >
2026-07-11 00:29:47 +00:00
< div class = "brand" >
< span class = "logo" > ▚< / span >
< div >
< h1 > Thermograph · Calendar< / h1 >
< / div >
< nav class = "view-nav" aria-label = "Views" >
< a href = "./" data-view = "map" > Weekly< / a >
< a href = "calendar" data-view = "calendar" class = "active" > Calendar< / a >
< a href = "day" data-view = "day" > Day< / a >
2026-07-11 02:58:56 +00:00
< a href = "compare" data-view = "compare" > Compare< / a >
2026-07-11 00:29:47 +00:00
< / nav >
< / div >
< / header >
< main >
< section class = "controls" >
< div class = "find-bar" >
< button type = "button" id = "find-btn" class = "find-btn" > < / button >
< span id = "loc-label" class = "loc-label" hidden > < / span >
< / div >
< / section >
< div id = "cal-head" class = "cal-head" hidden > < / div >
<!-- Time filters: month/year range + season + year, each a compact dropdown. -->
< div id = "cal-filters" class = "cal-filters" hidden >
< div class = "cal-range" id = "cal-range" >
< div class = "cal-range-head" >
< span class = "cal-filter-label" > Range< / span >
< span class = "hint" > longer spans load 2 years at a time< / span >
< / div >
< label > From < input id = "range-start" type = "month" / > < / label >
< label > To < input id = "range-end" type = "month" / > < / label >
< button type = "button" id = "range-refresh" class = "range-refresh" hidden > Refresh dates ↻< / button >
< / div >
< div id = "cal-seasonyear" class = "cal-seasonyear" > < / div >
< / div >
<!-- Metric selector sits below the time filters, right above the grid it colors. -->
< div id = "metric-block" class = "metric-block" hidden >
< span class = "cal-filter-label" > Color the calendar by< / span >
< div class = "metric-toggle" id = "metric-toggle" role = "group" aria-label = "Color the calendar by" >
< button type = "button" data-metric = "tmax" class = "active" > High< / button >
< button type = "button" data-metric = "tmin" > Low< / button >
< button type = "button" data-metric = "feels" > Feels< / button >
< button type = "button" data-metric = "humid" > Humid< / button >
< button type = "button" data-metric = "wind" > Wind< / button >
< button type = "button" data-metric = "gust" > Gust< / button >
< button type = "button" data-metric = "precip" > Precip< / button >
< button type = "button" data-metric = "dsr" > Dry streak< / button >
< / div >
< / div >
<!-- Category totals for the current metric across the shown (and weather -
filtered) days: a stacked share bar + per-category percentages. -->
< div id = "cal-totals" class = "cal-totals" hidden > < / div >
< div class = "placeholder" id = "cal-placeholder" >
< p > Search a place — or open this from a graded location on the map — to see how
every day of the last two years graded against that spot's own climate history.< / p >
< / div >
< div id = "calendar" class = "calendar" > < / div >
< div id = "cal-key" > < / div >
< div id = "cal-tip" class = "chart-tip" hidden > < / div >
< / main >
< script src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" > < / script >
< script src = "nav.js" > < / script >
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
< script src = "shared.js" > < / script >
2026-07-11 00:29:47 +00:00
< script src = "mappicker.js" > < / script >
< script src = "calendar.js" > < / script >
< / body >
< / html >