Split the pipeline documentation by purpose so each fact has one home: - docs/pipeline-plan.md keeps the plan, checklist, tracker, and guardrails - docs/decisions.md holds open decisions and the dated decision log - docs/reviews/ holds findings and tasks: one file per filter, plus 00-cross-filter.md for findings that span filters - scripts/common/README.md holds the shared-helper rules (formerly Phase 2) - filter-calculations.md now describes calculations only Filed findings 12-22 from a consistency audit of the app, docs, and scripts. Filter 1 (Köppen-Geiger): use "Köppen" with the umlaut in all prose, labels, docstrings, help text, and checker messages (finding 21), and correct the base build's "majority" docstring (finding 22). Filter 2 (annual avg temperature): record the adopted definition in filter-calculations.md §2: equally weighted 1991-2020 monthly normals, per WMO-No. 1203 and NOAA's 2020 methodology; area-weighted county means; blank unless all 12 months exist. Code changes for this filter are still pending. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
200 lines
11 KiB
Markdown
200 lines
11 KiB
Markdown
# 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; the filter's review is
|
||
[reviews/01-koppen.md](reviews/01-koppen.md), and 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.
|