Files
Climate-Mood-Analysis/docs/koppen-mixed-display-plan.md
T
KnouandClaude Opus 5 4d2b3e3d44 Complete Köppen-Geiger filter review with Mixed climate class
Classify each county by area-weighted Köppen class shares: a county is
predominantly its top class when that class covers at least 50% of its
land and leads the runner-up by at least 5 percentage points; otherwise
it is Mixed (133 of 3,143 counties in the 50 states and DC).

- Add build_county_koppen_metric.py (writes data/metrics/koppen.csv) and
  apply_koppen_metric_to_climate_data.py (writes koppenZone plus
  koppenPrimaryClass/koppenSecondaryClass for Mixed counties).
- Move shared helpers into scripts/common/ (county loading, Köppen
  legend, area-weighted raster shares); fix the 180th-meridian raster
  window for Aleutians West.
- Add check_climate_data.py to validate the app CSV.
- Draw Mixed counties in app.js as diagonal stripes of their top two
  classes, fixed to the ground and following the map at every zoom, with
  a crossfade only when the stripe size changes. Filtering a class also
  matches Mixed counties where it is primary or secondary.
- Document the rule, display, and pipeline plan in docs/ and update the
  README and data-source notes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 02:54:25 -04:00

199 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Mixed Climate Display
**Status (2026-09-14):** Complete. The app draws Mixed counties as stripes, the
pipeline writes the two stripe-class columns, and `data/climate-data.csv` has
been updated (142 counties Mixed, 133 of them in the 50 states and DC). The
display was reviewed in the browser and refined (see the history in section 7),
and the project documentation was updated (section 6).
**Scope:** Show Köppen-Geiger "Mixed" counties as striped on the map, filter them
by their top two classes, and carry the two stripe classes from the pipeline into
the app. The classification rule itself is described in
[filter-calculations.md](filter-calculations.md) §1; overall progress is tracked in
[pipeline-plan.md](pipeline-plan.md).
## 1. Design
- **Only Mixed counties are striped.** A county is Mixed when its top class covers
less than 50% of its land or leads the runner-up by less than 5 percentage
points. Every other county keeps its solid class color.
- **Stripe colors** are the county's top class (primary) and runner-up class
(secondary), using the existing Köppen class colors.
- **Stripes are diagonal at 45°**, from upper left to lower right, and run
unbroken to the county boundary. Neighboring Mixed counties share the same
angle and spacing, so stripes line up across borders. The county border is
drawn on top.
- **Stripes follow the map.** The density is exact at the default view
(`DEFAULT_COUNTRY_VIEW`, the project's single reference scale). From there the
stripe pair width doubles with each zoom-in step, all the way to the map's
deepest zoom (19), so the number of stripes in a county stays the same at
every zoom.
- **Far-out style.** When stripes would drop below 5 pixels per pair (zoom 4 and
below), the map-following width is doubled until it reaches 5 pixels (at
least once, so there are half as many lines), giving 8.53-pixel pairs split
70/30 so the secondary stays visible.
- **Stripes are fixed to the ground.** Each pattern is anchored to the map
projection's fixed origin, so a stripe stays on the same ground at every zoom
from 5 to 19. At the far-out zooms every other stripe stays put while the
crossfade blends the rest.
- **Fades happen only when the stripe size changes** (the switch between the
standard and far-out styles, and zoom steps within the far-out range). The fade
is a 250 ms true crossfade that runs just after Leaflet's zoom animation: the
old stripes, exactly as the animation left them, blend into the new ones with
no jump and no gap.
- **Filtering.** Choosing a class shows counties that are predominantly that
class plus Mixed counties where it is the primary or secondary class. Third and
lower classes do not match. One **"Mixed Climate"** option shows all Mixed
counties; there are no per-pair Mixed options.
- **Every class on the map is listed.** Classes that appear only as stripe
colors (today Dsc and Dwc, both in Alaska) are listed in the dropdown and
legend as regular classes.
- **Legend.** The "Mixed Climate" row has a swatch of two neutral grays in the
standard split (lighter gray primary, darker gray secondary), generated from
the split setting so it always matches the map, and a note that stripes show
each county's top two classes.
- **Deferred:** a donut chart of every class share (like Europa Universalis 5).
### Settings
All stripe settings sit together near the top of `app.js`:
| Setting | Value | Controls |
| --- | --- | --- |
| `KOPPEN_MIXED_PRIMARY_STRIPE_FRACTION` | 0.8 | Primary share of each stripe pair, standard style (zoom 5 and closer) |
| `KOPPEN_MIXED_LINE_PAIRS_PER_100_MILES` | 5 | Stripe pairs per 100 miles, measured across the stripes at the default view |
| `KOPPEN_MIXED_MIN_PAIR_WIDTH_PX` | 5 | Below this pair width, stripes switch to the far-out style |
| `KOPPEN_MIXED_FAR_PRIMARY_STRIPE_FRACTION` | 0.7 | Primary share of each stripe pair, far-out style |
| `KOPPEN_MIXED_STYLE_FADE_MS` | 250 | Length of the crossfade |
| `KOPPEN_MIXED_STRIPE_ROTATION_DEGREES` | -45 | Stripe angle (SVG rotation; -45 runs upper left to lower right) |
| `KOPPEN_MIXED_LEGEND_PRIMARY_GRAY`, `KOPPEN_MIXED_LEGEND_SECONDARY_GRAY` | `#d4d8df`, `#6b7383` | Legend swatch colors |
## 2. Data
`data/climate-data.csv` has two columns directly after `koppenZone`:
| Column | Filled when | Value |
| --- | --- | --- |
| `koppenPrimaryClass` | `koppenZone` is `Mixed` | Top class, e.g. `Csb` |
| `koppenSecondaryClass` | `koppenZone` is `Mixed` | Runner-up class, e.g. `Dsb` |
Both are blank for predominant counties. The values come from `koppenTopClass`
and `koppenSecondClass` in `data/metrics/koppen.csv`.
## 3. How the stripes are drawn
The map uses Leaflet 1.9.4 with its default SVG renderer. Each ordered
primary/secondary pair gets one SVG `<pattern>`, kept in a small hidden SVG added
to the page; fill references such as `url(#id)` resolve anywhere in the page, so
Leaflet's own SVG elements are not modified. The 133 Mixed counties in the
50 states and DC use 50 pairs. Order matters: Csb/Dsb and Dsb/Csb give the
primary share to different colors.
- **Pattern contents.** A primary-color background and a group of
secondary-color bands. Normally the group has one band per stripe pair; during
a crossfade it holds shared, old-only, and new-only bands.
- **Coordinates.** `patternUnits="userSpaceOnUse"`, so every county is filled in
the map's coordinate space and stripes align across borders.
- **Anchoring.** `patternTransform="rotate(-45) translate(x y)"`. Leaflet's SVG
coordinates start from a pixel origin that moves on every zoom, so the
translate places the pattern's origin at the map projection's fixed origin,
reduced to within one tile to avoid precision loss at deep zooms.
- **Fill.** A Mixed county's style sets
`fillColor: "url(#koppen-mixed-<primary>-<secondary>)"`.
**Pair width at the default view.** Leaflet uses Web Mercator, where
$$
\text{meters per pixel} = \frac{156{,}543.03 \times \cos(\text{latitude})}{2^{\text{zoom}}}.
$$
At the default view (latitude 39.5°, zoom 5) that is about 3,775 m per pixel,
so 100 miles (160,934 m) is about 42.6 pixels. At 5 pairs per 100 miles, one
stripe pair is about 8.5 pixels: about 6.8 pixels of primary color and
1.7 pixels of secondary color. The app computes this from the settings.
| Zoom | Style | Pair width | Split |
| --- | --- | --- | --- |
| 2–4 | Far-out | 8.53 px | 70/30 |
| 5 (default) | Standard | 8.53 px | 80/20 |
| 6 | Standard | 17.05 px | 80/20 |
| 7 | Standard | 34.11 px | 80/20 |
| 8 | Standard | 68.2 px | 80/20 |
| 9–19 | Standard | Doubling each step, to 139,704 px at zoom 19 | 80/20 |
**Zooming.** During Leaflet's zoom animation the whole SVG layer is scaled with
the map, so the stripes stretch with it. When the zoom ends, the stripes are
recomputed. Because every width is the reference width times a power of two, the
old stripes (previous width × 2 to the power of the zoom change) and the new
stripes fit one pattern tile. If the size is unchanged, the new layout is drawn
directly; if it changed, bands secondary in both layouts stay solid, old-only
bands fade out, and new-only bands fade in.
## 4. App code (`app.js`)
| Area | Functions and settings |
| --- | --- |
| Stripe settings | The `KOPPEN_MIXED_*` constants near the top of the file (section 1) |
| Mixed class and parsing | `KOPPEN_CLASS_META` (`Mixed`, labelled "Mixed Climate", sorted last), `normalizeKoppenCode`, `sanitizeOverrideRecord` (reads the two stripe-class columns) |
| Stripe styles | `getKoppenMixedStripePairWidthPx`, `getKoppenStripeStyle`, `getKoppenStripeClasses`, `getKoppenStripePatternId` |
| Patterns | `createKoppenStripePatterns`, `buildKoppenStripePattern`, `drawKoppenStripeBands`, `applyKoppenStripeStyle`, `anchorKoppenStripePatterns` |
| Zooming and crossfade | `updateKoppenStripesForZoom`, `crossfadeKoppenStripes`, `getKoppenSecondaryBands`, `classifyKoppenStripeBands`, `canCrossfadeKoppenStripes` |
| Fill, filter, and labels | `fillForCounty`, `shouldFeaturePassFilter`, `getUniqueCategoryValuesInData`, `getCategoricalDisplayLabel`, `formatCountyMetricValue` |
| Legend | `updateLegend`, `buildKoppenMixedLegendSwatchBackground` |
The hover tooltip shows only the county name, and `styles.css` is unchanged.
After changing `app.js`, bump `APP_ASSET_VERSION` and the `app.js?v=` query in
`index.html` so browsers load the new files.
## 5. Pipeline
- **`scripts/build_county_koppen_metric.py`** writes `data/metrics/koppen.csv`
with each county's class, top and runner-up class, and their shares.
- **`scripts/apply_koppen_metric_to_climate_data.py`** writes `koppenZone`,
`koppenPrimaryClass`, and `koppenSecondaryClass` into `data/climate-data.csv`,
adding the two stripe columns after `koppenZone` if missing and filling them
only for Mixed counties. `--dry-run` reports changes without writing.
- **`scripts/check_climate_data.py`** requires the two stripe columns: filled if
and only if `koppenZone` is `Mixed`, with valid and different Köppen codes.
- **Tests:** `tests/test_koppen_metric.py` and `tests/test_check_climate_data.py`.
## 6. Documentation (stage 5, done 2026-09-14)
- `filter-calculations.md` §1 describes the applied rule, the stripe columns,
and the filter; the old method is kept as a short "Previous method" note.
Review findings 2 and 4 are marked resolved for Köppen.
- `README.md` and `scripts/county_data_sources.md` describe the new
classification and list the Köppen build, apply, and check commands.
## 7. History
Decisions that were reversed or refined during the browser review:
- **2026-09-13 — Stripes follow the map.** Stripes were first a fixed width on
screen; the owner wanted the number of stripes in a county not to change with
zoom.
- **2026-09-13 — True crossfade.** The first fade faded the stripes out and back
in, which briefly left no stripes; it was replaced by a crossfade from the old
stripes to the new ones.
- **2026-09-13 — Crossfade after the zoom animation.** Starting zoom-in fades
together with the animation was tried and reverted: the perceived slowness had
been the map's own zoom animation.
- **2026-09-13/14 — Near style removed.** A thicker 70/30 split from zoom 9 was
added, then made instant (fades happen only when the stripe size changes), set
to 80/20 by the owner, and finally removed.
- **2026-09-14 — Stripes fixed to the ground.** Stripes slid across the land on
each zoom because patterns were anchored to Leaflet's per-zoom pixel origin.
- **2026-09-14 — No deepest-zoom limit.** A 60-pixel cap, later a 1,100-pixel
safety limit with halving, was removed: any cap forces stripes to thin or
subdivide on screen, and tests showed no performance cost without one.
- **2026-09-14 — Zoom response setting removed.** The fixed-to-the-ground
anchoring and the crossfade both require stripes to follow the map exactly.
## 8. Puerto Rico
The app already leaves Puerto Rico off the map: `prepareCountyFeature` in
`app.js` drops counties with state FIPS 72, and the dropdown and legend are built
from the counties on the map. The rows remain in `data/climate-data.csv` and
`data/metrics/koppen.csv`. Whether to also remove them from the data files is a
separate decision.