Month pages: show record high and low for every metric (#113)

The per-month records section listed only the warmest and coldest temperature.
Extend it to a card per metric (High, Low, Feels-like, Humidity, Wind, Gust,
Precip), each showing that calendar month's record high and low with the date.

Precip is special-cased: a per-day record low is just zero, and the dry-streak
helper would bridge year boundaries on month-filtered rows, so the wettest and
driest sides use the month's largest and smallest total accumulation, dated to
the year. Partial current months (< 26 days) are excluded so an unfinished month
can't win "driest" on a fraction of its rainfall.

Reuses the records page's card grid, so it's already vertical on mobile.

Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62
This commit is contained in:
Emi Griffith 2026-07-15 22:13:51 -07:00 committed by GitHub
parent da94b082c5
commit 59f6e11694
3 changed files with 68 additions and 10 deletions

View file

@ -408,13 +408,39 @@ def _month_context(request, city, history, month_idx: int) -> dict:
stats.append(("Typical low range", f"{_temp(tmin['p10'])} to {_temp(tmin['p90'])}"))
if precip:
stats.append(("Average daily precipitation", f"{precip['mean']:.2f} in"))
# Record high and low for every metric in this calendar month (mirrors the
# all-time records cards, but scoped to the month). Precip is special-cased:
# a per-day "record low" is just zero, and the dry-streak helper would wrongly
# bridge year boundaries on month-filtered rows, so the wettest/driest sides
# show this month's largest and smallest total accumulation, dated to the year.
records = []
if mrec.get("tmax"):
records.append({"label": "Warmest on record", "value": _temp(mrec["tmax"]["max"]),
"date": mrec["tmax"]["max_date"], "cls": temp_class(mrec["tmax"]["max"])})
if mrec.get("tmin"):
records.append({"label": "Coldest on record", "value": _temp(mrec["tmin"]["min"]),
"date": mrec["tmin"]["min_date"], "cls": temp_class(mrec["tmin"]["min"])})
for key, label in METRIC_LABELS:
r = mrec.get(key)
if not r:
continue
if key == "precip":
# Only whole months count — a partial current month would otherwise win
# "driest" on a fraction of its rainfall.
totals = (mdf.filter(pl.col("precip").is_not_null())
.group_by(pl.col("date").dt.year().alias("yr"))
.agg(pl.col("precip").sum().alias("total"),
pl.len().alias("days"))
.filter(pl.col("days") >= 26)
.sort("total"))
if totals.is_empty():
continue
lo, hi = totals.row(0, named=True), totals.row(totals.height - 1, named=True)
records.append({
"label": label,
"high": f"{hi['total']:.1f} in", "high_date": str(hi["yr"]), "high_tag": "Wettest",
"low": f"{lo['total']:.1f} in", "low_date": str(lo["yr"]), "low_tag": "Driest",
})
else:
records.append({
"label": label,
"high": _fmt(key, r["max"]), "high_date": r["max_date"], "high_tag": "Highest",
"low": _fmt(key, r["min"]), "low_date": r["min_date"], "low_tag": "Lowest",
})
prev_i = 12 if month_idx == 1 else month_idx - 1
next_i = 1 if month_idx == 12 else month_idx + 1

View file

@ -24,10 +24,26 @@
{% if records %}
<h2>{{ month_name }} records</h2>
<ul class="climate-records">
{% for r in records %}<li><b>{{ r.label }}:</b>
<span class="t-inline t-{{ r.cls }}">{{ r.value }}</span> on {{ r.date }}</li>{% endfor %}
</ul>
<p class="muted">The most extreme {{ month_name }} on record for each metric, across
{{ year_range[0] }}{{ year_range[1] }}. For rainfall, the wettest and driest are by
total {{ month_name }} accumulation.</p>
<div class="records-grid">
{% for r in records %}
<div class="record-card">
<h3 class="rc-metric">{{ r.label }}</h3>
<div class="rc-row rc-high">
<span class="rc-tag">{{ r.high_tag }}</span>
<span class="rc-val">{{ r.high }}</span>
<span class="rc-date">{{ r.high_date }}</span>
</div>
<div class="rc-row rc-low">
<span class="rc-tag">{{ r.low_tag }}</span>
<span class="rc-val">{{ r.low }}</span>
<span class="rc-date">{{ r.low_date }}</span>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<p><a class="cta" href="{{ base }}/#{{ tool_hash }}">See {{ name }}'s live weather grade →</a></p>

View file

@ -100,6 +100,22 @@ def test_month_and_records(client):
assert client.get(f"{B}/climate/{SLUG}/notamonth").status_code == 404
def test_month_records_cover_every_metric_high_and_low(client):
# The month page's records show a record high AND low for each metric present, not
# just warmest/coldest temperature — one card per metric with both extremes. (The
# test fixture carries tmax/tmin/precip; real cities add feels/humidity/wind/gust.)
b = client.get(f"{B}/climate/{SLUG}/july").text
assert "July records" in b
for label in ("High", "Low", "Precip"):
assert f'class="rc-metric">{label}<' in b
# Every card carries a labelled high and low row (temp metrics use Highest/Lowest).
n = b.count('rc-row rc-high')
assert n >= 3 and b.count('rc-row rc-low') == n
assert "Highest" in b and "Lowest" in b
# Precip's extremes are the wettest/driest whole month by total accumulation.
assert "Wettest" in b and "Driest" in b
def test_records_page_has_monthly_and_seasonal(client):
b = client.get(f"{B}/climate/{SLUG}/records").text
# Monthly records: all 12 months, each linking to its month page.