Skip to content

fix: update custom map legend panel when exporting image - #372

Merged
bdjulbic merged 4 commits into
mainfrom
fix/image-export-map-legend
Feb 20, 2026
Merged

bdjulbic merged 4 commits into
mainfrom
fix/image-export-map-legend

Conversation

@bdjulbic

@bdjulbic bdjulbic commented Feb 20, 2026 •

Copy link
Copy Markdown
Collaborator

Related to this slack thread:
https://foursquare.slack.com/archives/C08ANNK76FM/p1771564223125299?thread_ts=1771543581.796869&cid=C08ANNK76FM

tasks:

  • Change panel header to show Legend instead of Map Layers
  • Remove eye icon from the legend panel
  • Show only visible lanes in the legend panel
  • When exporting image fix the issue with double header title showing

Screenshot of the UI and exported image:
Screenshot 2026-02-20 at 14 14 36
Untitled Map

Summary by CodeRabbit

  • New Features
    • Export mode cleans up map legend: header and panel actions are hidden, visibility toggle controls are concealed, and only layers currently set visible are included in exports for a clearer, more professional output.

@coderabbitai

coderabbitai Bot commented Feb 20, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Map legend component gained an isExport prop. In export mode the header and visibility controls are omitted; only layers with layer.config.isVisible === true are rendered. LayerLegendItem now accepts isExport and visibility toggle logic/button were removed.

Changes

Cohort / File(s) Summary
Map legend export & visibility updates
packages/kepler/src/components/CustomMapLegend.tsx
Added isExport prop to MapLegend and LayerLegendItem; header and header actions hidden in export mode; layers are filtered to include only those with layer.config.isVisible; removed visibility toggle UI and handler; isExport forwarded to items.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 I nibble on pixels, tidy and spry,
Headers tuck in when the export comes by,
Only visible layers hop into sight,
Toggles tuck away for a calm, clean night. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly reflects the main change: updating the custom map legend panel's behavior during image export by conditionally hiding the header and visibility toggles.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/image-export-map-legend

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/kepler/src/components/CustomMapLegend.tsx (2)

166-177: ⚠️ Potential issue | 🔴 Critical

Visibility toggle button is permanently hidden in all modes, not just export

hidden is hardcoded unconditionally in the className, so the eye toggle is invisible even in normal (non-export) mode. Users can no longer toggle layer visibility from the legend panel at all. Per the PR objective, the button should only be hidden when isExport is true.

🐛 Proposed fix
-            <Button
-              className="h-7 w-7 hidden"
-              variant="ghost"
-              size="icon"
-              onClick={handleToggleVisibility}
-            >
+            <Button
+              className={`h-7 w-7${isExport ? ' hidden' : ''}`}
+              variant="ghost"
+              size="icon"
+              onClick={handleToggleVisibility}
+            >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kepler/src/components/CustomMapLegend.tsx` around lines 166 - 177,
The visibility toggle Button in CustomMapLegend is always hidden because the
string "hidden" is unconditionally in its className; update the Button rendering
(the Button that calls handleToggleVisibility and checks layer.config.isVisible)
to remove the unconditional "hidden" and only apply "hidden" when the prop/state
isExport is true (e.g., build className conditionally or use a classnames
utility) so the eye toggle is visible in normal mode and hidden in export mode.

147-149: ⚠️ Potential issue | 🟡 Minor

Guard at lines 147–149 is dead code given the current unconditional filter

Because the parent already strips all invisible layers via .filter((layer) => layer.config.isVisible) before passing them to LayerLegendItem, layer.config.isVisible is always true at this point, making isExport && !layer.config.isVisible permanently false. Once the filter above is fixed to be !isExport || layer.config.isVisible, this guard becomes meaningful and should be kept — so no removal needed, just noting the dependency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kepler/src/components/CustomMapLegend.tsx` around lines 147 - 149,
The parent currently filters out invisible layers unconditionally, making the
guard in LayerLegendItem (the if (isExport && !layer.config.isVisible) return
null; check) dead; update the parent filter logic to respect export mode by
changing its predicate to allow invisible layers when not exporting (use
!isExport || layer.config.isVisible) so the guard in LayerLegendItem becomes
meaningful, and leave the existing guard inside LayerLegendItem intact; locate
the parent use of .filter(...) that prepares layers for rendering in
CustomMapLegend and adjust it accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kepler/src/components/CustomMapLegend.tsx`:
- Around line 82-94: The legend currently filters out layers with
layer.config.isVisible unconditionally; change the logic so the .filter(...) is
only applied when isExport is true. Locate the JSX mapping over layers in
CustomMapLegend (the .map that renders LayerLegendItem) and conditionally
compute the list to map from: if isExport is true use layers.filter(l =>
l.config.isVisible), otherwise use layers as-is; keep passing key/index, layer,
containerW, isExport and {...restProps} to LayerLegendItem unchanged.

---

Outside diff comments:
In `@packages/kepler/src/components/CustomMapLegend.tsx`:
- Around line 166-177: The visibility toggle Button in CustomMapLegend is always
hidden because the string "hidden" is unconditionally in its className; update
the Button rendering (the Button that calls handleToggleVisibility and checks
layer.config.isVisible) to remove the unconditional "hidden" and only apply
"hidden" when the prop/state isExport is true (e.g., build className
conditionally or use a classnames utility) so the eye toggle is visible in
normal mode and hidden in export mode.
- Around line 147-149: The parent currently filters out invisible layers
unconditionally, making the guard in LayerLegendItem (the if (isExport &&
!layer.config.isVisible) return null; check) dead; update the parent filter
logic to respect export mode by changing its predicate to allow invisible layers
when not exporting (use !isExport || layer.config.isVisible) so the guard in
LayerLegendItem becomes meaningful, and leave the existing guard inside
LayerLegendItem intact; locate the parent use of .filter(...) that prepares
layers for rendering in CustomMapLegend and adjust it accordingly.

Comment on lines +82 to +94
{layers
.filter((layer) => layer.config.isVisible)
.map((layer, index) => {
return (
<LayerLegendItem
key={index}
layer={layer}
containerW={containerW}
isExport={isExport}
{...restProps}
/>
);
})}

@coderabbitai coderabbitai Bot Feb 20, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Layer filter runs unconditionally, breaking non-export visibility management

.filter((layer) => layer.config.isVisible) is applied regardless of isExport. In normal mode, layers toggled off disappear from the legend with no way to re-enable them through this panel (and the toggle button is also hidden per the bug above). Filtering should only apply during export.

🐛 Proposed fix
-            {layers
-              .filter((layer) => layer.config.isVisible)
-              .map((layer, index) => {
+            {layers
+              .filter((layer) => !isExport || layer.config.isVisible)
+              .map((layer, index) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kepler/src/components/CustomMapLegend.tsx` around lines 82 - 94, The
legend currently filters out layers with layer.config.isVisible unconditionally;
change the logic so the .filter(...) is only applied when isExport is true.
Locate the JSX mapping over layers in CustomMapLegend (the .map that renders
LayerLegendItem) and conditionally compute the list to map from: if isExport is
true use layers.filter(l => l.config.isVisible), otherwise use layers as-is;
keep passing key/index, layer, containerW, isExport and {...restProps} to
LayerLegendItem unchanged.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hidden layers should not show in the legend panel irregardless of the isExport

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@bdjulbic, thank you for the clarification! I understand now that the Legend panel is designed to display only visible layers, regardless of whether it's in export mode or normal mode. The filter .filter((layer) => layer.config.isVisible) is working as intended.

The visibility toggling functionality is handled in the MAP SETTINGS panel (as shown in your screenshots), not in the Legend panel. The Legend panel's purpose is solely to display the legend information for layers that are currently visible on the map.

I'll note this design decision for future reviews.


✏️ Learnings added
Learnt from: bdjulbic
Repo: sqlrooms/sqlrooms PR: 372
File: packages/kepler/src/components/CustomMapLegend.tsx:82-94
Timestamp: 2026-02-20T14:09:18.651Z
Learning: In the CustomMapLegend component (packages/kepler/src/components/CustomMapLegend.tsx), the Legend panel should only display layers where layer.config.isVisible is true, regardless of whether it's in export mode or normal mode. The filter `.filter((layer) => layer.config.isVisible)` before mapping layers is intentional and correct. Visibility toggling is handled in the MAP SETTINGS panel, not in the Legend panel.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
packages/kepler/src/components/CustomMapLegend.tsx (3)

149-149: Duplicate <style> tag injected per layer item — move to a single injection point.

A new <style> element is created and inserted into the DOM for every rendered LayerLegendItem. With N layers, the browser receives N identical style blocks to parse and apply. Move this rule to a module-level CSS file, a single top-level <style> tag at the MapLegend render root, or a createGlobalStyle / injectGlobal equivalent.

Additionally, this rule fires unconditionally, so kepler.gl's panel header actions are suppressed even in non-export mode (assuming the intent is export-only).

♻️ Proposed refactor — hoist style to component root, guarded by `isExport`

Remove the per-item <style> tag from LayerLegendItem:

-        <style>{`.legend--layer__item .panel--header__action { display: none !important; }`}</style>

Inject once at the MapLegend level, conditionally on isExport:

       <div className="flex w-full flex-1 flex-col items-center">
+          {isExport && (
+            <style>{`.legend--layer__item .panel--header__action { display: none !important; }`}</style>
+          )}
           {layers
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kepler/src/components/CustomMapLegend.tsx` at line 149, The inline
<style> injection in LayerLegendItem is duplicated per layer and unconditionally
hides panel header actions; remove the per-item injection from the
LayerLegendItem component and instead inject the rule `.legend--layer__item
.panel--header__action { display: none !important; }` once at the MapLegend
render root (or in a module/global CSS or createGlobalStyle), and only apply it
when isExport is true so the rule is guarded; update MapLegend to render that
single conditional style (or import the CSS) and ensure LayerLegendItem no
longer inserts any <style> tags.

140-142: Redundant guard — already covered by the layers.filter(...) upstream.

After the correct fix to line 83 (!isExport || layer.config.isVisible), invisible layers in export mode will never reach LayerLegendItem, making this early-return unreachable. It is also currently dead code given the unconditional filter on line 83. This block can be removed once the upstream filter is corrected.

-    if (isExport && !layer.config.isVisible) {
-      return null;
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kepler/src/components/CustomMapLegend.tsx` around lines 140 - 142,
Remove the redundant early-return guard in LayerLegendItem that checks "if
(isExport && !layer.config.isVisible) return null;" because invisible layers in
export mode are already filtered out upstream (after fixing the upstream filter
to "!isExport || layer.config.isVisible"), so this block is dead code; locate
the conditional in the LayerLegendItem component and delete the entire
if-statement to simplify the component and avoid unreachable code.

109-109: isExpanded initialised from layer.config.isVisible — orthogonal concerns.

Expand/collapse state and layer visibility are independent. Initialising isExpanded from layer.config.isVisible also means that once the unconditional filter (line 83) is fixed and invisible layers re-appear in normal mode, they will all start collapsed rather than in a consistent default state. true (start expanded) is the conventional default for legend items.

-    const [isExpanded, setIsExpanded] = useState(layer.config.isVisible);
+    const [isExpanded, setIsExpanded] = useState(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kepler/src/components/CustomMapLegend.tsx` at line 109, The
expand/collapse state is incorrectly initialized from layer.config.isVisible (in
CustomMapLegend using const [isExpanded, setIsExpanded] = useState(...)),
coupling visibility with expansion; change the initializer to a consistent
default (useState(true)) so legend items start expanded, and leave
layer.config.isVisible solely for rendering visibility—update any code that
relied on the previous coupling to use the separate isExpanded state instead of
layer.config.isVisible where appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/kepler/src/components/CustomMapLegend.tsx`:
- Around line 82-94: In CustomMapLegend, the unconditional .filter((layer) =>
layer.config.isVisible) removes non-visible layers even in interactive map mode;
update the rendering so the filter is applied only for export mode (isExport) —
e.g., compute the list to render as isExport ? layers.filter(l =>
l.config.isVisible) : layers — and pass each layer into LayerLegendItem (retain
containerW/isExport and {...restProps}) so the legend can display invisible
layers and allow toggling visibility when not exporting; locate the map/list
rendering around the layers.map(...) block and adjust accordingly.

---

Nitpick comments:
In `@packages/kepler/src/components/CustomMapLegend.tsx`:
- Line 149: The inline <style> injection in LayerLegendItem is duplicated per
layer and unconditionally hides panel header actions; remove the per-item
injection from the LayerLegendItem component and instead inject the rule
`.legend--layer__item .panel--header__action { display: none !important; }` once
at the MapLegend render root (or in a module/global CSS or createGlobalStyle),
and only apply it when isExport is true so the rule is guarded; update MapLegend
to render that single conditional style (or import the CSS) and ensure
LayerLegendItem no longer inserts any <style> tags.
- Around line 140-142: Remove the redundant early-return guard in
LayerLegendItem that checks "if (isExport && !layer.config.isVisible) return
null;" because invisible layers in export mode are already filtered out upstream
(after fixing the upstream filter to "!isExport || layer.config.isVisible"), so
this block is dead code; locate the conditional in the LayerLegendItem component
and delete the entire if-statement to simplify the component and avoid
unreachable code.
- Line 109: The expand/collapse state is incorrectly initialized from
layer.config.isVisible (in CustomMapLegend using const [isExpanded,
setIsExpanded] = useState(...)), coupling visibility with expansion; change the
initializer to a consistent default (useState(true)) so legend items start
expanded, and leave layer.config.isVisible solely for rendering
visibility—update any code that relied on the previous coupling to use the
separate isExpanded state instead of layer.config.isVisible where appropriate.

@bdjulbic bdjulbic changed the title update custom map legend panel fix: update custom map legend panel when exporting image Feb 20, 2026
@bdjulbic
bdjulbic requested a review from Copilot February 20, 2026 14:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the custom map legend panel to improve the appearance of exported images by cleaning up the legend UI. The changes conditionally hide interactive elements and filter layers based on visibility when exporting maps.

Changes:

  • Added isExport prop to control legend behavior during image export
  • Conditionally hide the legend header (title and close button) in export mode
  • Remove layer visibility toggle button from legend items
  • Filter layers to only show visible ones in the legend
Comments suppressed due to low confidence (1)

packages/kepler/src/components/CustomMapLegend.tsx:170

  • Consider hiding the expand/collapse chevron button in export mode for a cleaner appearance in the exported image. Since the export is a static image, interactive controls like this button don't serve a functional purpose. If hidden, ensure layers are expanded by default in export mode so the legend content is visible. This could be done by adding {!isExport && <Button ...>} around the button and ensuring isExpanded is set to true when isExport is true.
            <Button
              className="h-7 w-7"
              variant="ghost"
              size="icon"
              onClick={handleToggleExpanded}
            >
              {isExpanded ? (
                <ChevronDownIcon className="h-4 w-4" />
              ) : (
                <ChevronRightIcon className="h-4 w-4" />
              )}
            </Button>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

);
})}
{layers
.filter((layer) => layer.config.isVisible)

Copilot AI Feb 20, 2026

Copy link

Choose a reason for hiding this comment

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

The visibility filter should be applied conditionally based on isExport. Currently, all invisible layers are filtered out even in normal (non-export) mode. This changes the existing behavior where users could see and interact with invisible layers in the legend. The filter should only apply when isExport is true: .filter((layer) => !isExport || layer.config.isVisible)

Suggested change
.filter((layer) => layer.config.isVisible)
.filter((layer) => !isExport || layer.config.isVisible)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hidden layers should not show in the legend panel irregardless of the isExport

@bdjulbic
bdjulbic merged commit 144741e into main Feb 20, 2026
9 of 10 checks passed
@bdjulbic
bdjulbic deleted the fix/image-export-map-legend branch February 20, 2026 15:51
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.

3 participants