# Remix prompt: "Is [your city] hotter?"

**Fastest way to remix:** fork this
[shared Claude conversation](https://claude.ai/share/5fd3b3f2-eeb4-4d22-aa53-6921f1930797)
— the prompt below is already loaded. Just tell Claude your city.

Otherwise, paste the entire text below into a new Claude.ai conversation.
Claude will ask you a few clarifying questions about your city, then generate
a full reproducible analysis — Python scripts that pull 40 years of ERA5
reanalysis data from the (free, no-auth) Open-Meteo archive, and a single-file
editorial HTML page with the findings, charts, and FAQ. Everything runs
locally. No hosting keys, no API keys, no npm.

Original: https://prasannais.com/is-bangalore-hotter

---

# You are remixing "Is Bangalore hotter?" for a new city.

## What this is

A reproducible, editorial-quality web page that answers "is [city] *actually*
hotter?" using 40+ years of ECMWF ERA5 hourly reanalysis data from the
Open-Meteo archive. The original (prasannais.com/is-bangalore-hotter) found
that Bangalore's peak temperature barely moved since the 1980s, but the
sleep-cool window (hours below 22°C at night) collapsed from ~3.5 hrs/night to
under one, and April heat stress now runs until 6 PM instead of 4:30 PM.
The *experience* of the city changed; the max-temp chart hid it.

Your job is to run the same analysis for a new city, let the data tell its own
story, and produce a single-page site of the same editorial quality.

## Read the original before you start

Fetch https://prasannais.com/is-bangalore-hotter with WebFetch. Study:

- The section order (discrepancy → anatomy stacked bar → hourly curve →
  duration comparison → year-by-year trend → when-it-happens timeline →
  on-the-ground context → a concrete solution → FAQ → verdict → footer).
- The typography (Fraunces display, Instrument Serif italic accents,
  JetBrains Mono for labels and numbers).
- The editorial voice (honest, specific, no hype, drop-cap on the verdict).
- The visual palette (warm paper `#f4ede0`, heat ramp from cool blue
  `#2a5d6e` through amber `#f5b041` to deep red `#8b1a1a`).
- The six comfort bands (sleep-cool <22°C, comfort 22-26, warm 26-28,
  hot 28-30, very hot 30-32, stress ≥32).

Also fetch the public scripts and data so you can see exactly how the
computation works:

- https://prasannais.com/bangalore-is-hot/fetch_bangalore_april.py
- https://prasannais.com/bangalore-is-hot/fetch_yearly_april.py
- https://prasannais.com/bangalore-is-hot/compute_comfort.py
- https://prasannais.com/bangalore-is-hot/bangalore_comfort.json
  (the shape of the compute output)
- https://prasannais.com/bangalore-is-hot/bangalore_yearly.json
  (the shape of the year-by-year output)

## Step 0 — Ask the user

Before writing a line of code, ask the user these questions in a single
message, then *wait* for answers:

1. **City name** (proper noun — will appear in titles and copy)
2. **Latitude** (decimal degrees, north positive, 4 decimals)
3. **Longitude** (decimal degrees, east positive, 4 decimals)
4. **Timezone** (IANA — e.g. `Asia/Kolkata`, `America/New_York`,
   `Europe/Madrid`, `Africa/Lagos`)
5. **Which month to analyze** (default: the city's hottest month — for
   tropical cities that's often April-May, for temperate cities it's
   July-August)
6. **Climate band** (tropical / subtropical / temperate / arid) — this
   tells you whether the 22°C sleep-cool threshold is right. For a
   temperate city you may want to lower it to 18-20°C. Discuss with
   the user.
7. **Three comparison windows** (defaults: first 5 years of ERA5
   coverage ~ 1985-1989, most recent complete 5 years, current
   partial year)
8. **Local context** the user wants to land (built-up area growth,
   canopy loss, water-body loss, specific incidents). If they don't
   know, note that section will be a placeholder.
9. **Author names** (for the byline) — include "Claude (Anthropic)" as
   co-author by default if they agree.

Confirm their answers back and flag anything that looks off (e.g. lat/lon
not matching the named city, a month choice that isn't the hot season)
before proceeding.

## Step 1 — Write `fetch_windows.py`

Fetches hourly `temperature_2m` data from Open-Meteo's free public archive
for each of the three comparison windows. Python stdlib only —
`urllib.request`, `urllib.parse`, `json`, `time`, `datetime`, `pathlib`,
`statistics`. No external dependencies.

- Endpoint: `https://archive-api.open-meteo.com/v1/archive`
- Required params: `latitude`, `longitude`, `start_date`, `end_date`,
  `hourly=temperature_2m`, `timezone`
- No auth header needed. Send a `User-Agent` like
  `is-this-city-hotter/1.0`.
- Fetch each (window × month) request separately, 1s sleep between
  requests, retry up to 3 times on timeout/HTTPError with 30s backoff.
- Write `raw.json` with schema:
  ```
  {
    "location": {"city": "...", "lat": ..., "lon": ..., "timezone": "..."},
    "month": "april",
    "windows": {
      "w1985_89": {"label": "1985-89", "years_succeeded": [...],
                   "raw": [{"year": 1985, "hourly": {"time": [...], "temperature_2m": [...]}}, ...]},
      "w2021_25": {...},
      "wcurrent": {...}
    }
  }
  ```

## Step 2 — Write `fetch_yearly.py`

One request per year, full month of data, every year from the start of
ERA5 coverage (1985 is safe) through the current year. Same API. Writes
`yearly.json` with one object per year:

```
{
  "year": 2025,
  "n_days": 30,
  "avg_daily_min": 22.4, "avg_daily_max": 34.1,
  "min_daily_min": 18.2, "max_daily_max": 37.5,
  "night_below_22_hrs_per_night": 1.8,
  "night_ge_22_hrs_per_night": 7.2,
  "night_ge_24_hrs_per_night": 3.1,
  "day_ge_30_hrs_per_day": 8.4,
  "day_ge_32_hrs_per_day": 4.7
}
```

If the user picked a non-22°C sleep-cool threshold, adjust field names
and values accordingly.

## Step 3 — Write `compute.py`

Reads `raw.json`, produces `comfort.json` with, per window:

### Comfort bands
Hours-per-average-day in each of the six bands. Day window is 07:00-21:59
local, night window is 22:00-06:59 local.

### Exceedance curves
For each 0.5°C step from 24 to 36, the average hrs/day above that
threshold during the day window. Same for 20-28°C during the night
window.

### Threshold crossings
Using the 24-hour hourly mean curve (averaged across all days in the
window), linearly interpolate when the curve crosses:
- 32°C going up (heat stress starts)
- 32°C going down (heat stress ends)
- 22°C going down at night (sleep-cool window starts)
- 22°C going up in morning (sleep-cool window ends)

Print a human-readable summary table to stdout. This is your
sanity-check — read it before writing any prose.

## Step 4 — Find the story

Now look at the numbers. Don't jump to "same conclusion as Bangalore."
The city you're analyzing may have a different signal entirely.
Possibilities:

- **Sleep-cool collapse** (the Bangalore story) — night warming
  dominates, peak unchanged.
- **Peak escalation** — daily max genuinely rising, mornings
  unchanged.
- **Season extension** — old hot-month now as hot as new hot-month,
  but the hot month itself hasn't changed much.
- **No change** — some cities are genuinely stable. Report that
  honestly.
- **Regime shift** — a specific year marks a discontinuity;
  before-and-after are qualitatively different.

Which of these is the real story here? Write it in one sentence.
That sentence is your section heading and your social-share text.
If it's none of the above, say what it actually is.

**Rule: don't reuse Bangalore's headlines.** If the data says something
different, the page says something different.

## Step 5 — Build `index.html`

One HTML file. Chart.js 4.4.6 from jsDelivr. No other runtime deps. Bake
the numeric arrays from the JSON directly into inline `<script>` tags so
the page renders with zero network requests after the fonts load.

### Sections, in order

1. **Site header slot** — a plain `<header class="site-chrome-header">`
   with a placeholder nav. The user will wire this up to their own
   site later.
2. **Masthead** — `[City] · [lat]°N, [lon]°E · A Weather Report · [month] [year]`
3. **Hero** — headline + lede + byline. The byline includes
   `By · [human author] & Claude (Anthropic)`, reading time, place,
   and the question.
4. **The discrepancy** — two-card contrast: "The number you see"
   (the all-time peak max, with year) vs "The number you feel"
   (the warmest overnight low in the ERA5 record, with its date).
   This is the hook — show why the conventional chart hides what
   people experience. **Do not cite a "record-high minimum" from
   an aggregator like CEIC unless you can independently verify
   it against a primary IMD/station source or against your own
   ERA5 pull** — the Bangalore original made that mistake and had
   to retract it.
5. **The finding** (the lead chart) — stacked anatomy bars. Three
   rows (one per window), each a 100%-wide bar split by comfort band.
   Label the section with the finding, not with a generic "Chart".
   Caption explains which band shrank and which grew.
6. **The same day, hour by hour** — three-series line chart of the
   24-hour mean curve per window. Plot with zone-band background
   plugin (three pastel bands at <22 / 22-26 / >26) and dashed
   threshold lines at 22°C (sleep-cool) and 26°C (warm threshold).
7. **Duration, not average** — paired comparison bars for
   hrs/day above 28°C, 30°C, 32°C (day) and hrs/night above 22°C,
   24°C, 26°C (night). Two stacked sub-bars per row (then vs now),
   header above each with the numeric value.
8. **Is [year] just a bad year?** — two Chart.js line charts
   (sleep-cool hrs/night, heat-stress hrs/day) with 5-year rolling
   overlay. Highlight worst years with annotations. Three
   stat callouts underneath: how often a sleep-cool-poor year
   happened before 2010 vs after, the single worst year and why,
   and the trend direction (rolling mean slope).
9. **When it happens** — SVG timeline. Horizontal day-clock from
   00:00 to 24:00. For each era, draw: the sleep-cool window as
   a blue bar, the heat-stress window as a red bar. Caption shows
   the crossing times explicitly.
10. **What changed on the ground** — local UHI context. If the
    user supplied numbers (built-up area %, canopy loss %, lake
    loss %), use them. If not, mark `[needs local research]`
    and skip. Never fabricate this.
11. **What the city could do** — one concrete, cheap,
    already-proven intervention. For Bangalore it was cool roofs.
    For your city, ask the user or pick one based on climate and
    geography (shade trees, urban lakes, cool pavements).
12. **FAQ** — eight deep-linkable `<details>` items, each with
    `id="faq-..."`:
    - `faq-aberration` — is [year] a freak or a trend?
    - `faq-era5` — why ERA5 vs local weather station data
    - `faq-sleep-cool` — what "sleep-cool" means (adjust
      threshold + reasoning if user picked a different one)
    - `faq-never-below-22` — literal interpretation of the
      "night never cools" claim (adjust to actual finding)
    - `faq-ac` — does air conditioning make it worse
    - `faq-code-data` — links to the hosted scripts and JSON
      (once the user hosts them)
    - `faq-claude` — what "co-authored with Claude" means
    - `faq-other-cities` — will you do this for other cities
13. **Verdict** — short, direct, with drop-cap italic first
    letter. Closes with a single-sentence italicized kicker.
14. **Footer** — sources (the actual sources you cited, with real
    URLs), colophon (fonts, credits, date), copyright.
15. **Site footer slot** — plain chrome footer, placeholder links.

### Deep-linkable FAQ

Add a small inline script at the end of `<body>`:

```js
(function () {
  function openTargeted() {
    var h = location.hash.replace(/^#/, '');
    if (!h) return;
    var el = document.getElementById(h);
    if (el && el.tagName === 'DETAILS') {
      el.open = true;
      setTimeout(() => el.scrollIntoView({block: 'start', behavior: 'instant'}), 0);
    }
  }
  addEventListener('DOMContentLoaded', openTargeted);
  addEventListener('hashchange', openTargeted);
})();
```

### Chart.js essentials

- Use `Chart.register` for custom plugins:
  - `zoneBandsPlugin` — pastel `<22/22-26/>26` background rows on
    the hourly curve
  - `thresholdLinesPlugin` — dashed horizontal lines at 22°C and
    26°C
- For the anatomy stacked bar, don't use Chart.js — hand-roll a
  flexbox stacked `<div>` with six children, widths as
  percentages. Easier and crisper than Chart.js stacked bars for
  this use.

### Typography

```html
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,600;0,9..144,900;1,9..144,400&family=JetBrains+Mono:wght@400;500;700&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
```

- Display / body: Fraunces (variable opsz)
- Italic accents, kickers: Instrument Serif
- Labels, numbers, axis ticks: JetBrains Mono

## Step 6 — Run it locally

Print this as a code block at the end for the user:

```
python3 fetch_windows.py
python3 fetch_yearly.py
python3 compute.py
python3 -m http.server 8765
# then open http://localhost:8765/
```

No npm, no build step, no API keys. Only Python 3 stdlib + a web browser.

## Editorial principles (the important part)

**Honesty over narrative.** Three rules:

1. **Don't invent numbers.** Every stat in the prose traces to a specific
   value in one of the JSON files. If you write "nights never cool below
   22°C," the minimum of the 24-hour mean curve must actually be ≥22°C
   in the data. Check twice. Re-check before publishing.

2. **The surprise is the product.** When you first see the computed
   numbers, notice what shocks you. That reaction — before you've
   built a narrative around it — is the lede. In Bangalore the
   expected story was "peak is rising"; the data said "nights stopped
   cooling." The second story was the real story. Tell the real one.

3. **Don't bury the lede.** Section headings are findings, not labels.
   Not "Hourly temperature analysis" → "The sleep-cool window
   collapses." Write the heading only after you've seen the numbers.

## Claude co-authoring protocol

Add Claude (Anthropic) as a co-author in the byline and colophon by
default. Include the `faq-claude` FAQ item explaining what that means
concretely: Claude writes most of the Python, pulls the data, iterates on
chart design, drafts and rewrites prose, cross-checks numbers. The human
sets the question, pushes back on framing, makes the editorial calls,
and verifies the numbers. Every number on the page has been checked
against the source JSON by a human who reads ERA5 output for fun.

## Delivery checklist

Before handing the page back:

- [ ] Every number in the HTML appears in one of the JSON outputs.
- [ ] Anatomy bar percentages sum to 100 ± 0.1 in each row.
- [ ] Threshold crossings print sensible times (not "25:00" or negative).
- [ ] All FAQ `<details>` have unique IDs and the hash script is present.
- [ ] No leftover Bangalore-specific numbers or place names.
- [ ] The page renders with zero console errors (offer to start a local
      server and verify with curl / a browser snapshot).
- [ ] Tone is editorial, not technical. The page reads like a magazine
      feature, not a dashboard.
- [ ] The headline is the actual finding, not a generic tease.

## What NOT to do

- Don't fetch from the internet at page render time. Bake numbers in.
- Don't add analytics, cookies, pixels, or any third-party script beyond
  Chart.js + Google Fonts.
- Don't invent UHI, canopy, or water-body numbers. Leave a
  `[needs local research]` placeholder if the user hasn't provided them.
- Don't assume the climate. A temperate city's "sleep-cool" may be
  18°C, not 22°C. Ask before computing.
- Don't rewrite the site chrome (header/footer) to match the user's
  brand. Leave placeholder slots; they'll style it themselves.
- Don't commit to hosting paths the user hasn't set up. The FAQ links
  to hosted scripts should be marked `[user will fill in after upload]`.

## Closing

When the analysis is complete, send the user:

1. **The three most surprising findings** with exact numbers and the
   window they refer to.
2. **The recommended headline** — one sentence, specific, honest.
3. **What needs human follow-up** — UHI numbers, framing questions,
   any data gaps.
4. **A short draft of the social post** they can use to share it.

Now start with Step 0 — ask the user the city questions.
