Skip to content

feat(legend): derive discrete swatch legends from a match recolor (#334) - #339

Merged
cboettig merged 1 commit into
mainfrom
feat/derive-categorical-legend
Aug 4, 2026
Merged

feat(legend): derive discrete swatch legends from a match recolor (#334)#339
cboettig merged 1 commit into
mainfrom
feat/derive-categorical-legend

Conversation

@cboettig

@cboettig cboettig commented Aug 4, 2026

Copy link
Copy Markdown
Member

First half of #334. #333 made legends follow the paint on the map, but only for ramps — a match recolor derived nothing, so the section was dropped and the layer rendered with no legend.

Why

From the proxy logs, ca-30x30 session ce97e5b5 (2026-08-03, z-ai/glm-5.2). The user asked for unprotected biodiverse areas colored by taxon. The agent mapped five taxa onto codes 1–5 in its register_hex_tiles SQL and recolored correctly, unprompted:

set_style {"layer_id": "hex-1a669f5217773ef1",
           "fill-color": ["match", ["get","dominant_taxon"],
                          1,"#1f77b4", 2,"#ff7f0e", 3,"#2ca02c", 4,"#9467bd", 5,"#8c564b", "#cccccc"]}

The map was right. The legend had nowhere to put the key, so the agent posted it as a markdown table in chat — and when the user complained, it read the complaint as a paint problem and restyled an already-correct map, closing with "The map legend should now match this key", which was false.

(On the deployed pin, v3.23.0, the symptom was worse: the stale viridis colorbar from before #333. That part is already fixed on main.)

What changed

deriveCategoricalLegend(paint) in app/legend-helpers.js, beside the continuous parser, plus one resolver (_categoricalLegend) that every categorical legend now routes through, so paint wins over config once a restyle has replaced the colors:

  • A match recolor renders swatches on any vector layer — including one configured continuous, and including dynamic hex layers, whose per-resolution colorbar stops describing a fixed discrete ramp.
  • A config-categorical layer recolored past its legend_classes drops them rather than captioning the new colors with the old names. This is the same staleness set_style leaves the legend stale (or absent): legend never refreshes after a runtime restyle #333 fixed for colorbars; the categorical render path read state.legendClasses unconditionally and was missed.
  • Only match qualifies. step stays continuous (a binned numeric ramp), and case carries no value to label a swatch with. The case-wrapped per-resolution expression hex layers register with is rejected on both counts, so an unrestyled hex layer keeps its zoom-reactive colorbar and reset_style restores it.
  • A partial match — one arm carrying a nested expression instead of a color — is rejected whole, rather than rendering a swatch list that silently omits colors on screen.

legendType is only rewritten where #333 already did it (auto-promotion of a layer with no declared type). Hex and config-declared types are untouched; their legends follow the paint through the resolver instead, so reset_style needs no new bookkeeping.

Labels

Swatches are labelled with the matched value (1, 2, …). The tiles carry only the code — what it means lives in the SQL that produced it — so this is as far as derivation can honestly go. Naming them is #334's remaining half (set_legend), which follows in a stacked PR along with legend state in get_map_state so the agent can stop misdiagnosing this.

Tests

npm test → 614 passing. 13 new cases across test/legend-helpers.test.js (parser, including the real expression above and the hex case wrapper) and test/map-manager.legend-restyle.test.js (hex colorbar → swatches → restored by reset, stale config classes dropped, multi-value arms, #328 heading suppression kept config-only).

Two existing expectations changed intentionally, both asserting the old "no legend" outcome for a match:

  • demotes again when a later restyle is no longer describable — now uses a flat color for the undescribable case.
  • drops the legend when recolored categoricallyreplaces the colorbar with swatches when recolored categorically.

Browser-bound rendering is verified by the jsdom legend suites; worth a look on a deployed app before release.

…334)

#333 made legends follow the paint on the map, but only for ramps. An agent
that codes categories as integers in SQL and recolors with `match` produced a
layer with no legend at all — the derivation returned null and the section was
dropped.

Seen in the wild on ca-30x30 (session ce97e5b5, 2026-08-03): the agent mapped
five taxa onto codes 1-5, recolored correctly with `match`, and then posted its
color key as a markdown table in chat because the map had nowhere to put it.
It went on to misdiagnose the user's complaint as a paint problem and restyle
an already-correct map.

Adds `deriveCategoricalLegend(paint)` beside the continuous parser, and routes
every categorical legend through one resolver so paint wins over config once a
restyle has replaced the colors:

- A `match` recolor renders swatches on any vector layer, including one
  configured `continuous` and including dynamic hex layers, whose per-resolution
  colorbar no longer describes a fixed discrete ramp.
- A config-`categorical` layer recolored past its `legend_classes` now drops
  them instead of captioning the new colors with the old names — the same
  staleness #333 fixed for colorbars.
- Only `match` qualifies. `step` stays continuous (it is a binned numeric ramp)
  and `case` carries no value to label a swatch with. The `case`-wrapped
  per-resolution expression hex layers register with is rejected on both counts,
  so an unrestyled hex layer keeps its zoom-reactive colorbar.

Swatches are labelled with the matched value, since the tiles carry only the
code. Naming them is #334's remaining half (`set_legend`).
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 61.15% 6196 / 10131
🔵 Statements 61.13% 6196 / 10135
🔵 Functions 67.47% 222 / 329
🔵 Branches 84.6% 1423 / 1682
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
app/legend-helpers.js 98.57% 98.18% 100% 98.57% 82-83
app/map-manager.js 65.29% 85.41% 60.93% 65.35% 57-149, 186-190, 196-423, 432-476, 490-512, 553-589, 599-612, 620-633, 743-745, 822-823, 849-850, 1010-1019, 1047-1048, 1054-1057, 1066-1076, 1082-1089, 1181-1185, 1199-1202, 1218-1234, 1240-1241, 1247-1250, 1319-1320, 1340-1342, 1364-1369, 1375-1378, 1388-1406, 1413-1417, 1425-1518, 1525-1565, 1589-1591, 1603-1617, 1667-1672, 1688-1689, 1701-1707, 1918-1919, 2003-2022, 2025-2040, 2275-2276, 2309-2313, 459-493
Generated in workflow #190 for commit 18c5daa by the Vitest Coverage Report Action

@cboettig
cboettig merged commit 8aa5845 into main Aug 4, 2026
1 check passed
@cboettig
cboettig deleted the feat/derive-categorical-legend branch August 4, 2026 05:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant