diff --git a/city_events.py b/city_events.py index 23cca91..9fec566 100644 --- a/city_events.py +++ b/city_events.py @@ -13,7 +13,7 @@ EVENTS: dict[str, dict] = { "text": "In August 2005, Hurricane Katrina overwhelmed the city's levees and flooded roughly 80% of New Orleans, one of the deadliest and costliest disasters in U.S. history.", "url": "https://en.wikipedia.org/wiki/Effects_of_Hurricane_Katrina_in_New_Orleans"}, "houston-texas-us": { - "text": "In August 2017, Hurricane Harvey stalled over the region and dropped more than 50 inches of rain — a record for a U.S. tropical cyclone — causing catastrophic flooding across Houston.", + "text": "In August 2017, Hurricane Harvey stalled over the region and dropped more than 50 inches of rain, a record for a U.S. tropical cyclone, causing catastrophic flooding across Houston.", "url": "https://en.wikipedia.org/wiki/Hurricane_Harvey"}, "chicago-illinois-us": { "text": "A brutal heat wave in July 1995 pushed the heat index past 120°F and caused an estimated 700 heat-related deaths in Chicago, reshaping how the city prepares for extreme heat.", @@ -28,7 +28,7 @@ EVENTS: dict[str, dict] = { "text": "The Blizzard of 1978 paralyzed Boston for a week, dropping about 27 inches of snow, stranding thousands of motorists, and flooding coastal neighborhoods.", "url": "https://en.wikipedia.org/wiki/Northeastern_United_States_blizzard_of_1978"}, "seattle-washington-us": { - "text": "During the June 2021 Western North America heat wave, Seattle hit an all-time record 108°F — a temperature the normally mild city had never before approached.", + "text": "During the June 2021 Western North America heat wave, Seattle hit an all-time record 108°F, a temperature the normally mild city had never before approached.", "url": "https://en.wikipedia.org/wiki/2021_Western_North_America_heat_wave"}, "portland-oregon-us": { "text": "In the June 2021 heat dome, Portland reached an all-time record 116°F across three consecutive record-shattering days.", @@ -40,7 +40,7 @@ EVENTS: dict[str, dict] = { "text": "Deep in Tornado Alley, the Oklahoma City area was struck by the May 3, 1999 tornado, which produced the highest wind speed ever measured on Earth, around 302 mph.", "url": "https://en.wikipedia.org/wiki/1999_Bridge_Creek%E2%80%93Moore_tornado"}, "los-angeles-california-us": { - "text": "In January 2025, wind-driven wildfires — chiefly the Palisades and Eaton fires — swept through Los Angeles, destroying thousands of structures in one of the most destructive fire disasters in California's history.", + "text": "In January 2025, wind-driven wildfires, chiefly the Palisades and Eaton fires, swept through Los Angeles, destroying thousands of structures in one of the most destructive fire disasters in California's history.", "url": "https://en.wikipedia.org/wiki/January_2025_Southern_California_wildfires"}, # --- Canada --- @@ -65,7 +65,7 @@ EVENTS: dict[str, dict] = { "text": "In the summer of 2010, a record heat wave and surrounding peat and forest fires smothered Moscow in smog for weeks, with temperatures near 38°C and thousands of excess deaths.", "url": "https://en.wikipedia.org/wiki/2010_Russian_wildfires"}, "madrid-es": { - "text": "In January 2021, Storm Filomena buried Madrid under its heaviest snowfall in decades — around 50 cm — paralyzing the city for days.", + "text": "In January 2021, Storm Filomena buried Madrid under its heaviest snowfall in decades, around 50 cm, paralyzing the city for days.", "url": "https://en.wikipedia.org/wiki/Storm_Filomena"}, "athens-attica-gr": { "text": "In July 2018, wind-driven wildfires swept through Attica near Athens during extreme heat, killing more than 100 people in Greece's deadliest fire disaster.", @@ -76,7 +76,7 @@ EVENTS: dict[str, dict] = { "text": "During the 2019–20 'Black Summer' bushfires, hazardous smoke repeatedly blanketed Sydney, pushing its air quality to some of the worst of any major city.", "url": "https://en.wikipedia.org/wiki/2019%E2%80%9320_Australian_bushfire_season"}, "melbourne-victoria-au": { - "text": "On 'Black Saturday' in February 2009, extreme heat and wind drove bushfires across Victoria, around Melbourne, that killed 173 people — Australia's deadliest bushfires.", + "text": "On 'Black Saturday' in February 2009, extreme heat and wind drove bushfires across Victoria, around Melbourne, that killed 173 people in Australia's deadliest bushfires.", "url": "https://en.wikipedia.org/wiki/Black_Saturday_bushfires"}, "brisbane-queensland-au": { "text": "The 2010–11 Queensland floods peaked in January 2011 when the Brisbane River burst its banks, flooding thousands of homes and businesses across the city.", @@ -90,7 +90,7 @@ EVENTS: dict[str, dict] = { "text": "The December 2015 South Indian floods submerged much of Chennai after its heaviest rains in over a century, displacing hundreds of thousands.", "url": "https://en.wikipedia.org/wiki/2015_South_India_floods"}, "kolkata-west-bengal-in": { - "text": "In May 2020, Cyclone Amphan — the most powerful storm recorded in the Bay of Bengal — struck Kolkata directly, uprooting trees and cutting power across the city.", + "text": "In May 2020, Cyclone Amphan, the most powerful storm recorded in the Bay of Bengal, struck Kolkata directly, uprooting trees and cutting power across the city.", "url": "https://en.wikipedia.org/wiki/Cyclone_Amphan"}, "karachi-sindh-pk": { "text": "A severe heat wave in June 2015 killed an estimated 1,200 people in Karachi as temperatures reached about 45°C during Ramadan.", @@ -114,7 +114,7 @@ EVENTS: dict[str, dict] = { "text": "Super Typhoon Mangkhut battered Hong Kong in September 2018 with fierce winds and a storm surge, flooding low-lying districts and swaying skyscrapers.", "url": "https://en.wikipedia.org/wiki/Typhoon_Mangkhut"}, "tokyo-jp": { - "text": "In October 2019, Typhoon Hagibis — one of the most powerful to strike Japan in decades — brought record rainfall and widespread flooding to the Tokyo region.", + "text": "In October 2019, Typhoon Hagibis, one of the most powerful to strike Japan in decades, brought record rainfall and widespread flooding to the Tokyo region.", "url": "https://en.wikipedia.org/wiki/Typhoon_Hagibis"}, "seoul-kr": { "text": "In August 2022, record-breaking rainfall flooded Seoul, submerging roads and low-lying semi-basement homes and killing several people across the capital region.", @@ -122,7 +122,7 @@ EVENTS: dict[str, dict] = { # --- Middle East --- "dubai-ae": { - "text": "In April 2024, Dubai was deluged by about two years' worth of rain in a single day — its heaviest on record — flooding highways and bringing its international airport to a standstill.", + "text": "In April 2024, Dubai was deluged by about two years' worth of rain in a single day, its heaviest on record, flooding highways and bringing its international airport to a standstill.", "url": "https://en.wikipedia.org/wiki/2024_United_Arab_Emirates_floods"}, "jeddah-mecca-region-sa": { "text": "In November 2009, sudden flash floods swept through Jeddah after record rainfall, killing more than 100 people in one of the city's worst modern disasters.", @@ -138,7 +138,7 @@ EVENTS: dict[str, dict] = { # --- Africa --- "cape-town-western-cape-za": { - "text": "From 2015 to 2018 a severe drought pushed Cape Town toward 'Day Zero' — the day municipal taps would be shut off — narrowly averted through drastic water conservation.", + "text": "From 2015 to 2018 a severe drought pushed Cape Town toward 'Day Zero', the day municipal taps would be shut off, narrowly averted through drastic water conservation.", "url": "https://en.wikipedia.org/wiki/Cape_Town_water_crisis"}, "durban-kwazulu-natal-za": { "text": "In April 2022, extreme rainfall triggered catastrophic floods and landslides around Durban, killing more than 400 people in one of South Africa's deadliest storms.", diff --git a/climate.py b/climate.py index 884391a..6c4d359 100644 --- a/climate.py +++ b/climate.py @@ -100,9 +100,9 @@ class WeatherUnavailable(RuntimeError): def limit_message(daily: bool) -> str: """The user-facing rate-limit copy, in one place.""" if daily: - return ("Open-Meteo's daily request limit is exhausted — new locations will work again " + return ("Open-Meteo's daily request limit is exhausted, so new locations will work again " "tomorrow. Places you've already viewed still work.") - return "The weather service is rate-limited right now — please try again in a minute." + return "The weather service is rate-limited right now. Please try again in a minute." def is_rate_limit(e) -> bool: diff --git a/content.py b/content.py index bcaa6a6..de414bc 100644 --- a/content.py +++ b/content.py @@ -796,7 +796,7 @@ def hub_page(request: Request) -> Response: "n_countries": len(groups), "canonical_path": "/climate", "breadcrumb": [("Home", f"{BASE}/"), ("Climate", None)], - "page_title": "City climate pages — averages, records and how today compares", + "page_title": "City climate pages: averages, records and how today compares", "page_description": ("Browse climate pages for hundreds of cities worldwide: average temperatures by " "month, all-time records, and how today's weather compares to local history."), } @@ -810,19 +810,19 @@ GLOSSARY: dict[str, dict] = { "short": "The typical value of a weather metric for a place and time of year, averaged over decades.", "body": "A climate normal is the long-term average of a weather variable (say, the daily high) " "for a specific place and time of year. Thermograph builds each day's normal from every " - "historical day within ±7 days of that day-of-year, across ~45 years — so a day is judged " + "historical day within ±7 days of that day-of-year, across ~45 years, so a day is judged " "against its own season, not a single annual average.", }, "percentile": { "term": "Percentile", - "short": "Where a value ranks within a distribution — the 90th percentile is warmer than 90% of days.", + "short": "Where a value ranks within a distribution: the 90th percentile is warmer than 90% of days.", "body": "A percentile says where a value falls within a range of past values. If today's high is at " "the 97th percentile, only about 3% of comparable days in this location's history were warmer. " "Thermograph grades every day by its percentile against the local ±7-day seasonal distribution.", }, "temperature-anomaly": { "term": "Temperature anomaly", - "short": "How far a temperature departs from normal — the difference from the long-term average.", + "short": "How far a temperature departs from normal: the difference from the long-term average.", "body": "A temperature anomaly is how much warmer or colder it is than the local normal for the " "time of year. Thermograph expresses the same idea as a percentile and a grade (from “Below " "Normal” to “Near Record”), so an anomaly is easy to read at a glance for any location.", @@ -852,15 +852,15 @@ GLOSSARY: dict[str, dict] = { "humidity": { "term": "Absolute humidity", "short": "The actual mass of water vapor in the air, in grams per cubic meter.", - "body": "Thermograph reports absolute humidity (g/m³) — the real amount of water vapor in the " - "air — rather than relative humidity, which shifts with temperature. Graded against local history, " + "body": "Thermograph reports absolute humidity (g/m³), the real amount of water vapor in the " + "air, rather than relative humidity, which shifts with temperature. Graded against local history, " "it shows genuinely muggy or unusually dry days.", }, "reanalysis": { "term": "Reanalysis (ERA5)", "short": "A gridded, physically-consistent record of past weather for anywhere on Earth.", "body": "A reanalysis blends historical observations with a weather model to produce a consistent " - "record of past conditions everywhere — even where no station exists. Thermograph's ~45-year history " + "record of past conditions everywhere, even where no station exists. Thermograph's ~45-year history " "comes from ECMWF's ERA5 reanalysis (via Open-Meteo), which is why it works for any point on Earth.", }, } @@ -875,7 +875,7 @@ def glossary_index(request: Request) -> Response: "terms": [{"slug": s, **e} for s, e in GLOSSARY.items()], "canonical_path": "/glossary", "breadcrumb": [("Home", f"{BASE}/"), ("Glossary", None)], - "page_title": "Weather & climate glossary — heat index, feels-like, percentile, and more", + "page_title": "Weather & climate glossary: heat index, feels-like, percentile, and more", "page_description": ("Plain-language definitions of weather and climate terms: climate normal, percentile, " "temperature anomaly, feels-like, heat index, wind chill, humidity, and reanalysis."), } @@ -890,7 +890,7 @@ def glossary_term(request: Request, term: str) -> Response: "term": entry["term"], "body": _glossary_body(entry), "canonical_path": f"/glossary/{term}", "breadcrumb": [("Home", f"{BASE}/"), ("Glossary", f"{BASE}/glossary"), (entry["term"], None)], - "page_title": f"{entry['term']} — what it means | Thermograph", + "page_title": f"{entry['term']}: what it means | Thermograph", "page_description": entry["short"], "others": [{"slug": s, "term": e["term"]} for s, e in GLOSSARY.items() if s != term], } @@ -901,7 +901,7 @@ def about_page(request: Request) -> Response: ctx = { "canonical_path": "/about", "breadcrumb": [("Home", f"{BASE}/"), ("About", None)], - "page_title": "About Thermograph — how the weather grades are calculated", + "page_title": "About Thermograph: how the weather grades are calculated", "page_description": ("How Thermograph works: ~45 years of ERA5 climate history, a ±7-day seasonal " "window, and empirical percentiles that grade each day relative to its own location."), } @@ -912,7 +912,7 @@ def privacy_page(request: Request) -> Response: ctx = { "canonical_path": "/privacy", "breadcrumb": [("Home", f"{BASE}/"), ("Privacy", None)], - "page_title": "Privacy — Thermograph", + "page_title": "Privacy | Thermograph", "page_description": ("What Thermograph does and does not collect: no tracking, no ads, " "no account required, and location that never leaves your browser."), } @@ -933,7 +933,7 @@ _HOME_JSONLD = { "operatingSystem": "Web, iOS, Android", "isAccessibleForFree": True, "offers": {"@type": "Offer", "price": "0", "priceCurrency": "USD"}, - "description": ("How unusual is your weather? Any day, anywhere on Earth — graded " + "description": ("How unusual is your weather? Any day, anywhere on Earth, graded " "against 45 years of that place's own history."), } diff --git a/digest.py b/digest.py index a889ab9..0bce6ec 100644 --- a/digest.py +++ b/digest.py @@ -71,7 +71,7 @@ async def _store(email: str, place: str | None, source: str | None) -> None: email, "Confirm your Thermograph digest", "Thanks for signing up for the Thermograph monthly digest.\n\n" - "Confirmation links are not active yet — you're on the list, and " + "Confirmation links are not active yet, but you're on the list, and " "you'll get a confirmation as soon as they are.\n", ) diff --git a/discord.py b/discord.py index 7e08ece..3911fd8 100644 --- a/discord.py +++ b/discord.py @@ -56,7 +56,7 @@ def _line(card: dict) -> str: tag = " · ".join(p for p in (pct, grade) if p) when = f" ({card['date_label']})" if card.get("date_label") else "" normal_txt = f", normal {normal}" if normal else "" - return f"**{display}** — {metric} {val}{normal_txt}{when}" + (f" · {tag}" if tag else "") + return f"**{display}**: {metric} {val}{normal_txt}{when}" + (f" · {tag}" if tag else "") def build_embed(feed: dict) -> dict: diff --git a/notify.py b/notify.py index 001a8f2..58e5ae7 100644 --- a/notify.py +++ b/notify.py @@ -104,10 +104,10 @@ def _compose(sub, event_date, metric, direction, graded): val_txt = f" ({value}{unit})" if value is not None else "" if sub.kind == "forecast": body = (f"Forecast for {event_date}: the {noun} is projected at the " - f"{pct:g}th percentile{val_txt} — {grade}.") + f"{pct:g}th percentile{val_txt}, {grade}.") else: body = (f"On {event_date}, the {noun} hit the {pct:g}th " - f"percentile{val_txt} — {grade}.") + f"percentile{val_txt}, {grade}.") return title, body diff --git a/scoring.py b/scoring.py index c66decd..ef486ec 100644 --- a/scoring.py +++ b/scoring.py @@ -186,7 +186,7 @@ def _metric_entry(metric: str, base: np.ndarray, rec: np.ndarray) -> dict: "key": metric, "label": METRIC_LABELS[metric], "score": score, "mad": div["mad"], "bias": div["bias"], "tier": tier, "class": css, "direction": direction, - "grade": tier if score < 15 else f"{tier} — {direction}", + "grade": tier if score < 15 else f"{tier}, {direction}", "weight": WEIGHTS[metric], "per_q": div["per_q"], "n_recent": int(rec.size), "n_base": int(base.size), } @@ -265,7 +265,7 @@ def _annual_entry(metric: str, seasonal: list, history: pl.DataFrame, "key": metric, "label": METRIC_LABELS[metric], "score": score, "mad": round(mad, 1), "bias": round(bias, 1), "tier": tier, "class": css, "direction": direction, - "grade": tier if score < 15 else f"{tier} — {direction}", + "grade": tier if score < 15 else f"{tier}, {direction}", "weight": WEIGHTS[metric], "per_q": per_q, "n_recent": sum(e["n_recent"] for e in present), "n_base": sum(e["n_base"] for e in present), @@ -286,7 +286,7 @@ def build_scores(history: pl.DataFrame) -> dict: years = history["date"].dt.year() span = int(years.max() - years.min() + 1) if span < MIN_BASELINE_YEARS: - return {"unavailable": f"Only {span} years of record here — climate drift " + return {"unavailable": f"Only {span} years of record here; climate drift " f"needs at least {MIN_BASELINE_YEARS}."} recent, baseline = recent_baseline_split(history) latest = history["date"].max() diff --git a/templates/about.html.j2 b/templates/about.html.j2 index 5d20af3..1c38215 100644 --- a/templates/about.html.j2 +++ b/templates/about.html.j2 @@ -13,14 +13,14 @@ location's own climate history.

Where the data comes from

-

The ~45-year daily record — high, low, feels-like, humidity, wind, gust and precipitation — +

The ~45-year daily record (high, low, feels-like, humidity, wind, gust and precipitation) comes from the ERA5 reanalysis (ECMWF), served via Open-Meteo. Because reanalysis is a gridded, model-consistent record, Thermograph works even where there's - no weather station — anywhere on the ~4 sq mi grid.

+ no weather station, anywhere on the ~4 sq mi grid.

How a day is graded

For each metric and date, Thermograph builds a reference distribution from every historical day - within ±7 days of that day-of-year — a 15-day seasonal window across all years. The + within ±7 days of that day-of-year, a 15-day seasonal window across all years. The observed value is placed on that distribution as an empirical percentile, then mapped to a grade from “Below Normal” through “High” to “Near Record”.

Everything is relative to the location: a {{ temp(60) }} day can be “Above Normal” in one place and diff --git a/templates/city.html.j2 b/templates/city.html.j2 index 5eee6ed..969ae93 100644 --- a/templates/city.html.j2 +++ b/templates/city.html.j2 @@ -18,13 +18,13 @@ {% if flavor %}

{{ flavor.extract }}{% if flavor.url %} - — via Wikipedia{% endif %}

+ via Wikipedia{% endif %}

{% endif %} {% if today %}

How today compares

-

Latest recorded day: {{ today.date }} — each reading placed on {{ display }}'s +

Latest recorded day: {{ today.date }}. Each reading placed on {{ display }}'s own ±7-day seasonal distribution.

{% for c in today.cards %} @@ -82,14 +82,14 @@ {% endfor %}

Each bar spans the 10th-percentile daily low to the 90th-percentile daily - high — the range most days fall within — coloured cold → hot on a shared + high (the range most days fall within), coloured cold → hot on a shared {{ temp(-10) }} to {{ temp(115) }} scale, so the whole year's rhythm reads at a glance.

Thinking of visiting {{ name }}?

Trips live and die by the weather. See how {{ name }}'s comfort stacks up against where - you live — {{ name }} is pre-loaded, just add your city.

+ you live. {{ name }} is pre-loaded, just add your city.

Compare {{ name }}'s comfort with your city →

@@ -105,7 +105,7 @@ {% endif %}

Percentiles compare each day to {{ display }}'s own history, so grades are - relative to this location — a “Near Record” day here isn't the same temperature as elsewhere. + relative to this location: a “Near Record” day here isn't the same temperature as elsewhere. How the grades work →

{% endblock %} diff --git a/templates/glossary_term.html.j2 b/templates/glossary_term.html.j2 index 8febad1..e140efe 100644 --- a/templates/glossary_term.html.j2 +++ b/templates/glossary_term.html.j2 @@ -10,7 +10,7 @@

{{ term }}

{{ body | safe }}

-

See it live — grade any location's weather →

+

See it live: grade any location's weather →

More terms