Part of the Layer Management & Toggling guide.
Operative rule: Toggle each layer’s visible prop and call deck.setProps({layers}) — do not recreate the Deck; flipping visible reuses GPU buffers and interaction state, while a rebuild reuploads all data.
How a PyDeck Layer Switcher Works
pydeck renders a deck.gl scene, and deck.gl has no layer-control widget of its own — unlike Folium, where toggling GeoJSON overlays with Folium LayerControl is a one-line call. So a switcher in PyDeck is something you build: a set of HTML checkboxes in the exported page plus a small script that talks to the live deck instance through the deck.gl JavaScript API. That is the essence of Layer Management & Toggling for this renderer — the Python side defines the layers and their identities, and the browser side flips them on and off.
The mechanism that makes toggling cheap is deck.gl’s reactive layer model. Every render, deck.gl diffs the new layer list against the previous one by each layer’s id. If an id persists and only a prop like visible changed, deck.gl reuses the layer’s existing GPU buffers and simply re-renders — no data re-upload, no loss of hover or view state. That is why the operative rule is to flip visible and call deck.setProps({layers}) rather than to rebuild the Deck: a rebuild throws away the diff baseline and reuploads every attribute. Stable ids are the linchpin. Assign an explicit id to each pydeck.Layer in Python so the exported JSON carries it, then key your checkboxes to those same ids. This keeps toggling smooth even when rendering a million points with a PyDeck ScatterplotLayer, and it survives the export path described in exporting PyDeck visualizations to standalone HTML.
Production-Ready Implementation
The Python side builds two pydeck.Layer objects with explicit, stable ids and injects a checkbox panel plus a toggle script into the exported HTML. The JavaScript keeps the layer list in memory, flips the matching layer’s visible flag, and pushes the change through deck.setProps. The __deck handle that pydeck exposes on the page is the live deck.gl instance.
from __future__ import annotations
import pandas as pd
import pydeck as pdk
# Stable ids: these MUST match the checkbox data-layer-id values below.
SCATTER_ID = "incidents"
HEX_ID = "density"
def build_switcher_html(output_path: str = "pydeck_switcher.html") -> str:
df = pd.DataFrame(
{
"lat": [40.71, 40.72, 40.70, 40.73],
"lon": [-74.00, -74.01, -73.99, -74.02],
"weight": [10, 40, 25, 60],
}
)
records = df.to_dict(orient="records")
scatter = pdk.Layer(
"ScatterplotLayer",
id=SCATTER_ID, # explicit id -> deck.gl can diff it
data=records,
get_position=["lon", "lat"],
get_radius="weight",
get_fill_color=[228, 87, 86, 180],
radius_min_pixels=3,
pickable=True,
visible=True,
)
hexagon = pdk.Layer(
"HexagonLayer",
id=HEX_ID,
data=records,
get_position=["lon", "lat"],
radius=200,
elevation_scale=4,
extruded=True,
visible=False, # starts hidden; user can switch on
)
deck = pdk.Deck(
layers=[scatter, hexagon],
initial_view_state=pdk.ViewState(latitude=40.715, longitude=-74.0,
zoom=13, pitch=30),
map_style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
tooltip={"text": "weight: {weight}"},
)
# Export as a string so we can splice in the control panel + script.
html = deck.to_html(notebook_display=False, as_string=True)
controls = f"""
<div id="layer-switch" style="position:absolute;top:12px;right:12px;z-index:10;
background:rgba(255,255,255,0.9);padding:8px 12px;border-radius:6px;font-family:sans-serif;">
<label style="display:block"><input type="checkbox" data-layer-id="{SCATTER_ID}" checked> Incidents</label>
<label style="display:block"><input type="checkbox" data-layer-id="{HEX_ID}"> Density</label>
</div>
<script>
(function () {{
// pydeck exposes the live deck.gl instance on window as __deck.
function wire() {{
var deck = window.__deck || (window.deckInstance);
if (!deck) {{ return setTimeout(wire, 100); }} // wait for boot
// Keep our own reference to the current layer array.
var layers = deck.props.layers.slice();
document.querySelectorAll('#layer-switch input').forEach(function (box) {{
box.addEventListener('change', function () {{
var id = box.getAttribute('data-layer-id');
layers = layers.map(function (layer) {{
// Clone with the flipped visible prop; keep the SAME id so
// deck.gl diffs instead of rebuilding.
return layer.id === id
? layer.clone({{ visible: box.checked }})
: layer;
}});
deck.setProps({{ layers: layers }}); // re-render in place
}});
}});
}}
wire();
}})();
</script>
"""
html = html.replace("</body>", controls + "</body>")
with open(output_path, "w", encoding="utf-8") as fh:
fh.write(html)
return output_path
if __name__ == "__main__":
print("Wrote", build_switcher_html())
Alternative Variants
Regenerating the layer set by id when the data changes
Flipping visible covers show/hide. When the set of layers changes — a new category arrives from a fresh data fetch — rebuild the array by id and call setProps once. This still avoids constructing a new Deck; it only replaces the layer list.
// Rebuild the layer array from a spec keyed by id, then push once.
function applyLayerSpec(deck, specById) {
const layers = Object.entries(specById).map(([id, cfg]) =>
new deck.constructor.Layer({ id, ...cfg }) // stable id per entry
);
deck.setProps({ layers }); // one re-render; no new Deck instance
}
Toggle strategy reference
| Change | Approach | Cost |
|---|---|---|
| Show/hide one layer | clone({visible}) + setProps |
Cheapest — buffer reuse |
| Restyle (color/radius) | clone({getFillColor}) + setProps |
Cheap — attribute update |
| Add/remove a layer | Rebuild array by id + setProps |
Moderate — diff by id |
| Change data source | New data prop, same id | Moderate — data re-upload |
Recreate Deck |
new deck.Deck(...) |
Most expensive — avoid |
Verification Steps
- Ids are present: open the exported HTML and confirm each layer object in the embedded JSON carries the
idyou set; missing ids force rebuilds. - Toggle hides in place: uncheck a box and confirm the layer vanishes with no visible reload of the other layer and no view-state reset.
- No data re-upload on toggle: open DevTools → Network, toggle a layer, and confirm no new data request fires — visibility changes must stay on the GPU.
- State survives: hover a point to show a tooltip, toggle the other layer, and confirm the view position and interaction are preserved.
- Checkbox ids match: confirm every
data-layer-idequals a real layerid; a mismatch silently does nothing.
Common Errors & Fixes
Toggling a layer reloads or flickers the whole scene
The layers have no stable id, so deck.gl cannot match old to new and treats each setProps as an entirely new layer set, reuploading data. Fix: give every pydeck.Layer an explicit id in Python and preserve it when you clone the layer in JavaScript. With matching ids, deck.gl diffs and only flips visible.
Checkbox does nothing
Either the data-layer-id does not match any layer id, or the script ran before pydeck created the deck instance so window.__deck was undefined. Fix: confirm the ids line up exactly, and poll for the deck handle (as the implementation does with setTimeout(wire, 100)) so wiring happens only after the deck has booted.
Layer disappears but the map resets its pitch and zoom
The handler is recreating the Deck (or calling setProps without preserving initialViewState/other layers) instead of cloning the single changed layer. Fix: mutate only the target layer via clone({visible}), keep the rest of the array intact, and pass just {layers} to setProps so the view state is untouched. This is the operative rule — toggle visible, never rebuild.
layer.clone is not a function
The handler is holding plain serialized layer descriptors rather than live deck.gl Layer instances. pydeck’s exported JSON is instantiated by the runtime, so read the current layers from deck.props.layers, which are real instances with clone, rather than reconstructing them from the JSON. Fix: seed your working array from deck.props.layers.slice().
Related
- Layer Management & Toggling — parent guide covering toggle strategy across renderers
- Toggling GeoJSON Overlays with Folium LayerControl — the built-in control PyDeck lacks, for comparison
- Rendering a Million Points with PyDeck ScatterplotLayer — why buffer reuse on toggle matters at scale
- Exporting PyDeck Visualizations to Standalone HTML — the export path this switcher is spliced into