deckgl-marimo 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/.gitignore +3 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/PKG-INFO +98 -17
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/README.md +95 -16
- deckgl_marimo-0.4.0/docs/binary-transfer-optimization-research.md +282 -0
- deckgl_marimo-0.4.0/examples/arc_binary_compare.py +341 -0
- deckgl_marimo-0.4.0/examples/column_binary_compare.py +355 -0
- deckgl_marimo-0.4.0/examples/generate_1m_polygons.py +77 -0
- deckgl_marimo-0.4.0/examples/line_binary_compare.py +330 -0
- deckgl_marimo-0.4.0/examples/path_binary_compare.py +374 -0
- deckgl_marimo-0.4.0/examples/pointcloud_binary_compare.py +339 -0
- deckgl_marimo-0.4.0/examples/polygon_1m_stress.py +227 -0
- deckgl_marimo-0.4.0/examples/polygon_binary_compare.py +390 -0
- deckgl_marimo-0.4.0/examples/polygon_perf.py +296 -0
- deckgl_marimo-0.4.0/examples/polygon_stress.py +261 -0
- deckgl_marimo-0.4.0/examples/scatterplot_binary_compare.py +344 -0
- deckgl_marimo-0.4.0/js/src/index.js +257 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/js/src/layer-factory.js +68 -3
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/pyproject.toml +2 -1
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/__init__.py +6 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_base.py +12 -1
- deckgl_marimo-0.4.0/src/deckgl_marimo/_binary.py +168 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_map.py +40 -0
- deckgl_marimo-0.4.0/src/deckgl_marimo/layers/_core.py +1005 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/static/deckgl-marimo.bundle.js +165 -165
- deckgl_marimo-0.4.0/tests/test_binary.py +320 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/uv.lock +7 -2
- deckgl_marimo-0.3.0/js/src/index.js +0 -148
- deckgl_marimo-0.3.0/src/deckgl_marimo/layers/_core.py +0 -582
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/.github/workflows/ci.yml +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/.github/workflows/publish.yml +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/01_scatterplot.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/02_hexagon.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/03_geojson.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/04_heatmap.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/05_arc.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/06_path.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/07_polygon.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/08_column.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/09_icon.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/10_text.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/11_comprehensive.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/displacement.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/ellipse.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/hexagon_example.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/js/esbuild.config.mjs +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/js/package-lock.json +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/js/package.json +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/js/src/accessor-resolver.js +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_basemaps.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_data.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_utils.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_view_state.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/controls/__init__.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/layers/__init__.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/layers/_aggregation.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/layers/_composite.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/layers/_geo.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/layers/_mesh.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/static/deckgl-marimo.bundle.css +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/widget.css +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/widget.js +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/widget.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_base_layer.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_data.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_layers/__init__.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_layers/test_aggregation.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_layers/test_composite.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_layers/test_core.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_map.py +0 -0
- {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_widget.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deckgl-marimo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: deck.gl visualization library for marimo notebooks — interactive maps with 33 layer types
|
|
5
5
|
Project-URL: Homepage, https://github.com/kihaji/deckgl-marimo
|
|
6
6
|
Project-URL: Documentation, https://kihaji.github.io/deckgl-marimo
|
|
@@ -26,6 +26,8 @@ Requires-Dist: marimo>=0.22
|
|
|
26
26
|
Requires-Dist: narwhals>=1.0.0
|
|
27
27
|
Requires-Dist: pandas>=2.3.3
|
|
28
28
|
Requires-Dist: traitlets>=5.0.0
|
|
29
|
+
Provides-Extra: binary
|
|
30
|
+
Requires-Dist: numpy; extra == 'binary'
|
|
29
31
|
Provides-Extra: dev
|
|
30
32
|
Requires-Dist: duckdb; extra == 'dev'
|
|
31
33
|
Requires-Dist: geopandas; extra == 'dev'
|
|
@@ -51,10 +53,12 @@ Interactive [deck.gl](https://deck.gl) visualization library for [marimo](https:
|
|
|
51
53
|
|
|
52
54
|
## Features
|
|
53
55
|
|
|
54
|
-
- **
|
|
56
|
+
- **12 deck.gl layer types** — scatter plots, hexagonal bins, heatmaps, arcs, paths, polygons, GeoJSON, 3D columns, lines, point clouds, and more
|
|
57
|
+
- **Binary data transfer** — bypass JSON serialization for large datasets (30x faster, 4x smaller payloads)
|
|
55
58
|
- **Multi-layer maps** — compose multiple layers on a single map
|
|
56
59
|
- **Standalone layers** — display any layer directly without explicit map setup
|
|
57
60
|
- **Marimo-native reactivity** — bind layer properties to sliders, dropdowns, and other widgets
|
|
61
|
+
- **Performance metrics** — built-in FPS counter and frame time tracking via `perf_metrics` traitlet
|
|
58
62
|
- **Multiple data sources** — pandas, polars, geopandas, DuckDB, GeoJSON dicts, and URLs
|
|
59
63
|
- **Authenticated data loading** — pass HTTP headers, API keys, or credentials for remote data sources
|
|
60
64
|
- **Fully offline** — all JavaScript bundled in the package, no CDN dependencies
|
|
@@ -162,6 +166,79 @@ rel = duckdb.sql("SELECT lon, lat, value FROM 'data.parquet' WHERE value > 100")
|
|
|
162
166
|
layer = dgl.ScatterplotLayer(data=rel, get_position=["lon", "lat"])
|
|
163
167
|
```
|
|
164
168
|
|
|
169
|
+
### Binary data for large datasets
|
|
170
|
+
|
|
171
|
+
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.
|
|
172
|
+
|
|
173
|
+
**Using `use_binary=True` (automatic packing from list-of-dicts):**
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
layer = dgl.ScatterplotLayer(
|
|
177
|
+
data=large_df.to_dict("records"),
|
|
178
|
+
get_position=["lon", "lat"],
|
|
179
|
+
get_fill_color="color",
|
|
180
|
+
get_radius="radius",
|
|
181
|
+
use_binary=True,
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Using pre-built numpy arrays (fastest — zero dict iteration):**
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
import numpy as np
|
|
189
|
+
from deckgl_marimo._binary import pack_binary
|
|
190
|
+
|
|
191
|
+
# Prepare arrays
|
|
192
|
+
positions = np.column_stack([lons, lats]).astype(np.float32)
|
|
193
|
+
colors = np.array(color_data, dtype=np.uint8) # (n, 4)
|
|
194
|
+
|
|
195
|
+
# Create layer spec (no data — binary provides it)
|
|
196
|
+
layer = dgl.ScatterplotLayer(
|
|
197
|
+
get_fill_color=[255, 140, 0], # constant props still go in spec
|
|
198
|
+
radius_min_pixels=2,
|
|
199
|
+
use_binary=True,
|
|
200
|
+
)
|
|
201
|
+
spec = layer.to_spec()
|
|
202
|
+
|
|
203
|
+
# Pack binary buffer
|
|
204
|
+
meta, buf = pack_binary(
|
|
205
|
+
n=len(positions),
|
|
206
|
+
attributes={
|
|
207
|
+
"getPosition": (positions, "float32", 2),
|
|
208
|
+
"getFillColor": (colors, "uint8", 4),
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
meta["id"] = spec["id"]
|
|
212
|
+
|
|
213
|
+
# Send to map
|
|
214
|
+
map_widget.binary_metadata = {"layers": [meta]}
|
|
215
|
+
map_widget.binary_data = buf
|
|
216
|
+
map_widget.layer_specs = [spec]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Performance at 200k polygons:**
|
|
220
|
+
|
|
221
|
+
| Mode | Serialization | Payload | Speedup |
|
|
222
|
+
|------|--------------|---------|---------|
|
|
223
|
+
| JSON | 1,167 ms | 62 MB | — |
|
|
224
|
+
| Binary | 39 ms | 14.5 MB | 30x faster, 4.3x smaller |
|
|
225
|
+
|
|
226
|
+
Binary data is supported on: `ScatterplotLayer`, `PolygonLayer`, `PathLayer`, `ArcLayer`, `LineLayer`, `ColumnLayer`, and `PointCloudLayer`.
|
|
227
|
+
|
|
228
|
+
### Performance metrics
|
|
229
|
+
|
|
230
|
+
The `Map` widget includes a built-in FPS counter that reports metrics back to Python:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
map_widget = dgl.Map(basemap="dark-matter", center=(0, 0), zoom=1)
|
|
234
|
+
widget = mo.ui.anywidget(map_widget)
|
|
235
|
+
|
|
236
|
+
# Read performance metrics (updated every 500ms)
|
|
237
|
+
perf = widget.value.get("perf_metrics", {})
|
|
238
|
+
fps = perf.get("fps") # frames per second
|
|
239
|
+
frame_time = perf.get("frameTimeAvg") # ms per frame
|
|
240
|
+
```
|
|
241
|
+
|
|
165
242
|
### Authenticated remote data
|
|
166
243
|
|
|
167
244
|
Any layer that loads data from a URL supports custom HTTP headers via
|
|
@@ -200,28 +277,32 @@ headers take precedence.
|
|
|
200
277
|
|
|
201
278
|
## Available layers
|
|
202
279
|
|
|
203
|
-
### Fully tested (
|
|
280
|
+
### Fully tested (12)
|
|
281
|
+
|
|
282
|
+
| Layer | Use case | Binary support |
|
|
283
|
+
|-------|----------|----------------|
|
|
284
|
+
| `ScatterplotLayer` | Point data | Yes |
|
|
285
|
+
| `GeoJsonLayer` | Polygons, lines, points from GeoJSON/GeoDataFrame | — |
|
|
286
|
+
| `ArcLayer` | Origin-destination flows | Yes |
|
|
287
|
+
| `PathLayer` | Routes, trajectories | Yes |
|
|
288
|
+
| `PolygonLayer` | Filled regions | Yes |
|
|
289
|
+
| `IconLayer` | Marker icons | — |
|
|
290
|
+
| `TextLayer` | Labels | — |
|
|
291
|
+
| `ColumnLayer` | 3D bars on map | Yes |
|
|
292
|
+
| `HexagonLayer` | Hexagonal binning | — |
|
|
293
|
+
| `HeatmapLayer` | Density visualization | — |
|
|
294
|
+
| `LineLayer` | Straight lines between point pairs | Yes |
|
|
295
|
+
| `PointCloudLayer` | 3D point clouds (LiDAR, etc.) | Yes |
|
|
204
296
|
|
|
205
|
-
|
|
206
|
-
|-------|----------|
|
|
207
|
-
| `ScatterplotLayer` | Point data |
|
|
208
|
-
| `GeoJsonLayer` | Polygons, lines, points from GeoJSON/GeoDataFrame |
|
|
209
|
-
| `ArcLayer` | Origin-destination flows |
|
|
210
|
-
| `PathLayer` | Routes, trajectories |
|
|
211
|
-
| `PolygonLayer` | Filled regions |
|
|
212
|
-
| `IconLayer` | Marker icons |
|
|
213
|
-
| `TextLayer` | Labels |
|
|
214
|
-
| `ColumnLayer` | 3D bars on map |
|
|
215
|
-
| `HexagonLayer` | Hexagonal binning |
|
|
216
|
-
| `HeatmapLayer` | Density visualization |
|
|
297
|
+
`LineLayer` and `PointCloudLayer` are newly exported experimental layers.
|
|
217
298
|
|
|
218
|
-
### Experimental (
|
|
299
|
+
### Experimental (21+)
|
|
219
300
|
|
|
220
301
|
All additional deck.gl layers are available as experimental stubs via `deckgl_marimo.layers`:
|
|
221
302
|
|
|
222
303
|
```python
|
|
223
304
|
from deckgl_marimo.layers import TripsLayer, MVTLayer, H3HexagonLayer, ContourLayer
|
|
224
|
-
# ... and
|
|
305
|
+
# ... and more
|
|
225
306
|
```
|
|
226
307
|
|
|
227
308
|
## Basemaps
|
|
@@ -10,10 +10,12 @@ Interactive [deck.gl](https://deck.gl) visualization library for [marimo](https:
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **
|
|
13
|
+
- **12 deck.gl layer types** — scatter plots, hexagonal bins, heatmaps, arcs, paths, polygons, GeoJSON, 3D columns, lines, point clouds, and more
|
|
14
|
+
- **Binary data transfer** — bypass JSON serialization for large datasets (30x faster, 4x smaller payloads)
|
|
14
15
|
- **Multi-layer maps** — compose multiple layers on a single map
|
|
15
16
|
- **Standalone layers** — display any layer directly without explicit map setup
|
|
16
17
|
- **Marimo-native reactivity** — bind layer properties to sliders, dropdowns, and other widgets
|
|
18
|
+
- **Performance metrics** — built-in FPS counter and frame time tracking via `perf_metrics` traitlet
|
|
17
19
|
- **Multiple data sources** — pandas, polars, geopandas, DuckDB, GeoJSON dicts, and URLs
|
|
18
20
|
- **Authenticated data loading** — pass HTTP headers, API keys, or credentials for remote data sources
|
|
19
21
|
- **Fully offline** — all JavaScript bundled in the package, no CDN dependencies
|
|
@@ -121,6 +123,79 @@ rel = duckdb.sql("SELECT lon, lat, value FROM 'data.parquet' WHERE value > 100")
|
|
|
121
123
|
layer = dgl.ScatterplotLayer(data=rel, get_position=["lon", "lat"])
|
|
122
124
|
```
|
|
123
125
|
|
|
126
|
+
### Binary data for large datasets
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
130
|
+
**Using `use_binary=True` (automatic packing from list-of-dicts):**
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
layer = dgl.ScatterplotLayer(
|
|
134
|
+
data=large_df.to_dict("records"),
|
|
135
|
+
get_position=["lon", "lat"],
|
|
136
|
+
get_fill_color="color",
|
|
137
|
+
get_radius="radius",
|
|
138
|
+
use_binary=True,
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Using pre-built numpy arrays (fastest — zero dict iteration):**
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import numpy as np
|
|
146
|
+
from deckgl_marimo._binary import pack_binary
|
|
147
|
+
|
|
148
|
+
# Prepare arrays
|
|
149
|
+
positions = np.column_stack([lons, lats]).astype(np.float32)
|
|
150
|
+
colors = np.array(color_data, dtype=np.uint8) # (n, 4)
|
|
151
|
+
|
|
152
|
+
# Create layer spec (no data — binary provides it)
|
|
153
|
+
layer = dgl.ScatterplotLayer(
|
|
154
|
+
get_fill_color=[255, 140, 0], # constant props still go in spec
|
|
155
|
+
radius_min_pixels=2,
|
|
156
|
+
use_binary=True,
|
|
157
|
+
)
|
|
158
|
+
spec = layer.to_spec()
|
|
159
|
+
|
|
160
|
+
# Pack binary buffer
|
|
161
|
+
meta, buf = pack_binary(
|
|
162
|
+
n=len(positions),
|
|
163
|
+
attributes={
|
|
164
|
+
"getPosition": (positions, "float32", 2),
|
|
165
|
+
"getFillColor": (colors, "uint8", 4),
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
meta["id"] = spec["id"]
|
|
169
|
+
|
|
170
|
+
# Send to map
|
|
171
|
+
map_widget.binary_metadata = {"layers": [meta]}
|
|
172
|
+
map_widget.binary_data = buf
|
|
173
|
+
map_widget.layer_specs = [spec]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Performance at 200k polygons:**
|
|
177
|
+
|
|
178
|
+
| Mode | Serialization | Payload | Speedup |
|
|
179
|
+
|------|--------------|---------|---------|
|
|
180
|
+
| JSON | 1,167 ms | 62 MB | — |
|
|
181
|
+
| Binary | 39 ms | 14.5 MB | 30x faster, 4.3x smaller |
|
|
182
|
+
|
|
183
|
+
Binary data is supported on: `ScatterplotLayer`, `PolygonLayer`, `PathLayer`, `ArcLayer`, `LineLayer`, `ColumnLayer`, and `PointCloudLayer`.
|
|
184
|
+
|
|
185
|
+
### Performance metrics
|
|
186
|
+
|
|
187
|
+
The `Map` widget includes a built-in FPS counter that reports metrics back to Python:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
map_widget = dgl.Map(basemap="dark-matter", center=(0, 0), zoom=1)
|
|
191
|
+
widget = mo.ui.anywidget(map_widget)
|
|
192
|
+
|
|
193
|
+
# Read performance metrics (updated every 500ms)
|
|
194
|
+
perf = widget.value.get("perf_metrics", {})
|
|
195
|
+
fps = perf.get("fps") # frames per second
|
|
196
|
+
frame_time = perf.get("frameTimeAvg") # ms per frame
|
|
197
|
+
```
|
|
198
|
+
|
|
124
199
|
### Authenticated remote data
|
|
125
200
|
|
|
126
201
|
Any layer that loads data from a URL supports custom HTTP headers via
|
|
@@ -159,28 +234,32 @@ headers take precedence.
|
|
|
159
234
|
|
|
160
235
|
## Available layers
|
|
161
236
|
|
|
162
|
-
### Fully tested (
|
|
237
|
+
### Fully tested (12)
|
|
238
|
+
|
|
239
|
+
| Layer | Use case | Binary support |
|
|
240
|
+
|-------|----------|----------------|
|
|
241
|
+
| `ScatterplotLayer` | Point data | Yes |
|
|
242
|
+
| `GeoJsonLayer` | Polygons, lines, points from GeoJSON/GeoDataFrame | — |
|
|
243
|
+
| `ArcLayer` | Origin-destination flows | Yes |
|
|
244
|
+
| `PathLayer` | Routes, trajectories | Yes |
|
|
245
|
+
| `PolygonLayer` | Filled regions | Yes |
|
|
246
|
+
| `IconLayer` | Marker icons | — |
|
|
247
|
+
| `TextLayer` | Labels | — |
|
|
248
|
+
| `ColumnLayer` | 3D bars on map | Yes |
|
|
249
|
+
| `HexagonLayer` | Hexagonal binning | — |
|
|
250
|
+
| `HeatmapLayer` | Density visualization | — |
|
|
251
|
+
| `LineLayer` | Straight lines between point pairs | Yes |
|
|
252
|
+
| `PointCloudLayer` | 3D point clouds (LiDAR, etc.) | Yes |
|
|
163
253
|
|
|
164
|
-
|
|
165
|
-
|-------|----------|
|
|
166
|
-
| `ScatterplotLayer` | Point data |
|
|
167
|
-
| `GeoJsonLayer` | Polygons, lines, points from GeoJSON/GeoDataFrame |
|
|
168
|
-
| `ArcLayer` | Origin-destination flows |
|
|
169
|
-
| `PathLayer` | Routes, trajectories |
|
|
170
|
-
| `PolygonLayer` | Filled regions |
|
|
171
|
-
| `IconLayer` | Marker icons |
|
|
172
|
-
| `TextLayer` | Labels |
|
|
173
|
-
| `ColumnLayer` | 3D bars on map |
|
|
174
|
-
| `HexagonLayer` | Hexagonal binning |
|
|
175
|
-
| `HeatmapLayer` | Density visualization |
|
|
254
|
+
`LineLayer` and `PointCloudLayer` are newly exported experimental layers.
|
|
176
255
|
|
|
177
|
-
### Experimental (
|
|
256
|
+
### Experimental (21+)
|
|
178
257
|
|
|
179
258
|
All additional deck.gl layers are available as experimental stubs via `deckgl_marimo.layers`:
|
|
180
259
|
|
|
181
260
|
```python
|
|
182
261
|
from deckgl_marimo.layers import TripsLayer, MVTLayer, H3HexagonLayer, ContourLayer
|
|
183
|
-
# ... and
|
|
262
|
+
# ... and more
|
|
184
263
|
```
|
|
185
264
|
|
|
186
265
|
## Basemaps
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Binary Data Transfer Optimization Research
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
When rendering 1M polygons via the binary data path in deckgl-marimo, the Python-side packing is fast (~223ms) and GPU rendering is smooth (120 FPS), but there's a noticeable UI hitch during initial load. The bottleneck is transferring a 72.5MB binary payload from Python to the browser through anywidget's traitlet sync mechanism.
|
|
6
|
+
|
|
7
|
+
### Current Pipeline
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
numpy arrays (.tobytes()) → traitlets.Bytes → anywidget sync → WebSocket → JS DataView → ArrayBuffer copy → typed array views → deck.gl GPU upload
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Measured Performance (1M polygons)
|
|
14
|
+
|
|
15
|
+
| Step | Time |
|
|
16
|
+
|------|------|
|
|
17
|
+
| File load (.npz) | 200 ms |
|
|
18
|
+
| Binary packing (.tobytes()) | 223 ms |
|
|
19
|
+
| **Transfer + JS processing** | **~500-1000ms (the hitch)** |
|
|
20
|
+
| GPU rendering | 8.33 ms/frame (120 FPS) |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## How anywidget Transfers Binary Data
|
|
25
|
+
|
|
26
|
+
- `traitlets.Bytes` is the correct type — anywidget automatically uses the Jupyter widget binary buffer protocol
|
|
27
|
+
- Binary data bypasses JSON serialization entirely; Python `bytes` arrive as JS `DataView`
|
|
28
|
+
- No base64 encoding — this is already the optimal traitlet type
|
|
29
|
+
- The `DataView` may share an underlying `ArrayBuffer` with a non-zero `byteOffset` (anywidget implementation detail), requiring a copy to a standalone buffer before creating typed array views
|
|
30
|
+
|
|
31
|
+
### Relevant Code (current)
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
// js/src/index.js — handling DataView from anywidget
|
|
35
|
+
if (binaryData instanceof DataView) {
|
|
36
|
+
const src = new Uint8Array(binaryData.buffer, binaryData.byteOffset, binaryData.byteLength);
|
|
37
|
+
const copy = new ArrayBuffer(binaryData.byteLength);
|
|
38
|
+
new Uint8Array(copy).set(src); // ~80-150ms for 72MB
|
|
39
|
+
buffer = copy;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## How marimo Handles Widget Communication
|
|
46
|
+
|
|
47
|
+
- All Python ↔ JS communication goes over a single WebSocket connection
|
|
48
|
+
- No special handling for large binary payloads — the entire 72MB is sent as one WebSocket message
|
|
49
|
+
- marimo hooks anywidgets into its reactive execution engine, syncing traitlet changes
|
|
50
|
+
- Known issue: [marimo#2506](https://github.com/marimo-team/marimo/issues/2506) — binary data from file reads sometimes fails to transmit correctly (numpy array bytes work fine)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Root Cause of the UI Hitch
|
|
55
|
+
|
|
56
|
+
The hitch is caused by several blocking operations on the browser's main thread:
|
|
57
|
+
|
|
58
|
+
1. **WebSocket message processing** — browser receives and buffers the entire 72MB frame before exposing it to JS
|
|
59
|
+
2. **ArrayBuffer copy** — copying from shared DataView buffer to standalone ArrayBuffer (~80-150ms)
|
|
60
|
+
3. **Typed array construction** — creating Float32Array/Uint32Array views (~50ms)
|
|
61
|
+
4. **deck.gl layer initialization** — GPU buffer upload for 6M vertices (~100-300ms)
|
|
62
|
+
|
|
63
|
+
All of this happens synchronously on the main thread. The browser cannot paint, respond to input, or animate during this time.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Optimization Options
|
|
68
|
+
|
|
69
|
+
### 1. Chunked Transfer (Highest Priority)
|
|
70
|
+
|
|
71
|
+
**Problem:** One 72MB message blocks the browser event loop.
|
|
72
|
+
|
|
73
|
+
**Solution:** Split the binary payload into ~5MB chunks, sending each as a separate traitlet update. The browser can process UI events between chunks.
|
|
74
|
+
|
|
75
|
+
**Python side:**
|
|
76
|
+
```python
|
|
77
|
+
CHUNK_SIZE = 5 * 1024 * 1024 # 5MB
|
|
78
|
+
|
|
79
|
+
def send_chunked(widget, metadata, buffer):
|
|
80
|
+
n_chunks = math.ceil(len(buffer) / CHUNK_SIZE)
|
|
81
|
+
widget.binary_metadata = {
|
|
82
|
+
**metadata,
|
|
83
|
+
"chunked": True,
|
|
84
|
+
"totalSize": len(buffer),
|
|
85
|
+
"chunkCount": n_chunks,
|
|
86
|
+
}
|
|
87
|
+
for i in range(n_chunks):
|
|
88
|
+
start = i * CHUNK_SIZE
|
|
89
|
+
chunk = buffer[start:start + CHUNK_SIZE]
|
|
90
|
+
widget.binary_chunk_index = i
|
|
91
|
+
widget.binary_data = chunk
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**JS side:**
|
|
95
|
+
```javascript
|
|
96
|
+
let chunks = [];
|
|
97
|
+
let expectedChunks = 0;
|
|
98
|
+
|
|
99
|
+
model.on("change:binary_data", () => {
|
|
100
|
+
const meta = model.get("binary_metadata");
|
|
101
|
+
if (!meta.chunked) {
|
|
102
|
+
// Non-chunked path (existing code)
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const chunkIndex = model.get("binary_chunk_index");
|
|
107
|
+
const data = model.get("binary_data");
|
|
108
|
+
chunks[chunkIndex] = data;
|
|
109
|
+
expectedChunks = meta.chunkCount;
|
|
110
|
+
|
|
111
|
+
if (chunks.filter(Boolean).length === expectedChunks) {
|
|
112
|
+
// Reassemble
|
|
113
|
+
const totalSize = meta.totalSize;
|
|
114
|
+
const buffer = new ArrayBuffer(totalSize);
|
|
115
|
+
const view = new Uint8Array(buffer);
|
|
116
|
+
let offset = 0;
|
|
117
|
+
for (const chunk of chunks) {
|
|
118
|
+
const src = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
119
|
+
view.set(src, offset);
|
|
120
|
+
offset += src.byteLength;
|
|
121
|
+
}
|
|
122
|
+
chunks = [];
|
|
123
|
+
// Process complete buffer
|
|
124
|
+
applyBinaryData(specs, buffer, meta);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Expected impact:** Eliminates UI freeze. Instead of a single ~1s hitch, the load spreads across ~15 chunks with the browser responsive between each.
|
|
130
|
+
|
|
131
|
+
**Trade-offs:**
|
|
132
|
+
- Slightly more total transfer time due to traitlet sync overhead per chunk
|
|
133
|
+
- More complex state management on JS side
|
|
134
|
+
- Need to handle partial state (some chunks arrived, not all)
|
|
135
|
+
|
|
136
|
+
**Where to implement:** Changes needed in anywidget/marimo, or in deckgl-marimo's Map widget and JS entry point.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### 2. Compression (Medium Priority)
|
|
141
|
+
|
|
142
|
+
**Problem:** 72MB over WebSocket is slow regardless of chunking.
|
|
143
|
+
|
|
144
|
+
**Solution:** Compress the binary payload before sending. Polygon coordinate data has good compressibility (~3-5x with zstd).
|
|
145
|
+
|
|
146
|
+
**Python side:**
|
|
147
|
+
```python
|
|
148
|
+
import zstandard as zstd
|
|
149
|
+
|
|
150
|
+
cctx = zstd.ZstdCompressor(level=3) # fast compression
|
|
151
|
+
compressed = cctx.compress(buffer) # 72MB → ~15-20MB
|
|
152
|
+
widget.binary_metadata = {**metadata, "compressed": "zstd", "originalSize": len(buffer)}
|
|
153
|
+
widget.binary_data = compressed
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**JS side (using fzstd — lightweight, no WASM):**
|
|
157
|
+
```javascript
|
|
158
|
+
import { decompress } from 'fzstd';
|
|
159
|
+
|
|
160
|
+
if (meta.compressed === "zstd") {
|
|
161
|
+
const decompressed = decompress(new Uint8Array(buffer));
|
|
162
|
+
buffer = decompressed.buffer;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Expected impact:**
|
|
167
|
+
- Wire transfer: 72MB → ~15-20MB (3.5-5x reduction)
|
|
168
|
+
- Compression: ~50-100ms on Python side
|
|
169
|
+
- Decompression: ~50-100ms on JS side
|
|
170
|
+
- Net win if transfer time savings > compression + decompression overhead
|
|
171
|
+
|
|
172
|
+
**Trade-offs:**
|
|
173
|
+
- Additional dependency: `zstandard` (Python), `fzstd` (JS)
|
|
174
|
+
- CPU cost on both sides
|
|
175
|
+
- Combines well with chunking (compress, then chunk)
|
|
176
|
+
|
|
177
|
+
**Where to implement:** deckgl-marimo's `_binary.py` and JS entry point. No changes needed in anywidget/marimo.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### 3. HTTP Static File Serving (Alternative for Pre-generated Data)
|
|
182
|
+
|
|
183
|
+
**Problem:** The WebSocket comm channel isn't designed for bulk data transfer.
|
|
184
|
+
|
|
185
|
+
**Solution:** Instead of sending binary data through the traitlet, write it to a temporary file and serve it via marimo's HTTP server. JS fetches it directly.
|
|
186
|
+
|
|
187
|
+
**Python side:**
|
|
188
|
+
```python
|
|
189
|
+
import tempfile
|
|
190
|
+
|
|
191
|
+
# Write binary to temp file
|
|
192
|
+
tmp = tempfile.NamedTemporaryFile(suffix=".bin", delete=False)
|
|
193
|
+
tmp.write(buffer)
|
|
194
|
+
tmp.close()
|
|
195
|
+
|
|
196
|
+
# marimo can serve files from its working directory
|
|
197
|
+
# or use a custom endpoint
|
|
198
|
+
widget.binary_metadata = {
|
|
199
|
+
**metadata,
|
|
200
|
+
"method": "fetch",
|
|
201
|
+
"url": f"/files/{tmp.name}", # depends on marimo's file serving
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**JS side:**
|
|
206
|
+
```javascript
|
|
207
|
+
if (meta.method === "fetch") {
|
|
208
|
+
const response = await fetch(meta.url);
|
|
209
|
+
const buffer = await response.arrayBuffer();
|
|
210
|
+
applyBinaryData(specs, buffer, meta);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Expected impact:**
|
|
215
|
+
- HTTP supports streaming, range requests, and browser caching
|
|
216
|
+
- No WebSocket congestion — other widget updates continue flowing
|
|
217
|
+
- Browser can show download progress
|
|
218
|
+
- Best for large static/semi-static datasets
|
|
219
|
+
|
|
220
|
+
**Trade-offs:**
|
|
221
|
+
- Only works for data that doesn't change reactively (or changes infrequently)
|
|
222
|
+
- Requires understanding marimo's file serving mechanism
|
|
223
|
+
- Temp file cleanup needed
|
|
224
|
+
- May not work in all deployment scenarios (e.g., remote marimo sessions)
|
|
225
|
+
|
|
226
|
+
**Where to investigate:** marimo's static file serving API, how `mo.download()` works internally.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### 4. SharedArrayBuffer (Not Recommended)
|
|
231
|
+
|
|
232
|
+
- Requires cross-origin isolation headers (`Cross-Origin-Opener-Policy`, `Cross-Origin-Embedder-Policy`)
|
|
233
|
+
- marimo would need to set these headers on its HTTP server
|
|
234
|
+
- Complex synchronization between Python/JS
|
|
235
|
+
- Not worth the complexity for this use case
|
|
236
|
+
|
|
237
|
+
### 5. Web Workers for Processing (Supplementary)
|
|
238
|
+
|
|
239
|
+
**Problem:** Even after receiving data, typed array construction and deck.gl initialization block the main thread.
|
|
240
|
+
|
|
241
|
+
**Solution:** Process the binary buffer in a Web Worker, then transfer the resulting typed arrays back via `postMessage` with transferable objects (zero-copy).
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
// worker.js
|
|
245
|
+
self.onmessage = ({ data: { buffer, metadata } }) => {
|
|
246
|
+
const startIndices = new Uint32Array(buffer, meta.startIndices.offset, ...);
|
|
247
|
+
const coords = new Float32Array(buffer, meta.getPolygon.offset, ...);
|
|
248
|
+
const colors = new Uint8Array(buffer, meta.getFillColor.offset, ...);
|
|
249
|
+
|
|
250
|
+
// Transfer ownership back to main thread (zero-copy)
|
|
251
|
+
self.postMessage(
|
|
252
|
+
{ startIndices, coords, colors },
|
|
253
|
+
[startIndices.buffer, coords.buffer, colors.buffer]
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Expected impact:** Moves ~100-200ms of typed array construction off the main thread. The GPU upload still happens on main thread (WebGL requirement).
|
|
259
|
+
|
|
260
|
+
**Where to implement:** deckgl-marimo's JS bundle. No changes needed in anywidget/marimo.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Recommended Priority
|
|
265
|
+
|
|
266
|
+
| # | Optimization | Impact | Effort | Dependencies |
|
|
267
|
+
|---|-------------|--------|--------|-------------|
|
|
268
|
+
| 1 | **Chunking** | High — eliminates UI freeze | Medium | None (pure deckgl-marimo change) |
|
|
269
|
+
| 2 | **Compression** | Medium — reduces transfer time | Low | zstandard (Python), fzstd (JS) |
|
|
270
|
+
| 3 | **HTTP serving** | High for static data | Medium | Investigate marimo file serving |
|
|
271
|
+
| 4 | **Web Worker** | Low-medium — offloads CPU work | Medium | None |
|
|
272
|
+
|
|
273
|
+
Chunking + compression together would reduce a 72MB single-message hitch to ~15 progressive 1MB chunks — effectively invisible loading.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Questions to Research Further
|
|
278
|
+
|
|
279
|
+
1. **marimo file serving:** Does marimo expose a way to serve arbitrary files to the frontend? How does `mo.download()` work internally? Could we piggyback on that for binary data?
|
|
280
|
+
2. **anywidget binary buffer protocol:** Is there a way to use `send()` (one-shot message) instead of trait sync for binary data? The Jupyter protocol distinguishes between state sync and custom messages.
|
|
281
|
+
3. **WebSocket compression:** Does marimo enable `permessage-deflate` on its WebSocket? If so, our binary data may already be compressed in transit (though less efficiently than zstd).
|
|
282
|
+
4. **marimo's reactivity and traitlet batching:** When we set `binary_metadata` and `binary_data` in sequence, does marimo batch these into one WebSocket message or send two? If two, the order matters (metadata must arrive before or with data).
|