From 28b10a0783511025a2f265110ae63f941c7d23db Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Sun, 19 Jul 2026 18:48:33 -0700 Subject: [PATCH] Remove em-dashes from site copy and tighten the prose (#206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace em-dashes in user-facing copy across the server-rendered pages, static frontend views, and the strings the app injects at runtime, using colons, commas, parentheses or full stops as the context wants. Data placeholder glyphs (a lone "—" for a missing reading) are left alone, since a hyphen there reads as a minus sign in temperature columns. Also tighten the high-visibility surfaces (home hero and meta, about, privacy, city and records ledes, glossary blurbs) toward a plainer, more direct voice while keeping every factual claim intact. Claude-Session: https://claude.ai/code/session_01XXxmNFy9cZ6Gh8Y9thZn62 --- city_events.py | 18 +++++++++--------- climate.py | 4 ++-- content.py | 24 ++++++++++++------------ digest.py | 2 +- discord.py | 2 +- notify.py | 4 ++-- scoring.py | 6 +++--- templates/about.html.j2 | 6 +++--- templates/city.html.j2 | 10 +++++----- templates/glossary_term.html.j2 | 2 +- templates/home.html.j2 | 8 ++++---- templates/hub.html.j2 | 4 ++-- templates/privacy.html.j2 | 6 +++--- templates/records.html.j2 | 8 ++++---- 14 files changed, 52 insertions(+), 52 deletions(-) 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