Skip to content

Fix tech map legend not updating with layer toggles#2073

Open
tsubasakong wants to merge 5 commits intokoala73:mainfrom
tsubasakong:lucas/fix-worldmonitor-1967-tech-legend-sync
Open

Fix tech map legend not updating with layer toggles#2073
tsubasakong wants to merge 5 commits intokoala73:mainfrom
tsubasakong:lucas/fix-worldmonitor-1967-tech-legend-sync

Conversation

@tsubasakong
Copy link
Copy Markdown
Contributor

Summary

  • make the tech map legend derive its entries from the currently enabled layers
  • refresh the legend during normal DeckGL map renders so toggle changes show up immediately
  • keep the existing CII legend behavior intact while hiding the main legend when no relevant entries are active

Testing

  • npm run typecheck
  • npm run build:tech
  • npm run build:full

Fixes #1967

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 22, 2026

@tsubasakong is attempting to deploy a commit to the Elie Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions bot added the trust:safe Brin: contributor trust score safe label Mar 22, 2026
@koala73 koala73 added High Value Meaningful contribution to the project Ready to Merge PR is mergeable, passes checks, and adds value labels Mar 26, 2026
Copy link
Copy Markdown
Owner

@koala73 koala73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @tsubasakong — thanks for picking this up! The bug is real and the getLegendItems() extraction is a clean direction. A couple of things need to land before this can merge.


🔴 Blocking

1. updateLegend() inside the RAF hot path — DOM churn at 60fps

render() fires on every pan, zoom, time-range change, and layer toggle. By placing updateLegend() inside its RAF callback, every one of those events now does:

  • querySelector('.deckgl-legend') on every frame
  • Rebuilds all SVG strings via getLegendItems() (including getCurrentTheme() + multiple t() i18next lookups)
  • legend.innerHTML = ... — full subtree destroy + rebuild
  • document.createElement() for the CII legend, sets its innerHTML, appendChild

For finance, happy, and default variants this is entirely unconditional — same items every time, DOM wiped and rebuilt for nothing. On a 60fps pan lasting 300ms that's 18 full DOM rebuilds.

Fix: Move updateLegend() out of render(). Call it in the four layer-mutation paths instead — the CII toggle handler, setLayers(), enableLayer(), toggleLayer(). Each already calls this.render(), so just add this.updateLegend() before those calls.

Alternatively, the minimal guard:

private updateLegend(): void {
  const legend = this.legendEl; // cached ref (see P3 below)
  if (!legend) return;
  const legendItems = this.getLegendItems();
  const key = legendItems.map(i => i.label).join(',') + '|cii:' + this.state.layers.ciiChoropleth;
  if (legend.dataset.legendKey === key) return; // skip — nothing changed
  legend.dataset.legendKey = key;
  // ... rest unchanged
}

2. ciiChoroplethLegend recreation makes the toggle handler dead code

updateLegend() wipes legend.innerHTML then creates a new <div id="ciiChoroplethLegend"> on every call. The existing CII toggle handler (line 4247) does querySelector('#ciiChoroplethLegend') + manually sets style.display — but render() fires immediately after, updateLegend() recreates the element with the correct display from this.state.layers.ciiChoropleth, and the toggle handler's mutation is silently overwritten. It's now dead code.

Fix: Create ciiLegendEl once in createLegend() and store it as private ciiLegendEl. In updateLegend(), after the innerHTML wipe, do legend.appendChild(this.ciiLegendEl) (DOM move, not clone) and toggle style.display on it. Remove the querySelector('#ciiChoroplethLegend') block from the toggle handler.


🟡 Should Fix

3. syncMapAfterLayoutChange — remove the RAF call

requestAnimationFrame(sync);       // fires ~16ms — CSS transition hasn't started
window.setTimeout(sync, delayMs);  // fires at 320ms — after transition ✓

The original code only had setTimeout(320), which was deliberate (CSS transition duration). The RAF fires before the container has its final dimensions. Remove it, keep only the timeout.

4. finance / happy / default variants don't filter by active layers

Only tech filters entries via this.state.layers[layer]. The default and happy variants both include aircraft (maps to flights) and datacenter — both toggleable. Toggling those off on non-tech variants still shows stale legend entries. Same bug class this PR is fixing.

5. Tests check source text, not behavior

map-fullscreen-resize.test.mjs regex-matches event-handlers.ts as a string. It doesn't verify that the legend actually updates when a layer is toggled (the PR's stated fix for #1967). Consider a unit test on syncMapAfterLayoutChange with a mock ctx.map, and an integration test that toggles a layer and asserts the legend item count changes.


🔵 Nice to Have

  • Cache legendEl: Store this.legendEl in createLegend() instead of querySelector on every call.
  • shapes object: The four SVG generator functions are recreated on every getLegendItems() call. Move to a module-level constant.

Happy to push fixes on top of your branch if that's easier — just let me know!

@koala73
Copy link
Copy Markdown
Owner

koala73 commented Mar 27, 2026

Hey @tsubasakong — thanks for picking this up! The bug is real and the getLegendItems() extraction is a clean direction. A couple of things need to land before this can merge.


🔴 Blocking

1. updateLegend() inside the RAF hot path — DOM churn at 60fps

render() fires on every pan, zoom, time-range change, and layer toggle. By placing updateLegend() inside its RAF callback, every one of those events now does:

  • querySelector('.deckgl-legend') on every frame
  • Rebuilds all SVG strings via getLegendItems() (including getCurrentTheme() + multiple t() i18next lookups)
  • legend.innerHTML = ... — full subtree destroy + rebuild every frame
  • document.createElement() for the CII legend, sets its innerHTML, appendChild

For finance, happy, and default variants this is unconditional — same items every time, DOM wiped and rebuilt for nothing. On a 60fps pan lasting 300ms that's 18 full DOM rebuilds.

Fix: Move updateLegend() out of render(). Call it directly in the four layer-mutation paths — the CII toggle handler, setLayers(), enableLayer(), toggleLayer(). Each already calls this.render(), so just add this.updateLegend() before those calls. Or at minimum add a dirty-check guard so the DOM write is skipped when layer state hasn't changed.

2. ciiChoroplethLegend recreation makes the toggle handler dead code

updateLegend() wipes legend.innerHTML then creates a new <div id="ciiChoroplethLegend"> on every call. The existing CII toggle handler (line ~4247) does querySelector('#ciiChoroplethLegend') + manually sets style.display — but render() fires immediately after, updateLegend() recreates the element with the correct display state, and the toggle handler's mutation is silently overwritten. It's now dead code that will confuse future contributors.

Fix: Create ciiLegendEl once in createLegend(), store as private ciiLegendEl. In updateLegend(), after the innerHTML wipe, legend.appendChild(this.ciiLegendEl) (DOM move, not clone) and toggle style.display. Remove the querySelector('#ciiChoroplethLegend') block from the toggle handler.


🟡 Should Fix

3. Remove the RAF call from syncMapAfterLayoutChange

requestAnimationFrame(sync);       // fires ~16ms — CSS transition hasn't started
window.setTimeout(sync, delayMs);  // fires at 320ms — after transition ✓

The original code only had setTimeout(320), which was deliberate (CSS transition duration). The RAF fires before the container has its final dimensions. Remove it, keep only the timeout.

4. finance / happy / default variants don't filter by active layers

Only tech filters entries via this.state.layers[layer]. The default and happy variants include aircraft (maps to flights) and datacenter — both toggleable. Same bug class, other variants.

5. Tests check source text, not behavior

map-fullscreen-resize.test.mjs regex-matches the source file as a string. It doesn't verify the legend actually updates when a layer is toggled (the stated fix for #1967). A unit test on syncMapAfterLayoutChange with a mock ctx.map, or an integration test that toggles a layer and asserts the legend item count changes, would cover the actual fix.


🔵 Nice to Have

  • Cache this.legendEl in createLegend() instead of querySelector on every call.
  • Move the shapes SVG generators to a module-level constant (recreated on every getLegendItems() call).

Happy to push fixes on top of your branch if that's easier — just let me know!

@SebastienMelki
Copy link
Copy Markdown
Collaborator

@tsubasakong@koala73 left detailed blocking feedback on Mar 27. To move this forward:

  1. Blocking Batch market and RSS fetching with progressive updates #1: Move updateLegend() out of the RAF hot path — call it only from layer-mutation paths
  2. Blocking Add smart zoom visibility for dense map layers #2: Fix ciiChoroplethLegend recreation — store as private ciiLegendEl, use DOM move instead of recreate
  3. Rebase on main

Once those are addressed, the should-fix items (RAF in syncMapAfterLayoutChange, non-tech variant filtering) would be good too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

High Value Meaningful contribution to the project Ready to Merge PR is mergeable, passes checks, and adds value trust:safe Brin: contributor trust score safe

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Legend does not update in Map/Globe

3 participants