August 31, 2026

![Josiah Parry](https://ricochet.rs/author-avatars/parry.png)Josiah Parry

# Scalable Spatial Viz: Lonboard and Shiny for Python

How GeoArrow, GeoParquet, and GPU rendering are reshaping geospatial visualization.

[python](https://ricochet.rs/blog/tags/python) [shiny](https://ricochet.rs/blog/tags/shiny) [spatial](https://ricochet.rs/blog/tags/spatial)

The release of ricochet [v0.6](https://docs.ricochet.rs/releases/index.html) stabilized support for [Shiny for Python](https://shiny.posit.co/py/), rounding out our existing Shiny support across both R and Python.

To celebrate, we built a demo app ([source](https://github.com/ricochet-rs/test-apps/tree/main/python/shiny-py-lonboard)) that showcases modern scalable geospatial visualization using [`lonboard`](https://developmentseed.org/lonboard/) from [Development Seed](https://developmentseed.org/). The app visualizes the [King County housing sales dataset](https://www.kaggle.com/datasets/harlfoxem/housesalesprediction) (~22k home sales) on an interactive map with filtering by price, bedrooms, bathrooms, and square footage.

The app is live on [gallery.ricochet.rs](https://gallery.ricochet.rs/app/shiny-lonboard/) and embedded below.

[shiny-lonboard](https://gallery.ricochet.rs/app/shiny-lonboard/)

This blog post will discuss what makes lonboard _so fast_ 🏎️💨.

## TL;DR

-   GeoJSON adds a _ton_ bloat and is a real bottleneck
-   Using “end-to-end binary data pipeline” provides meaningful speed ups
-   [GeoParquet](https://geoparquet.org/) results in smaller file sizes
-   [GeoArrow](https://geoarrow.org/) native represention results in 0 cost data transfer

## Run the app locally

If you want to run the app directly, the source code for the app is in our repo [`ricochet-rs/test-apps`](https://github.com/ricochet-rs/test-apps/).

Clone the example:

bash

```bash
git clone --filter=blob:none --sparse https://github.com/ricochet-rs/test-apps.git
cd test-apps
git sparse-checkout set python/shiny-py-lonboard
cd python/shiny-py-lonboard
```

Install dependencies and run locally:

bash

```bash
uv sync
uv run shiny run app.py
```

## Status Quo

Interactive visualization of spatial data has traditionally been done using the library [Leaflet](https://leafletjs.com/) (via [folium](https://python-visualization.github.io/folium/latest/) in python and [`{leaflet}`](https://rstudio.github.io/leaflet/) in R). There are other alternative such as [`pydeck`](https://deckgl.readthedocs.io/en/latest/) and [kepler.gl](https://github.com/keplergl/kepler.gl). The R community has also begun transitioning away from `{leaflet}` towards the newer and modern [`{mapgl}`](https://walker-data.com/mapgl/).

However, **all of** these libraries still suffer from the same bottleneck: [GeoJSON](https://geojson.org/). GeoJSON is an extension of [JSON](https://www.json.org/json-en.html) (JavaScript Object Notation). It is a _pure text_ representation of spatial data. And frankly, text **is not** an efficient way to store spatial data—a shapefile is likely better.

There are two primary issues with GeoJSON:

1.  It is text which grows in size very quickly
2.  Data in GeoJSON needs to be _parsed_ into an in-memory representation
3.  GeoJSON is row-based representation

### (De)serialization

Converting an in memory object such as a data frame (e.g. `GeoDataFrame` or `sf`) to another format is called **serialization**. Serializing to GeoJSON can be _very_ slow. Going the other way—e.g. _reading_ from a GeoJSON—is called **de**serialization. This is, in most cases, even slower.

Most browser-based mapping tools (Leaflet, MapLibre, pydeck, etc) require your data to arrive as GeoJSON.

This results in a workflow like so:

```
flowchart LR
A[GeoDataFrame] --> B[GeoJSON]
B --> C[Send to the browser]
C --> D[Parse the GeoJSON]
D --> E[Render]
```

The (de)serialization steps take up the bulk of the time.

The whole workflow is fraught. Serializing a GeoDataFrame toe GeoJSON is slow. The string itself is large. Parsing it in the browser is slow. For small datasets, this isn’t very noticable. However, when working with detailed geometries or large datasets, that round-trip becomes a huge bottleneck.

Lonboard’s special sauce is that it uses an [“end-to-end binary data pipeline”](https://nyhackr.blob.core.windows.net/presentations/Lonboard-Fast-Interactive-Geospatial-Vector-Data-Visualization-in-Jupyter-and-JavaScript_Kyle-Barron.pdf) skipping GeoJSON entirely.

## Lonboard’s innovations

There are 3 major innovations to lonboard that make it so powerful:

1.  Lonboard uses [**GeoParquet**](https://geoparquet.org/) instead of GeoJSON
2.  Data is stored as **GeoArrow** in memory
3.  It uses deck.gl, a GPU powered renderer

By relying on GeoParquet and GeoArrow throughout the entire pipeline, lonboard avoids expensive (de)serialization costs every step of the way.

### GeoParquet

Lonboard eliminates the text serialization entirely by leaning on [GeoParquet](https://geoparquet.org/). GeoParquet is an extension to the [Parquet](https://parquet.apache.org/) file format, which is itself a compressed binary columnar format. Instead of sending a multi-megabyte string across the wire and then parsing it, lonboard sends the GeoParquet file as binary.

The workflow looks like this:

```
flowchart LR
A[GeoDataFrame] --> B[Write to GeoParquet]
B --> C[Send to the browser]
C --> D[Read with parquet-wasm]
D --> E[Pass directly to deck.gl]
```

According to the [lonboard docs](https://developmentseed.org/lonboard/latest/how-it-works/#how-is-it-so-fast):

> Saving GeoArrow to Parquet was 135x faster than saving a GeoDataFrame to GeoJSON. The resulting file was 26x smaller. And parsing the Parquet to Arrow on the frontend was 5.6x faster than `JSON.parse`.

The format yields speedups orders of magnitude faster than we could achieve with GeoJSON. This change makes it possible to ship datasets to the browser that were previously off the table.

Watch Kyle Barron's (author of lonboard) great talk on [Why GeoParquet Matters](https://www.youtube.com/watch?v=KWQMa-y8CNg).

## GeoArrow

The real benefit that we get from GeoParqeut comes from the usage of GeoArrow. GeoArrow is an extension of the [Apache Arrow](https://arrow.apache.org/) (Arrow for short) specification. Arrow specifies how data should be represented _in memory_. Arrow is a [_columnar_ storage format](https://arrow.apache.org/docs/format/Intro.html).

### Row vs. Column Storage

GeoJSON and even other more efficient formats like [FlatGeoBuf](https://flatgeobuf.org/) are not designed for analytical workflows. These file formats store each _row_ of data together. For example, to calculate the average of a single column, each row has to be read and every value except the one from the column of interest is discarded. Columnar formats store each variable together. Doing so make it so that only columns can be accessed without having to sift through every single row of a dataset.

![The Apache Arrow columnar memory layout](https://arrow.apache.org/docs/_images/columnar-diagram_1.svg)

Diagram from the [Apache Arrow columnar format specification](https://arrow.apache.org/docs/format/Intro.html)

Arrow is a _standard_ and not a library. The benefit of this is that Arrow data can be sent between tools / languages / runtimes without incurring a (de)serialization cost enabling “seamless” interoperability.

![Arrow data passed between languages without a serialization step](https://josiah.rs/posts/cross-language-ds/images/paste-8.png)

Diagram from [Apache Arrow, Rust, and cross-language data science](https://josiah.rs/posts/cross-language-ds)

GeoArrow builds upon the Arrow specification to create a standard for how spatial data shuold be represented in memory. Meaning any tool that can use GeoArrow can use the same data—i.e. GeoArrow from Python can be used by [`geoarrow-r`](https://github.com/geoarrow/geoarrow-r) or [`{nanoarrow}`](https://arrow.apache.org/nanoarrow/latest/r/).

### GeoArrow 🤝 GeoParquet

The [GeoParquet 1.1](https://geoparquet.org/releases/v1.1.0/) release added support for **native** GeoArrow encoding. When geometry is stored in native GeoArrow format inside a Parquet file, the browser can read the file and access the geometry directly without converting the data _to_ GeoArrow.

[deck.gl](http://deck.gl/), the tool used by lonboard, has an extension that supports the use of native GeoArrow. Thus, coupling GeoParquet and native GeoArrow enables transferring data from disk to GPU [without any intermediate format](https://github.com/geoarrow/deck.gl-geoarrow/tree/main#features).

### Parquet and the browser

The remaining piece is [parquet-wasm](https://github.com/kylebarron/parquet-wasm), which reads the GeoParquet file in the browser via WebAssembly. [WebAssembly](https://webassembly.org/) (Wasm) is an instruction format that lets you run code written in languages like Rust or C++ inside the browser at near-native speed. `parquet-wasm` uses WASM to read Parquet files directly in the browser, so the data never has to be converted out of its native format before reaching deck.gl.

## GPU rendering

The first hurdle is improving data transfer and storage. The second one is actually _where_ the rendering happens. Over the past few years, Graphics Processing Units (GPUs), have become incredibly powerful and, up until recently, affordable. Libraries like Leaflet render each and every polygon and point using the CPU. That works fine for a few hundred or even thousands of features. But the scale and efficiency that the GPU provides far exceeds what is possible from the CPU.

Lonboard renders it’s map via [deck.gl](https://deck.gl/), which runs on [WebGL](https://www.khronos.org/webgl/). WebGL, in overly simplistic terms, pushes web rendering to the GPU.

## DeckGL and GeoArrow

The piece that ties this all together is [`@geoarrow/deck.gl-geoarrow`](https://github.com/geoarrow/deck.gl-geoarrow). It is a small “glue library” that lets deck.gl consume GeoArrow data. `deck.gl-geoarrow` copies data from an [Arrow JS](https://arrow.apache.org/js/) [RecordBatch](https://arrow.apache.org/docs/python/data.html) directly to the GPU.

The “full” internal machinery can be simplified as:

1.  Convert to GeoParquet
2.  Send GeoParquet to the browser
3.  Use `parquet-wasm` to read into Arrow Table
4.  `deck.gl-geoarrow` accesses underlying arrays
5.  `deck.gl` renders in the browser

## Building the Shiny app

Since lonboard integrates with Jupyter Widgets via [anywidget](https://anywidget.dev/), it works in Shiny for Python through [shinywidgets](https://github.com/posit-dev/py-shinywidgets) with minimal glue code. The `@render_widget` decorator drops the map directly into the UI:

python

```python
@render_widget
def map():
    return Map(
        layers=[layer],
        view_state={
            "longitude": -122.2,
            "latitude": 47.5,
            "zoom": 9,
        },
        show_side_panel=False,
    )
```

### Efficient Filtering

Transferring data is where much of the bottleneck comes from. When adding interactivity, it’s critical that data only be sent _once_. Recreating the widget sends the full dataset from Python to the browser. Lonboard’s `DataFilterExtension` solves this by moving filtering to the GPU. It sets the filter range inside of the browser and, critically, does not re-render the widget with the subsetted data.

python

```python
@reactive.effect
def _update_filter():
    layer.filter_range = [
        [input.price_min(), input.price_max()],
        [BED_CHOICES[input.bedrooms()], BEDS_MAX],
        [BATH_CHOICES[input.bathrooms()], BATHS_MAX],
        [input.sqft_min(), input.sqft_max()],
    ]
```

This ensures that the map doesn’t re-render.

## Progress in the R-ecosystem

The [R Consortium](https://r-consortium.org/) funded [active work](https://r-consortium.org/all-projects/2025-group-2.html#modernizing-rs-web-mapping-capabilities) on new R packages that bring similar capabilities to the R ecosystem from [Tim Appelhans](https://github.com/tim-salabim).

[geoarrowWidget](https://github.com/r-spatial/geoarrowWidget) is an extension to [`{htmlwidgets}`](https://github.com/ramnathv/htmlwidgets) that simplifies transfering data from a file or URL to any other htmlwidget. The package also ensures that the requisite JavaScript dependencies are also available—including Arrow JS and parquet-wasm.

[deckglgeoarrow](https://github.com/r-spatial/deckglgeoarrow), on the otherhand, is an extension to `{mapgl}` that wraps `deck.gl-geoarrow` enabling conversions from common spatial formats such as `sf`, `wk`, `geos`, and terra, to GeoArrow.

## Deploy to ricochet

Install the CLI, add the server, and deploy:

bash

```bash
curl -fsSL https://raw.githubusercontent.com/ricochet-rs/cli/main/install.sh | sh
ricochet servers add try https://try.ricochet.rs
ricochet login -S try
ricochet deploy
```

## Further reading

-   [How lonboard works](https://developmentseed.org/lonboard/latest/how-it-works/#how-is-it-so-fast)
-   [Why GeoParquet (Kyle Barron talk)](https://www.youtube.com/watch?v=KWQMa-y8CNg)
-   [GeoParquet 1.1 spec](https://geoparquet.org/releases/v1.1.0/)
-   [parquet-wasm](https://github.com/kylebarron/parquet-wasm)
-   [geoarrowWidget for R](https://github.com/r-spatial/geoarrowWidget)
-   [Getting started with Apache Arrow](https://blog.djnavarro.net/posts/2021-11-19_starting-apache-arrow-in-r/#overview-of-arrow)
-   [Apache Arrow, Rust, and cross-language data science](https://josiah.rs/posts/cross-language-ds/)

---

Source: https://ricochet.rs/blog/high-performance-spatial
