diff --git a/content.py b/content.py index 16f2347..391a361 100644 --- a/content.py +++ b/content.py @@ -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 diff --git a/templates/month.html.j2 b/templates/month.html.j2 index c4fa3c1..8d5e8ce 100644 --- a/templates/month.html.j2 +++ b/templates/month.html.j2 @@ -24,10 +24,26 @@ {% if records %}

{{ month_name }} records

- +

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.

+
+ {% for r in records %} +
+

{{ r.label }}

+
+ {{ r.high_tag }} + {{ r.high }} + {{ r.high_date }} +
+
+ {{ r.low_tag }} + {{ r.low }} + {{ r.low_date }} +
+
+ {% endfor %} +
{% endif %}

See {{ name }}'s live weather grade →

diff --git a/tests/test_content.py b/tests/test_content.py index 130ccc2..812757f 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -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.