Refine project documentation and track metric files
Close gaps found in a review of the documentation: - Track data/metrics/ and data/metric_sources.json in git so the data checker passes on a fresh clone (finding 23; decision logged) - State the Alaska and Hawaii coverage gap in the README limitations and extend finding 12 - File findings 24-26: the data-sources doc lacks gridMET and several pipeline commands; wettest/driest month are computed twice; solar GHI is written by the extreme-temperature apply step - Add the stale "fallback values" note to filter 2's tasks Tidy the document system: - Add docs/reviews/README.md with the numbering rules and a finding index - Rename koppen-mixed-display-plan.md to koppen-mixed-display.md and fix its stale Puerto Rico and "stage 5" text - Add the precipitation-month step to the README enrichment list - Describe the Current method / Previous method pattern in plan section 5 - Add CLAUDE.md with the project guardrails and doc layout Format filter-calculations.md so it renders on GitHub and in VS Code: inline math uses $...$, ranges use en dashes, and implementation references name functions instead of line numbers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# 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 (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`, as decided on 2026-09-13; see "Puerto Rico" in
|
||||
[decisions.md](decisions.md).
|
||||
Reference in New Issue
Block a user