streamlit-lonboard
A Streamlit custom component for lonboard — fast, GPU-accelerated geospatial visualization in Streamlit, powered by deck.gl and GeoArrow.
AI Slop This library was completely generated by the Claude Code Agent utilizing the latest models (Sonnet 5, Opus 5, and Fable 5). I needed this connector for another project, where I also tested its functionality. Every single line of code was reviewed by myself; overall, the code looked okay-ish to me. Nothing I would do myself, but it works and it works well enough. Of course, I would welcome proper first-party support by Lonboard to make this package obsolete. Until then, this slop may allow us to use Lonboard for Streamlit dashboards. If you hesitate to use AI-generated code, do NOT use this library!
Status: early development. Scatterplot/Path/Polygon/SolidPolygon/Column/PointCloud layers, multi-layer maps, click/hover picking, and view-state persistence across reruns all work. Heatmap is wired but untested. DGGS layers — H3, S2, A5, Geohash — and Arc layers work too, but they carry geometry in accessor columns (cell IDs / point pairs) rather than a bounding geometry column, so lonboard can't auto-compute a default view — pass an explicit
view_state=(H3 can auto-center, but only ifh3-pyis installed). Trip layers render a single static frame (drivelayer._current_timeyourself, e.g. from a slider, for animation — no built-in animation loop). Text/Bitmap/BitmapTile/Raster layers aren't supported yet (Text is provisional upstream; Bitmap/Raster carry raster, not GeoArrow, data).All four lonboard layer extensions work —
PathStyleExtension,DataFilterExtension(pairs well withst.sliderdrivinglayer.filter_range),BrushingExtension,CollisionFilterExtension.st_lonboard()also forwardspicking_radius,parameters,use_device_pixels,custom_attribution,map.controls(fullscreen/zoom/scale, on by default -GeocoderControlisn't supported), and hovertooltip=(bool or explicit column list). Seeexamples/extensions_app.py.pyarrow 25.0.0 is excluded (
pyarrow>=14,!=25.0.0inpyproject.toml): its bundled mimalloc 3.3.1 segfaults when libarrow is first loaded on a non-main thread that then exits — which is exactly how Streamlit runs every script. Known upstream as apache/arrow#50471 / microsoft/mimalloc#1287; no fixed release yet. If another dependency forces 25.0.0 on you, setARROW_DEFAULT_MEMORY_POOL=systemas a workaround. All Python versions ≥3.11 (including 3.14) are supported.
Install
uv add streamlit-lonboard
or via pip
pip install streamlit-lonboard
Why?
Streamlit's built-in DeckGL support (st.pydeck_chart) goes through pydeck, which serializes data as GeoJSON/JSON — slow to encode, slow to transfer, slow to parse, and impractical beyond ~100k features.
Lonboard instead moves data as Apache Arrow (GeoArrow) binary buffers that deck.gl can consume with zero parsing. But lonboard is built on anywidget / Jupyter widgets, which Streamlit does not support. The lonboard maintainers consider a Streamlit connector out of scope for lonboard itself, but support a third-party one — this project is that connector.
The previously suggested workarounds don't cut it:
Map.to_html()+st.components.v1.html: static snapshot, no bidirectionality, full re-render on every rerun, huge inlined HTML.streamlit-deckgl: pydeck/JSON only — exactly the bottleneck we want to avoid.
How it works
lonboard Map/Layers (Python) frontend (TypeScript)
pyarrow.Table (GeoArrow) apache-arrow: parse IPC
→ Arrow IPC bytes ──────► → @geoarrow/deck.gl-layers
layer props → JSON → deck.gl + MapLibre basemap
▲ │
└── picking / view state (bidi) ◄───────┘
Data crosses the Python↔browser boundary as raw Arrow IPC bytes via Streamlit's custom components v2 — no GeoJSON anywhere in the pipeline.
API
import geopandas as gpd
import streamlit as st
from lonboard import ScatterplotLayer
from streamlit_lonboard import st_lonboard
gdf = gpd.read_parquet("internet-speeds.parquet")
layer = ScatterplotLayer.from_geopandas(gdf, get_fill_color=[255, 0, 0])
result = st_lonboard(layers=[layer], height=600, key="map")
st.write("Clicked feature index:", result.clicked)
See examples/app.py for scatterplot/path/polygon,
examples/dggs_app.py for H3 and Arc layers (the "geometry lives in
accessor columns" case — see the status note above), and
examples/extensions_app.py for layer extensions, tooltip=, and map
controls/attribution.
Performance
Streamlit reruns your whole script on every interaction, so building this
ScatterplotLayer from scratch happens again on every rerun unless you cache
it. Wrap layer construction in @st.cache_resource:
@st.cache_resource
def build_layer():
gdf = gpd.read_parquet("internet-speeds.parquet")
return ScatterplotLayer.from_geopandas(gdf, get_fill_color=[255, 0, 0])
layer = build_layer()
result = st_lonboard(layers=[layer], height=600, key="map")
This matters more than it might look like: st_lonboard() memoizes its own
Arrow serialization keyed on the layer object, so a cached layer skips
re-serialization entirely on reruns that don't touch it (invalidated
automatically if you mutate a layer's properties). Without
@st.cache_resource, a fresh layer object is built every rerun and the cache
never hits. See examples/app.py for a full example and
IMPLEMENTATION_PLAN.md Phase 4 for the full
performance investigation, including a genuinely surprising find: Streamlit's
component runtime already skips re-parsing and re-rendering on the frontend
entirely when a rerun's output is byte-for-byte unchanged (see
benchmarks/RESULTS.md for measured numbers at
10k/100k/1M points) — so the main thing left to optimize is Python-side
re-serialization, which is exactly what the cache above avoids.
Compression
st_lonboard(..., compression="auto" | "gzip" | None) (default "auto")
gzips the Arrow payload above a 1MB threshold. Measure before relying on
this — at 1M points it only shaved off ~11% (clustered and uniform-random
data compressed about the same; gzip finds repeated byte sequences, not
spatial/numeric proximity, so real GPS-precision coordinates don't compress
much better than random ones) while costing ~900ms-1s of Python-side CPU plus
~200ms of browser-side decompression per rerun — a net loss on localhost or
any reasonably fast link, and only a likely win on slow/high-latency
connections where the transfer savings outweigh that added CPU time. See
benchmarks/RESULTS.md for the numbers behind
this. Pass compression=None to disable it outright.
vs. st.pydeck_chart and Map.to_html()
Measured across 10k-10M points (benchmarks/playwright_driver.py,
benchmarks/payload_sizes.py; full numbers and methodology in
benchmarks/RESULTS.md):
- Wire size:
st_lonboard's Arrow IPC payload is a consistent ~8.3x smaller thanst.pydeck_chart's JSON at every scale tested (230MB vs. 1.9GB at 10M points). Map.to_html()embedded viast.components.v1.html— the workaround people use today without a custom component — doesn't render at all, at any scale. Root cause: inside Streamlit's sandboxedsrcdociframe,document.location.hrefis the opaque string"about:srcdoc", which breaks requirejs/anywidget's module-loading URL resolution; the actual widget bundle never loads and no error is shown. The same HTML renders fine served standalone (outside an iframe).st.pydeck_chartitself renders fine interactively, but rendering timing wasn't reliably measurable under headless browser automation in our environment (an intermittent WebGL/GPU stall unrelated to pydeck's correctness) — reported as an environment limitation rather than forced.
Development
Managed with uv. A Hatchling build
hook runs npm install && npm run build automatically
whenever the package is built or synced, so uv sync/uv build produce a
wheel with the frontend already bundled into
src/streamlit_lonboard/frontend_dist/ (gitignored source-tree-side; only
Node is required to build it, not to install the published wheel):
uv sync --extra dev
uv run streamlit run examples/app.py
If you edit the frontend, run cd frontend && npm run dev (watch build) or
npm run build (one-off) yourself and refresh the browser tab — the build
hook only runs when the package itself is (re)built (uv sync/uv build),
not on every uv run.
uv build # sdist + wheel into dist/
uv run pytest # tests/test_serialize.py
uv run ruff check # lint
License
MIT for this project's own code (Python and frontend/src/). The built
frontend_dist/index.js bundles compiled code from deck.gl, apache-arrow,
maplibre-gl and their transitive dependencies under their own licenses
(mostly MIT/BSD-3-Clause, with Apache-2.0 for apache-arrow and flatbuffers);
a generated THIRD-PARTY-NOTICES.txt listing them and their license texts
ships alongside it in every wheel.
Acknowledgements
- lonboard by Development Seed
- @geoarrow/deck.gl-layers
- Prior discussion: developmentseed/lonboard#342
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file streamlit_lonboard-0.2.0.tar.gz.
File metadata
- Download URL: streamlit_lonboard-0.2.0.tar.gz
- Upload date:
- Size: 2.6 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ac6d35e4044a46ccc4547b31f48ed2ae5d9d9395c35ec1866a055a6d6368c1e1
|
|
| MD5 |
e250a2f5e4261cc2b73ae6defd7b2c59
|
|
| BLAKE2b-256 |
9843320331894b4d24d8f16c1cf74c4187a5d44d61b34aeec69f3184e32cce53
|
Provenance
The following attestation bundles were made for streamlit_lonboard-0.2.0.tar.gz:
Publisher:
release.yml on relativityhd/streamlit-lonboard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
streamlit_lonboard-0.2.0.tar.gz -
Subject digest:
ac6d35e4044a46ccc4547b31f48ed2ae5d9d9395c35ec1866a055a6d6368c1e1 - Sigstore transparency entry: 2531266951
- Sigstore integration time:
-
Permalink:
relativityhd/streamlit-lonboard@e8fee3dcec48da0fb1b3e5d733286cd20d9d4484 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/relativityhd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e8fee3dcec48da0fb1b3e5d733286cd20d9d4484 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file streamlit_lonboard-0.2.0-py3-none-any.whl.
File metadata
- Download URL: streamlit_lonboard-0.2.0-py3-none-any.whl
- Upload date:
- Size: 2.6 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f168c1080677923ac558410d90b012a30dd18498f51a1a18f49b3a6da62aa9f2
|
|
| MD5 |
88ba0ffd66474d39463d4714472a3e03
|
|
| BLAKE2b-256 |
91407a7f8525109f54806052b4638a6f90b29fe2743cfe032ffbe21d89c736d0
|
Provenance
The following attestation bundles were made for streamlit_lonboard-0.2.0-py3-none-any.whl:
Publisher:
release.yml on relativityhd/streamlit-lonboard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
streamlit_lonboard-0.2.0-py3-none-any.whl -
Subject digest:
f168c1080677923ac558410d90b012a30dd18498f51a1a18f49b3a6da62aa9f2 - Sigstore transparency entry: 2531267006
- Sigstore integration time:
-
Permalink:
relativityhd/streamlit-lonboard@e8fee3dcec48da0fb1b3e5d733286cd20d9d4484 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/relativityhd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e8fee3dcec48da0fb1b3e5d733286cd20d9d4484 -
Trigger Event:
workflow_dispatch
-
Statement type: