Part of the Static vs Dynamic Export Methods guide.
Operative rule: Always set notebook_display=False when exporting pydeck maps outside a Jupyter kernel — the default True wraps output in an iframe scaffold that breaks standalone browser rendering.
How It Works
pydeck serializes your Python layer configuration — viewport state, data payloads, and Deck.gl constructor options — into a single .html file. That file bundles an inline JavaScript bootstrap that loads the Deck.gl WebGL runtime (from a CDN by default) and reconstructs the interactive map entirely in the browser. No Python process, no tile server, and no WebSocket connection is needed after the file is written.
The export mechanism sits squarely in the Static vs Dynamic Export Methods decision space: you trade real-time data mutability for a zero-infrastructure artifact that can be committed to a repository, uploaded to object storage, or dropped into an <iframe> inside responsive dashboard layouts. Because all data is inlined at export time, the map reflects the state of your DataFrame at the moment the script ran — fitting for nightly reports, cached snapshots, and scheduled map rebuild workflows that regenerate the file on a cron schedule.
The diagram below shows how Python data flows through pydeck into the standalone HTML artifact and then into the browser’s WebGL pipeline.
Production-Ready Implementation
The script below is copy-ready for use in a CLI tool, CI step, or scheduled map rebuild workflow. It uses Python 3.10+ type hints, produces a deterministic filename with a content hash for cache busting, and wraps the export in a context that works identically in headless runners and local terminals.
from __future__ import annotations
import hashlib
import os
from pathlib import Path
import pandas as pd
import pydeck as pdk
def export_scatterplot_map(
output_dir: Path,
mapbox_key: str = "",
) -> Path:
"""
Build a ScatterplotLayer map from sample data and export it to
a standalone HTML file with a content-hash suffix.
Args:
output_dir: Directory where the .html file will be written.
mapbox_key: Mapbox public token. Pass "" to use a MapLibre
style URL instead (avoids Mapbox billing).
Returns:
Absolute path to the written HTML file.
"""
# 1. Prepare geospatial data — coordinates must be EPSG:4326 (lon/lat).
df = pd.DataFrame(
{
"lat": [40.7128, 34.0522, 41.8781, 29.7604, 47.6062],
"lon": [-74.0060, -118.2437, -87.6298, -95.3698, -122.3321],
"city": ["New York", "Los Angeles", "Chicago", "Houston", "Seattle"],
"value": [120, 85, 200, 150, 95],
}
)
# 2. Define the layer. get_position must list lon before lat.
scatter_layer = pdk.Layer(
"ScatterplotLayer",
data=df.to_dict(orient="records"),
get_position=["lon", "lat"], # Deck.gl expects [longitude, latitude]
get_radius="value",
get_fill_color=[255, 140, 0, 180],
pickable=True,
radius_min_pixels=4,
radius_max_pixels=24,
)
# 3. Choose a basemap style. Swap in a MapLibre URL to skip Mapbox auth.
map_style = (
"mapbox://styles/mapbox/light-v11"
if mapbox_key
else "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
)
# 4. Construct the Deck object.
deck = pdk.Deck(
layers=[scatter_layer],
initial_view_state=pdk.ViewState(
latitude=38.5,
longitude=-96.0,
zoom=3,
pitch=0,
),
map_style=map_style,
mapbox_key=mapbox_key or None,
tooltip={"text": "{city}: {value}"},
)
# 5. Export to HTML.
# notebook_display=False strips the Jupyter iframe wrapper — required
# for any context outside a running Jupyter kernel.
html_string: str = deck.to_html(
notebook_display=False,
as_string=True, # return string so we can hash it
custom_css=(
"body { margin: 0; padding: 0; }"
" #deck-container { width: 100vw; height: 100vh; }"
),
)
# 6. Append a short content hash to the filename for cache busting.
content_hash = hashlib.sha256(html_string.encode()).hexdigest()[:8]
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / f"dashboard_{content_hash}.html"
output_path.write_text(html_string, encoding="utf-8")
return output_path.resolve()
if __name__ == "__main__":
token = os.getenv("MAPBOX_TOKEN", "")
path = export_scatterplot_map(
output_dir=Path("dist"),
mapbox_key=token,
)
print(f"Exported: {path}")
Alternative Variants
Embedding in an existing page via <iframe>
When the exported file is deployed to a static host, drop it into any HTML page with a standard iframe. Set allow="fullscreen" if your layer supports fullscreen toggling.
<iframe
src="/maps/dashboard_a8f3c2.html"
width="100%"
height="600"
frameborder="0"
title="City value distribution map"
allow="fullscreen">
</iframe>
For CORS and sandbox restrictions that affect this pattern, see the iframe embedding isolation guide.
Key to_html() parameter reference
| Parameter | Default | When to change |
|---|---|---|
notebook_display |
True |
Set False outside Jupyter — mandatory for browser-open files |
as_string |
False |
Set True to post-process (hash, upload to S3) before writing |
custom_css |
"" |
Override #deck-container margins for embedded or full-screen use |
iframe_width / iframe_height |
"100%" / "500px" |
Tune for fixed-size dashboard panels in Jupyter |
open_browser |
False |
Set True during local development to auto-open the file |
Offline / air-gapped export: to_html() bakes a CDN <script> tag for the Deck.gl bundle into the output. For air-gapped CI environments, use as_string=True, retrieve the HTML, and replace the CDN <script src="..."> with a <script src="/static/deck.gl.min.js"> pointing to a locally hosted bundle. The Deck.gl release page publishes pre-built UMD bundles alongside each version tag.
Verification Steps
After running the export script, confirm the output is correct before deploying:
- File exists and is non-empty:
ls -lh dist/dashboard_*.html— expect 200 KB–2 MB depending on data payload size. - Deck.gl script tag present:
grep -o 'unpkg.com/deck.gl' dist/dashboard_*.htmlshould return a match (or your local bundle path for offline builds). - Browser smoke-test: Open the file directly with
python -m http.server 8000and navigate tohttp://localhost:8000/dist/dashboard_<hash>.html. The map should render, pan, zoom, and show tooltips on hover. - WebGL check: Open browser DevTools → Console. A
WebGL context lostmessage indicates hardware acceleration is disabled; enable it in browser flags or test on a different machine. - Data size check: Open DevTools → Network, reload, and inspect the document size. If the
.htmlresponse exceeds 5 MB, refactor to host data externally (see the “Inline Data Limits” note in the performance section below).
Common Errors & Fixes
AttributeError: 'Deck' object has no attribute 'to_html'
Root cause: pydeck version is below 0.8.0, where to_html() did not exist. Earlier versions required a notebook=True workaround via deck.show() in Jupyter.
Fix: Upgrade with pip install "pydeck>=0.8.0" and verify with python -c "import pydeck; print(pydeck.__version__)".
Exported file opens but the map canvas is blank (no tiles, no layers)
Root cause: Most common cause is a missing or invalid Mapbox token when using map_style="light" or "dark". Secondary cause: notebook_display=True left in place, producing an iframe-wrapped document that browsers refuse to render as a top-level page.
Fix: Pass a valid mapbox_key to pdk.Deck(), or switch map_style to a MapLibre-compatible URL (e.g. a Carto or ESRI public style). Also set notebook_display=False.
TypeError: Object of type ndarray is not JSON serializable
Root cause: NumPy arrays in the DataFrame were not converted to plain Python types before passing to pdk.Layer. The pydeck serializer does not automatically coerce numpy.float32, numpy.int64, or numpy.nan values.
Fix: Call df = df.astype({"value": float}).where(df.notna(), other=None) before df.to_dict(orient="records"). For GeoJSON geometry columns, extract coordinates as plain Python lists with .tolist().
CORS error when loading data from an external URL
Root cause: The Deck.gl runtime in the exported HTML fetches the data URL at render time. If that URL is on a different origin without Access-Control-Allow-Origin: *, the browser blocks the request.
Fix: Inline the data in the Python script (the default behaviour of df.to_dict(orient="records")), or ensure the data host sets appropriate CORS headers. For large datasets, place the data file on the same origin as the HTML file — e.g. the same S3 bucket with a public bucket policy.
Related
- Static vs Dynamic Export Methods — parent guide covering the full trade-off between pre-rendered HTML bundles and live data endpoints
- Iframe Embedding Isolation — sandbox attributes, CSP headers, and cross-origin considerations when dropping exported maps into host applications
- Responsive Dashboard Layouts — making the iframe or full-page PyDeck export adapt to mobile viewports
- Scheduled Map Rebuild Workflows — automating the export script on a nightly or merge-triggered schedule