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.
Files changed (70) hide show
  1. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/.gitignore +3 -0
  2. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/PKG-INFO +98 -17
  3. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/README.md +95 -16
  4. deckgl_marimo-0.4.0/docs/binary-transfer-optimization-research.md +282 -0
  5. deckgl_marimo-0.4.0/examples/arc_binary_compare.py +341 -0
  6. deckgl_marimo-0.4.0/examples/column_binary_compare.py +355 -0
  7. deckgl_marimo-0.4.0/examples/generate_1m_polygons.py +77 -0
  8. deckgl_marimo-0.4.0/examples/line_binary_compare.py +330 -0
  9. deckgl_marimo-0.4.0/examples/path_binary_compare.py +374 -0
  10. deckgl_marimo-0.4.0/examples/pointcloud_binary_compare.py +339 -0
  11. deckgl_marimo-0.4.0/examples/polygon_1m_stress.py +227 -0
  12. deckgl_marimo-0.4.0/examples/polygon_binary_compare.py +390 -0
  13. deckgl_marimo-0.4.0/examples/polygon_perf.py +296 -0
  14. deckgl_marimo-0.4.0/examples/polygon_stress.py +261 -0
  15. deckgl_marimo-0.4.0/examples/scatterplot_binary_compare.py +344 -0
  16. deckgl_marimo-0.4.0/js/src/index.js +257 -0
  17. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/js/src/layer-factory.js +68 -3
  18. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/pyproject.toml +2 -1
  19. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/__init__.py +6 -0
  20. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_base.py +12 -1
  21. deckgl_marimo-0.4.0/src/deckgl_marimo/_binary.py +168 -0
  22. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_map.py +40 -0
  23. deckgl_marimo-0.4.0/src/deckgl_marimo/layers/_core.py +1005 -0
  24. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/static/deckgl-marimo.bundle.js +165 -165
  25. deckgl_marimo-0.4.0/tests/test_binary.py +320 -0
  26. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/uv.lock +7 -2
  27. deckgl_marimo-0.3.0/js/src/index.js +0 -148
  28. deckgl_marimo-0.3.0/src/deckgl_marimo/layers/_core.py +0 -582
  29. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/.github/workflows/ci.yml +0 -0
  30. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/.github/workflows/publish.yml +0 -0
  31. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/01_scatterplot.py +0 -0
  32. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/02_hexagon.py +0 -0
  33. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/03_geojson.py +0 -0
  34. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/04_heatmap.py +0 -0
  35. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/05_arc.py +0 -0
  36. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/06_path.py +0 -0
  37. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/07_polygon.py +0 -0
  38. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/08_column.py +0 -0
  39. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/09_icon.py +0 -0
  40. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/10_text.py +0 -0
  41. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/11_comprehensive.py +0 -0
  42. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/displacement.py +0 -0
  43. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/ellipse.py +0 -0
  44. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/examples/hexagon_example.py +0 -0
  45. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/js/esbuild.config.mjs +0 -0
  46. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/js/package-lock.json +0 -0
  47. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/js/package.json +0 -0
  48. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/js/src/accessor-resolver.js +0 -0
  49. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_basemaps.py +0 -0
  50. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_data.py +0 -0
  51. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_utils.py +0 -0
  52. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/_view_state.py +0 -0
  53. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/controls/__init__.py +0 -0
  54. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/layers/__init__.py +0 -0
  55. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/layers/_aggregation.py +0 -0
  56. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/layers/_composite.py +0 -0
  57. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/layers/_geo.py +0 -0
  58. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/layers/_mesh.py +0 -0
  59. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/static/deckgl-marimo.bundle.css +0 -0
  60. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/widget.css +0 -0
  61. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/widget.js +0 -0
  62. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/src/deckgl_marimo/widget.py +0 -0
  63. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_base_layer.py +0 -0
  64. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_data.py +0 -0
  65. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_layers/__init__.py +0 -0
  66. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_layers/test_aggregation.py +0 -0
  67. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_layers/test_composite.py +0 -0
  68. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_layers/test_core.py +0 -0
  69. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_map.py +0 -0
  70. {deckgl_marimo-0.3.0 → deckgl_marimo-0.4.0}/tests/test_widget.py +0 -0
@@ -40,3 +40,6 @@ htmlcov/
40
40
 
41
41
  # mypy / type checking
42
42
  .mypy_cache/
43
+
44
+ # Large test data files
45
+ examples/data/*.npz
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deckgl-marimo
3
- Version: 0.3.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
- - **10 deck.gl layer types** — scatter plots, hexagonal bins, heatmaps, arcs, paths, polygons, GeoJSON, 3D columns, and more
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 (10)
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
- | Layer | Use case |
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 (23)
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 19 more
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
- - **10 deck.gl layer types** — scatter plots, hexagonal bins, heatmaps, arcs, paths, polygons, GeoJSON, 3D columns, and more
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 (10)
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
- | Layer | Use case |
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 (23)
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 19 more
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).