Files
Climate-Mood-Analysis/docs/koppen-mixed-display-plan.md
T
KnouandClaude Opus 5 e855d583e3 Document restructuring and the beginnings of Filter 2 changes
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>
2026-09-15 16:13:11 -04:00

11 KiB
Raw Blame History

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 §1; the filter's review is reviews/01-koppen.md, and overall progress is tracked in 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.