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:
parent
da94b082c5
commit
59f6e11694
3 changed files with 68 additions and 10 deletions
38
content.py
38
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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue