Metadata-Version: 2.4
Name: deckgl-marimo
Version: 0.7.0
Summary: deck.gl visualization library for marimo notebooks — 15 fully tested layer types on MapLibre basemaps, plus experimental access to the full deck.gl catalog
Project-URL: Homepage, https://github.com/kihaji/deckgl-marimo
Project-URL: Documentation, https://kihaji.github.io/deckgl-marimo
Project-URL: Repository, https://github.com/kihaji/deckgl-marimo
Project-URL: Issues, https://github.com/kihaji/deckgl-marimo/issues
Author-email: Scott Lemke <scott.r.lemke@gmail.com>
License-Expression: MIT
Keywords: anywidget,deck.gl,geospatial,maplibre,maps,marimo,visualization
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Jupyter
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.11
Requires-Dist: anywidget>=0.10.0
Requires-Dist: narwhals>=2.0.0
Requires-Dist: traitlets>=5.14
Provides-Extra: binary
Requires-Dist: numpy>=1.24; extra == 'binary'
Provides-Extra: dev
Requires-Dist: duckdb; extra == 'dev'
Requires-Dist: geopandas; extra == 'dev'
Requires-Dist: httptools>=0.7.1; extra == 'dev'
Requires-Dist: marimo>=0.22; extra == 'dev'
Requires-Dist: numpy>=1.24; extra == 'dev'
Requires-Dist: pandas; extra == 'dev'
Requires-Dist: polars; extra == 'dev'
Requires-Dist: pytest-cov>=7.1; extra == 'dev'
Requires-Dist: pytest>=9; extra == 'dev'
Requires-Dist: ruff>=0.15; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material; extra == 'docs'
Requires-Dist: mkdocstrings[python]; extra == 'docs'
Provides-Extra: marimo
Requires-Dist: marimo>=0.22; extra == 'marimo'
Description-Content-Type: text/markdown

# deckgl-marimo

[![PyPI](https://img.shields.io/pypi/v/deckgl-marimo)](https://pypi.org/project/deckgl-marimo/)
[![Python](https://img.shields.io/pypi/pyversions/deckgl-marimo)](https://pypi.org/project/deckgl-marimo/)
[![CI](https://github.com/kihaji/deckgl-marimo/actions/workflows/ci.yml/badge.svg)](https://github.com/kihaji/deckgl-marimo/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Open in molab](https://marimo.io/molab-shield.svg)](https://molab.marimo.io/github/kihaji/deckgl-marimo/blob/main/examples/hexagon_example.py)

Interactive [deck.gl](https://deck.gl) visualization library for [marimo](https://marimo.io) notebooks. Render GPU-accelerated maps with 15 fully tested layer types (plus experimental access to the full deck.gl catalog), powered by [MapLibre GL](https://maplibre.org) and [anywidget](https://anywidget.dev).

## Features

- **15 fully tested layer types** — scatter plots, hexagonal bins, heatmaps, arcs, paths, polygons, GeoJSON, 3D columns, lines, point clouds, and more — plus 17 experimental layers covering the rest of the deck.gl catalog
- **Binary data transfer** — bypass JSON serialization for large datasets (30x faster, 4x smaller payloads)
- **Multi-layer maps** — compose multiple layers on a single map
- **Standalone layers** — display any layer directly without explicit map setup
- **Marimo-native reactivity** — bind layer properties to sliders, dropdowns, and other widgets
- **Performance metrics** — built-in FPS counter and frame time tracking via `perf_metrics` traitlet
- **Color scales** — map numeric columns to color palettes (viridis, plasma, etc.) with linear or log scaling
- **Callable accessors** — pass Python functions to any `get_*` accessor for custom per-row logic
- **Multiple data sources** — pandas, polars, geopandas, DuckDB, GeoJSON dicts, and URLs
- **Authenticated data loading** — pass HTTP headers, API keys, or credentials for remote data sources
- **Fully offline** — all JavaScript bundled in the package, no CDN dependencies
- **Viewport readback** — read the current map center, zoom, pitch, and bearing from Python
- **Click & hover events** — inspect picked objects reactively in downstream cells

## Installation

```bash
pip install deckgl-marimo
# or
uv add deckgl-marimo
```

## Quickstart

### Standalone layer

```python
import marimo as mo
import deckgl_marimo as dgl

layer = dgl.ScatterplotLayer(
    data=df,
    get_position=["longitude", "latitude"],
    get_fill_color=[255, 140, 0],
    get_radius="population",
    radius_scale=10,
)

# Displays a map with one layer
widget = mo.ui.anywidget(layer)
```

### Multi-layer map

```python
m = dgl.Map(
    layers=[
        dgl.ScatterplotLayer(
            data=cities_df,
            get_position=["lon", "lat"],
            get_fill_color=[255, 140, 0],
            get_radius=5,
            radius_min_pixels=3,
        ),
        dgl.ArcLayer(
            data=flights_df,
            get_source_position=["src_lon", "src_lat"],
            get_target_position=["dst_lon", "dst_lat"],
            get_source_color=[0, 128, 255],
            get_target_color=[255, 0, 128],
        ),
    ],
    basemap="dark-matter",
    center=(-98.5, 39.8),
    zoom=4,
    pitch=45,
)

widget = mo.ui.anywidget(m)
```

### Reactive controls

> **Important:** Create the `Map` widget in a cell that does **not** depend on
> slider values. Update layers by assigning to `map_widget.layer_specs` in a
> separate cell. This keeps the map instance stable — sliders update layers
> via traitlet sync instead of recreating the entire map, which would cause
> tile reloads and a black screen flash.

```python
# Cell 1 — sliders
radius = mo.ui.slider(200, 5000, value=1000, label="Radius")

# Cell 2 — create map widget (NO slider deps — stable, never re-executes)
map_widget = dgl.Map(basemap="dark-matter", center=(-1.4, 52.2), zoom=6, pitch=40)
widget = mo.ui.anywidget(map_widget)

# Cell 3 — display
widget

# Cell 4 — update layers reactively (re-executes when slider changes)
map_widget.set_layers([
    dgl.HexagonLayer(
        data=df,
        get_position=["lon", "lat"],
        radius=radius.value,
        extruded=True,
        elevation_scale=250,
    )
])

# Cell 5 — viewport readback
vp = widget.value.get("viewport", {})
mo.md(f"Zoom: {vp.get('zoom', 'N/A'):.1f}")
```

### Color scales

Map a numeric column to interpolated colors using `ColorScale`. Supports named palettes, custom color ramps, and linear or logarithmic scaling.

```python
# Named palette
layer = dgl.ScatterplotLayer(
    data=df,
    get_position=["lon", "lat"],
    get_fill_color=dgl.ColorScale("temperature", palette="viridis"),
)

# Two-color ramp with log scale
layer = dgl.ScatterplotLayer(
    data=df,
    get_position=["lon", "lat"],
    get_fill_color=dgl.ColorScale("population", colors=["blue", "red"], scale="log"),
)
```

**Available palettes:** `viridis`, `plasma`, `inferno`, `magma`, `cividis`, `coolwarm`, `RdBu`, `spectral`, `turbo`

**ColorScale parameters:**

| Parameter | Default | Description |
|-----------|---------|-------------|
| `column` | *(required)* | Numeric column name to map |
| `palette` | — | Named palette (mutually exclusive with `colors`) |
| `colors` | — | List of 2+ colors: names (`"blue"`), hex (`"#FF0000"`), or RGB (`[255, 0, 0]`) |
| `domain` | auto | `(min, max)` value range; auto-detected from data if omitted |
| `scale` | `"linear"` | `"linear"` or `"log"` |
| `alpha` | `255` | Alpha channel (0–255) for all output colors |

### Callable accessors

Pass a Python function to any `get_*` accessor for full control over per-row values:

```python
layer = dgl.ScatterplotLayer(
    data=df,
    get_position=["lon", "lat"],
    get_fill_color=lambda row: [
        int(row["temperature"] * 2.55),
        50,
        255 - int(row["temperature"] * 2.55),
        200,
    ],
)
```

Both `ColorScale` and callable accessors work with the binary data path (`use_binary=True`) — colors are resolved in Python and packed into the binary buffer automatically.

### DuckDB integration

```python
import duckdb

rel = duckdb.sql("SELECT lon, lat, value FROM 'data.parquet' WHERE value > 100")
layer = dgl.ScatterplotLayer(data=rel, get_position=["lon", "lat"])
```

### Binary data for large datasets

For datasets with 100k+ rows, binary data transfer bypasses JSON serialization entirely, sending typed arrays directly to the GPU via deck.gl's native binary format.

**Using `use_binary=True` (automatic packing from list-of-dicts):**

```python
layer = dgl.ScatterplotLayer(
    data=large_df.to_dict("records"),
    get_position=["lon", "lat"],
    get_fill_color="color",
    get_radius="radius",
    use_binary=True,
)
```

**Using pre-built numpy arrays (fastest — zero dict iteration):**

```python
import numpy as np
from deckgl_marimo import pack_binary

# Prepare arrays
positions = np.column_stack([lons, lats]).astype(np.float32)
colors = np.array(color_data, dtype=np.uint8)  # (n, 4)

# Create layer spec (no data — binary provides it)
layer = dgl.ScatterplotLayer(
    get_fill_color=[255, 140, 0],  # constant props still go in spec
    radius_min_pixels=2,
    use_binary=True,
)
spec = layer.to_spec()

# Pack binary buffer
meta, buf = pack_binary(
    n=len(positions),
    attributes={
        "getPosition": (positions, "float32", 2),
        "getFillColor": (colors, "uint8", 4),
    },
)
meta["id"] = spec["id"]

# Send to map
map_widget.binary_metadata = {"layers": [meta]}
map_widget.binary_data = buf
map_widget.layer_specs = [spec]
```

**Performance at 200k polygons:**

| Mode | Serialization | Payload | Speedup |
|------|--------------|---------|---------|
| JSON | 1,167 ms | 62 MB | — |
| Binary | 39 ms | 14.5 MB | 30x faster, 4.3x smaller |

Binary data is supported on: `ScatterplotLayer`, `PolygonLayer`, `PathLayer`, `ArcLayer`, `LineLayer`, `ColumnLayer`, and `PointCloudLayer`.

### Performance metrics

The `Map` widget includes an opt-in FPS counter that reports metrics back
to Python. It is off by default (the tracker pushes updates every 500 ms,
which is wasted traffic for an idle map):

```python
map_widget = dgl.Map(basemap="dark-matter", center=(0, 0), zoom=1, show_perf_metrics=True)
widget = mo.ui.anywidget(map_widget)

# Read performance metrics (updated every 500ms while enabled)
perf = widget.value.get("perf_metrics", {})
fps = perf.get("fps")           # frames per second
frame_time = perf.get("frameTimeAvg")  # ms per frame

# Toggle at runtime
map_widget.show_perf_metrics = False
```

### Authenticated remote data

Any layer that loads data from a URL supports custom HTTP headers via
`fetch_headers`, or full control over the fetch request via `load_options`.

```python
# Bearer token
layer = dgl.GeoJsonLayer(
    data="https://secure-api.example.com/data.geojson",
    fetch_headers={"Authorization": "Bearer my-token"},
    get_fill_color=[0, 180, 230, 160],
)

# API key
layer = dgl.GeoJsonLayer(
    data="https://api.example.com/features",
    fetch_headers={"X-API-Key": "abc123"},
)

# Full fetch control (mTLS / CORS / custom options)
layer = dgl.GeoJsonLayer(
    data="https://internal.example.com/data.geojson",
    load_options={
        "fetch": {
            "credentials": "include",
            "mode": "cors",
            "headers": {"Authorization": "Bearer token"},
        }
    },
)
```

Both parameters are available on all layer types via `BaseLayer`. When
`fetch_headers` and `load_options` both specify headers, the `load_options`
headers take precedence.

## Available layers

### Fully tested (15)

| Layer | Use case | Binary support |
|-------|----------|----------------|
| `ScatterplotLayer` | Point data | Yes |
| `GeoJsonLayer` | Polygons, lines, points from GeoJSON/GeoDataFrame | — |
| `ArcLayer` | Origin-destination flows | Yes |
| `PathLayer` | Routes, trajectories | Yes |
| `PolygonLayer` | Filled regions | Yes |
| `SolidPolygonLayer` | Filled regions without stroke (fastest polygons) | Yes |
| `IconLayer` | Marker icons | — |
| `TextLayer` | Labels | — |
| `ColumnLayer` | 3D bars on map | Yes |
| `HexagonLayer` | Hexagonal binning | — |
| `HeatmapLayer` | Density visualization | — |
| `LineLayer` | Straight lines between point pairs | Yes |
| `PointCloudLayer` | 3D point clouds (LiDAR, etc.) | Yes |
| `DisplacementLayer` | Origin vs reported-position arcs + dots (composite) | — |
| `EllipseLayer` | Ellipses from center/axes/orientation (composite) | — |

### Experimental (17)

All additional deck.gl layers are available as experimental stubs via `deckgl_marimo.layers` (they emit a warning on construction and may not be fully tested):

```python
from deckgl_marimo.layers import TripsLayer, MVTLayer, H3HexagonLayer, ContourLayer
# ... and more
```

## Basemaps

```python
dgl.Basemaps.list_available()
# ['bright', 'dark', 'dark-matter', 'liberty', 'light', 'none', 'osm', 'positron', 'voyager']

# Use any MapLibre-compatible style URL
dgl.Map(basemap="https://my-tileserver.example.com/style.json")
```

## Troubleshooting

### `Content-Length` errors in the marimo console

Fixed in marimo >= 0.22.0 — update marimo.

### Map flashes black / camera resets when a slider moves

The `Map` widget is being recreated on every reactive run. Create the
`Map` in a cell with **no** slider dependencies and update layers from a
separate cell with `map_widget.set_layers([...])` — see
[Reactive controls](#reactive-controls) above. Assigning
`map_widget.layer_specs` directly also works for JSON layers, but skips
binary re-packing; prefer `set_layers`.

### Blank map / WebGL errors on WSL or headless environments

deck.gl requires WebGL2. In WSL, make sure your browser has hardware
acceleration enabled (check `chrome://gpu` or https://webglreport.com).
For headless Chromium testing, launch with
`--use-gl=angle --use-angle=swiftshader --enable-unsafe-swiftshader`.


## License

MIT
