feat(legend): derive discrete swatch legends from a match recolor (#334) - #339
Merged
Conversation
…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`).
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First half of #334. #333 made legends follow the paint on the map, but only for ramps — a
matchrecolor 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 itsregister_hex_tilesSQL and recolored correctly, unprompted: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)inapp/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:matchrecolor renders swatches on any vector layer — including one configuredcontinuous, and including dynamic hex layers, whose per-resolution colorbar stops describing a fixed discrete ramp.categoricallayer recolored past itslegend_classesdrops 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 readstate.legendClassesunconditionally and was missed.matchqualifies.stepstays continuous (a binned numeric ramp), andcasecarries no value to label a swatch with. Thecase-wrapped per-resolution expression hex layers register with is rejected on both counts, so an unrestyled hex layer keeps its zoom-reactive colorbar andreset_stylerestores it.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.legendTypeis 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, soreset_styleneeds 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 inget_map_stateso the agent can stop misdiagnosing this.Tests
npm test→ 614 passing. 13 new cases acrosstest/legend-helpers.test.js(parser, including the real expression above and the hexcasewrapper) andtest/map-manager.legend-restyle.test.js(hex colorbar → swatches → restored by reset, stale config classes dropped, multi-value arms,#328heading 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 categorically→replaces 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.