Skip to main content

streamlit-lonboard

A Streamlit custom component for lonboard — fast, GPU-accelerated geospatial visualization in Streamlit, powered by deck.gl and GeoArrow.

Status: early development. Scatterplot/Path/Polygon/SolidPolygon layers, multi-layer maps, click/hover picking, and view-state persistence across reruns all work. Heatmap is wired but untested; Bitmap/Raster layers aren't supported yet. See IMPLEMENTATION_PLAN.md for the roadmap and progress.

pyarrow 25.0.0 is excluded (pyarrow>=14,!=25.0.0 in pyproject.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, set ARROW_DEFAULT_MEMORY_POOL=system as 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)

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 than st.pydeck_chart's JSON at every scale tested (230MB vs. 1.9GB at 10M points).
  • Map.to_html() embedded via st.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 sandboxed srcdoc iframe, document.location.href is 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_chart itself 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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

streamlit_lonboard-0.1.0.tar.gz (2.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

streamlit_lonboard-0.1.0-py3-none-any.whl (2.1 MB view details)

Uploaded Python 3

File details

Details for the file streamlit_lonboard-0.1.0.tar.gz.

File metadata

  • Download URL: streamlit_lonboard-0.1.0.tar.gz
  • Upload date:
  • Size: 2.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for streamlit_lonboard-0.1.0.tar.gz
Algorithm Hash digest
SHA256 22364286d039d7bbadc57410dbe846a75bf3682d4eccae334b693f2e2f7ae090
MD5 d266affe9d98f645865d5858b6b9695e
BLAKE2b-256 0904e3c544564d416b056dded46ce11052fe1166c74593e97e2d8f82986cd414

See more details on using hashes here.

Provenance

The following attestation bundles were made for streamlit_lonboard-0.1.0.tar.gz:

Publisher: release.yml on relativityhd/streamlit-lonboard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file streamlit_lonboard-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for streamlit_lonboard-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 90056c4fc6ae5733d52afc0d32d61053b5966f981f2afa64fe7e9e2831ce0717
MD5 5c2c175684b2c0387def26c0f67be5b1
BLAKE2b-256 e2324cd44dbc523d4aadcca098cf4d6b53381235eece51c5fbc21b42242b07f3

See more details on using hashes here.

Provenance

The following attestation bundles were made for streamlit_lonboard-0.1.0-py3-none-any.whl:

Publisher: release.yml on relativityhd/streamlit-lonboard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.1

2 files

0.2.0

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page