deckgl-marimo 0.1.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.
@@ -0,0 +1,47 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v5
20
+
21
+ - name: Set up Python ${{ matrix.python-version }}
22
+ run: uv python install ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: uv sync --extra dev
26
+
27
+ - name: Run tests
28
+ run: uv run pytest
29
+
30
+ build:
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+
35
+ - name: Install uv
36
+ uses: astral-sh/setup-uv@v5
37
+
38
+ - name: Set up Python
39
+ run: uv python install 3.12
40
+
41
+ - name: Build package
42
+ run: uv build
43
+
44
+ - name: Check dist contents
45
+ run: |
46
+ ls -la dist/
47
+ uv run --with twine twine check dist/*
@@ -0,0 +1,46 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+ id-token: write # Required for trusted publishing
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v5
19
+
20
+ - name: Set up Python
21
+ run: uv python install 3.12
22
+
23
+ - name: Build package
24
+ run: uv build
25
+
26
+ - name: Upload dist artifacts
27
+ uses: actions/upload-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+
32
+ publish-pypi:
33
+ needs: build
34
+ runs-on: ubuntu-latest
35
+ environment: pypi
36
+ permissions:
37
+ id-token: write # Trusted publishing via OIDC
38
+ steps:
39
+ - name: Download dist artifacts
40
+ uses: actions/download-artifact@v4
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+
45
+ - name: Publish to PyPI
46
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,37 @@
1
+ .agents/
2
+ .claude/
3
+
4
+ # Byte-compiled / optimized
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+
9
+ # Distribution / packaging
10
+ dist/
11
+ build/
12
+ *.egg-info/
13
+ *.egg
14
+
15
+ # Virtual environments
16
+ .venv/
17
+ venv/
18
+
19
+
20
+ # IDE
21
+ .idea/
22
+ .vscode/
23
+ *.swp
24
+ *.swo
25
+ *~
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Testing
32
+ .pytest_cache/
33
+ .coverage
34
+ htmlcov/
35
+
36
+ # mypy / type checking
37
+ .mypy_cache/
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: deckgl-marimo
3
+ Version: 0.1.0
4
+ Summary: deck.gl HexagonLayer widget for marimo notebooks via anywidget
5
+ Project-URL: Homepage, https://github.com/kihaji/deckgl-marimo
6
+ Project-URL: Repository, https://github.com/kihaji/deckgl-marimo
7
+ Project-URL: Issues, https://github.com/kihaji/deckgl-marimo/issues
8
+ Author-email: Scott Lemke <scott.r.lemke@gmail.com>
9
+ License-Expression: MIT
10
+ Keywords: anywidget,deck.gl,geospatial,hexagon,maplibre,marimo,visualization
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Framework :: Jupyter
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: GIS
21
+ Classifier: Topic :: Scientific/Engineering :: Visualization
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: anywidget>=0.9.0
24
+ Requires-Dist: narwhals>=1.0.0
25
+ Requires-Dist: traitlets>=5.0.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: marimo; extra == 'dev'
28
+ Requires-Dist: pandas; extra == 'dev'
29
+ Requires-Dist: polars; extra == 'dev'
30
+ Requires-Dist: pytest; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # deckgl-marimo
34
+
35
+ deck.gl HexagonLayer widget for marimo notebooks via anywidget.
@@ -0,0 +1,3 @@
1
+ # deckgl-marimo
2
+
3
+ deck.gl HexagonLayer widget for marimo notebooks via anywidget.
@@ -0,0 +1,99 @@
1
+ {
2
+ "version": "1",
3
+ "metadata": {
4
+ "marimo_version": "0.19.11"
5
+ },
6
+ "cells": [
7
+ {
8
+ "id": "Hbol",
9
+ "code_hash": "1d0db38904205bec4d6f6f6a1f6cec3e",
10
+ "outputs": [
11
+ {
12
+ "type": "data",
13
+ "data": {
14
+ "text/plain": ""
15
+ }
16
+ }
17
+ ],
18
+ "console": []
19
+ },
20
+ {
21
+ "id": "MJUe",
22
+ "code_hash": "c1b7b1d18b5ff786fb8c4d2ef4e0138a",
23
+ "outputs": [
24
+ {
25
+ "type": "data",
26
+ "data": {
27
+ "text/plain": ""
28
+ }
29
+ }
30
+ ],
31
+ "console": []
32
+ },
33
+ {
34
+ "id": "vblA",
35
+ "code_hash": "e0da53828dff4c092a11a51b5f00f0eb",
36
+ "outputs": [
37
+ {
38
+ "type": "data",
39
+ "data": {
40
+ "text/plain": ""
41
+ }
42
+ }
43
+ ],
44
+ "console": []
45
+ },
46
+ {
47
+ "id": "bkHC",
48
+ "code_hash": "fa94d27d952855a6772472b4e8f6ecdc",
49
+ "outputs": [
50
+ {
51
+ "type": "data",
52
+ "data": {
53
+ "text/markdown": "<span class=\"markdown prose dark:prose-invert contents\"><span class=\"paragraph\">Loaded <strong>140,056</strong> records</span></span>"
54
+ }
55
+ }
56
+ ],
57
+ "console": []
58
+ },
59
+ {
60
+ "id": "lEQa",
61
+ "code_hash": "c7575307c006c2ed5553f0d9d215674f",
62
+ "outputs": [
63
+ {
64
+ "type": "data",
65
+ "data": {
66
+ "text/html": "<div style='display: flex;flex: 1;flex-direction: row;justify-content: flex-start;align-items: normal;flex-wrap: nowrap;gap: 2rem'><marimo-ui-element object-id='lEQa-0' random-id='5c1e4105-6c46-16c3-9473-cf14df809327'><marimo-slider data-initial-value='1000' data-label='&quot;&lt;span class=&#92;&quot;markdown prose dark:prose-invert contents&#92;&quot;&gt;&lt;span class=&#92;&quot;paragraph&#92;&quot;&gt;Radius&lt;/span&gt;&lt;/span&gt;&quot;' data-start='200' data-stop='5000' data-step='100' data-steps='[]' data-debounce='false' data-disabled='false' data-orientation='&quot;horizontal&quot;' data-show-value='true' data-include-input='false' data-full-width='false'></marimo-slider></marimo-ui-element><marimo-ui-element object-id='lEQa-1' random-id='6d7fcd3d-8cac-9611-d358-f00c40344150'><marimo-slider data-initial-value='1.0' data-label='&quot;&lt;span class=&#92;&quot;markdown prose dark:prose-invert contents&#92;&quot;&gt;&lt;span class=&#92;&quot;paragraph&#92;&quot;&gt;Coverage&lt;/span&gt;&lt;/span&gt;&quot;' data-start='0.1' data-stop='1.0' data-step='0.1' data-steps='[]' data-debounce='false' data-disabled='false' data-orientation='&quot;horizontal&quot;' data-show-value='true' data-include-input='false' data-full-width='false'></marimo-slider></marimo-ui-element><marimo-ui-element object-id='lEQa-2' random-id='65e4f2d5-335c-1da8-a9c5-b56c49b77c55'><marimo-slider data-initial-value='250' data-label='&quot;&lt;span class=&#92;&quot;markdown prose dark:prose-invert contents&#92;&quot;&gt;&lt;span class=&#92;&quot;paragraph&#92;&quot;&gt;Elevation Scale&lt;/span&gt;&lt;/span&gt;&quot;' data-start='10' data-stop='500' data-step='10' data-steps='[]' data-debounce='false' data-disabled='false' data-orientation='&quot;horizontal&quot;' data-show-value='true' data-include-input='false' data-full-width='false'></marimo-slider></marimo-ui-element></div>"
67
+ }
68
+ }
69
+ ],
70
+ "console": []
71
+ },
72
+ {
73
+ "id": "PKri",
74
+ "code_hash": "b437fd62320eb4d780bf1edf1df7357c",
75
+ "outputs": [
76
+ {
77
+ "type": "data",
78
+ "data": {
79
+ "text/html": "<marimo-ui-element object-id='PKri-0' random-id='51d61d28-36af-a34d-f5b6-6c287efed962'><marimo-anywidget data-initial-value='{&quot;model_id&quot;:&quot;dc5e9da0e699447a942a8a94268ce515&quot;}' data-label='null' data-js-url='&quot;./@file/3198-157436-phLLaEna.js&quot;' data-js-hash='&quot;32edf553288349a3f7e95f75f1ed45ae&quot;'></marimo-anywidget></marimo-ui-element>"
80
+ }
81
+ }
82
+ ],
83
+ "console": []
84
+ },
85
+ {
86
+ "id": "Xref",
87
+ "code_hash": "eb722657ace6aadab388436b0f2ae915",
88
+ "outputs": [
89
+ {
90
+ "type": "data",
91
+ "data": {
92
+ "text/markdown": "<span class=\"markdown prose dark:prose-invert contents\"><span class=\"paragraph\"><strong>Viewport</strong></span>\n<table>\n<thead>\n<tr>\n<th>Property</th>\n<th>Value</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Longitude</td>\n<td>-1.8872</td>\n</tr>\n<tr>\n<td>Latitude</td>\n<td>53.7638</td>\n</tr>\n<tr>\n<td>Zoom</td>\n<td>6.00</td>\n</tr>\n<tr>\n<td>Pitch</td>\n<td>6.0</td>\n</tr>\n<tr>\n<td>Bearing</td>\n<td>-41.6</td>\n</tr>\n</tbody>\n</table></span>"
93
+ }
94
+ }
95
+ ],
96
+ "console": []
97
+ }
98
+ ]
99
+ }
@@ -0,0 +1,113 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = [
4
+ # "marimo",
5
+ # "pandas",
6
+ # "deckgl-marimo",
7
+ # ]
8
+ #
9
+ # [tool.uv.sources]
10
+ # deckgl-marimo = { path = ".." }
11
+ # ///
12
+
13
+ import marimo
14
+
15
+ __generated_with = "0.19.11"
16
+ app = marimo.App(width="full")
17
+
18
+
19
+ @app.cell
20
+ def _():
21
+ import marimo as mo
22
+
23
+ return (mo,)
24
+
25
+
26
+ @app.cell
27
+ def _():
28
+ import pandas as pd
29
+
30
+ return (pd,)
31
+
32
+
33
+ @app.cell
34
+ def _():
35
+ from deckgl_marimo import DeckGLHexagonWidget
36
+
37
+ return (DeckGLHexagonWidget,)
38
+
39
+
40
+ @app.cell
41
+ def _(mo, pd):
42
+ URL = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv"
43
+ df = pd.read_csv(URL)
44
+ mo.md(f"Loaded **{len(df):,}** records")
45
+ return (df,)
46
+
47
+
48
+ @app.cell
49
+ def _(mo):
50
+ radius_slider = mo.ui.slider(
51
+ start=200, stop=5000, step=100, value=1000, show_value=True, label="Radius"
52
+ )
53
+ coverage_slider = mo.ui.slider(
54
+ start=0.1, stop=1.0, step=0.1, value=1.0, show_value=True, label="Coverage"
55
+ )
56
+ elevation_scale_slider = mo.ui.slider(
57
+ start=10, stop=500, step=10, value=250, show_value=True, label="Elevation Scale"
58
+ )
59
+
60
+ mo.hstack(
61
+ [radius_slider, coverage_slider, elevation_scale_slider],
62
+ justify="start",
63
+ gap=2,
64
+ )
65
+ return coverage_slider, elevation_scale_slider, radius_slider
66
+
67
+
68
+ @app.cell
69
+ def _(DeckGLHexagonWidget, coverage_slider, df, elevation_scale_slider, mo, radius_slider):
70
+ widget = mo.ui.anywidget(
71
+ DeckGLHexagonWidget(
72
+ style_url="https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
73
+ data=df,
74
+ lat_col="lat",
75
+ lon_col="lng",
76
+ center_lon=-1.4157,
77
+ center_lat=52.2324,
78
+ zoom=6.0,
79
+ pitch=40.5,
80
+ radius=radius_slider.value,
81
+ coverage=coverage_slider.value,
82
+ elevation_scale=elevation_scale_slider.value,
83
+ )
84
+ )
85
+ widget
86
+ return (widget,)
87
+
88
+
89
+ @app.cell
90
+ def _(mo, widget):
91
+ viewport = widget.value.get("viewport", {})
92
+
93
+ def _fmt(val, spec):
94
+ return format(val, spec) if isinstance(val, (int, float)) else "—"
95
+
96
+ mo.md(
97
+ f"""
98
+ **Viewport**
99
+
100
+ | Property | Value |
101
+ |----------|-------|
102
+ | Longitude | {_fmt(viewport.get('longitude'), '.4f')} |
103
+ | Latitude | {_fmt(viewport.get('latitude'), '.4f')} |
104
+ | Zoom | {_fmt(viewport.get('zoom'), '.2f')} |
105
+ | Pitch | {_fmt(viewport.get('pitch'), '.1f')} |
106
+ | Bearing | {_fmt(viewport.get('bearing'), '.1f')} |
107
+ """
108
+ )
109
+ return (viewport,)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ app.run()
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "deckgl-marimo"
7
+ version = "0.1.0"
8
+ description = "deck.gl HexagonLayer widget for marimo notebooks via anywidget"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Scott Lemke", email = "scott.r.lemke@gmail.com" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Framework :: Jupyter",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Scientific/Engineering :: GIS",
26
+ "Topic :: Scientific/Engineering :: Visualization",
27
+ ]
28
+ keywords = ["deck.gl", "maplibre", "marimo", "anywidget", "hexagon", "geospatial", "visualization"]
29
+ dependencies = [
30
+ "anywidget>=0.9.0",
31
+ "traitlets>=5.0.0",
32
+ "narwhals>=1.0.0",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/kihaji/deckgl-marimo"
37
+ Repository = "https://github.com/kihaji/deckgl-marimo"
38
+ Issues = "https://github.com/kihaji/deckgl-marimo/issues"
39
+
40
+ [project.optional-dependencies]
41
+ dev = [
42
+ "marimo",
43
+ "pandas",
44
+ "polars",
45
+ "pytest",
46
+ ]
@@ -0,0 +1,5 @@
1
+ """deckgl-marimo: deck.gl HexagonLayer widget for marimo notebooks."""
2
+
3
+ from deckgl_marimo.widget import DeckGLHexagonWidget
4
+
5
+ __all__ = ["DeckGLHexagonWidget"]
@@ -0,0 +1,37 @@
1
+ """DataFrame to positions conversion using narwhals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import narwhals as nw
8
+
9
+
10
+ def dataframe_to_positions(
11
+ data: Any,
12
+ lat_col: str = "lat",
13
+ lon_col: str = "lon",
14
+ ) -> list[list[float]]:
15
+ """Convert a DataFrame or list of dicts to [[lon, lat], ...] pairs.
16
+
17
+ Parameters
18
+ ----------
19
+ data
20
+ A pandas DataFrame, polars DataFrame, or list of dicts.
21
+ lat_col
22
+ Name of the latitude column.
23
+ lon_col
24
+ Name of the longitude column.
25
+
26
+ Returns
27
+ -------
28
+ list[list[float]]
29
+ Coordinate pairs as [[lon, lat], ...] for deck.gl.
30
+ """
31
+ if isinstance(data, list):
32
+ return [[row[lon_col], row[lat_col]] for row in data]
33
+
34
+ df = nw.from_native(data)
35
+ lons = df[lon_col].to_list()
36
+ lats = df[lat_col].to_list()
37
+ return [[lon, lat] for lon, lat in zip(lons, lats)]
@@ -0,0 +1,21 @@
1
+ @import url("https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css");
2
+
3
+ .deckgl-marimo-container {
4
+ border-radius: 8px;
5
+ overflow: hidden;
6
+ border: 1px solid #e0e0e0;
7
+ }
8
+
9
+ @media (prefers-color-scheme: dark) {
10
+ .deckgl-marimo-container {
11
+ border-color: #444;
12
+ }
13
+
14
+ .deckgl-marimo-container .maplibregl-ctrl-group {
15
+ background-color: #2d2d2d;
16
+ }
17
+
18
+ .deckgl-marimo-container .maplibregl-ctrl-group button {
19
+ filter: invert(1);
20
+ }
21
+ }
@@ -0,0 +1,119 @@
1
+ import maplibregl from "https://esm.sh/maplibre-gl@4.7.1";
2
+
3
+ // --- deck.gl via standalone bundle (avoids ESM packaging issues) ---
4
+ const DECKGL_VERSION = "9.1.8";
5
+ const deckReady = (() => {
6
+ if (window.deck) return Promise.resolve(window.deck);
7
+ return new Promise((resolve, reject) => {
8
+ const existing = document.querySelector(
9
+ 'script[src*="deck.gl/dist.min.js"]'
10
+ );
11
+ if (existing) {
12
+ existing.addEventListener("load", () => resolve(window.deck));
13
+ return;
14
+ }
15
+ const script = document.createElement("script");
16
+ script.src = `https://unpkg.com/deck.gl@${DECKGL_VERSION}/dist.min.js`;
17
+ script.onload = () => resolve(window.deck);
18
+ script.onerror = () => reject(new Error("Failed to load deck.gl"));
19
+ document.head.appendChild(script);
20
+ });
21
+ })();
22
+
23
+ // Traitlet names that map to HexagonLayer props
24
+ const LAYER_PROPS = [
25
+ "radius",
26
+ "elevation_scale",
27
+ "color_range",
28
+ "extruded",
29
+ "coverage",
30
+ "upper_percentile",
31
+ "pickable",
32
+ ];
33
+
34
+ function getLayerProps(model) {
35
+ return {
36
+ radius: model.get("radius"),
37
+ elevationScale: model.get("elevation_scale"),
38
+ colorRange: model.get("color_range"),
39
+ extruded: model.get("extruded"),
40
+ coverage: model.get("coverage"),
41
+ upperPercentile: model.get("upper_percentile"),
42
+ pickable: model.get("pickable"),
43
+ };
44
+ }
45
+
46
+ function buildLayer(deck, model) {
47
+ const positions = model.get("positions") || [];
48
+ return new deck.HexagonLayer({
49
+ id: "hexagon-layer",
50
+ data: positions,
51
+ getPosition: (d) => d,
52
+ ...getLayerProps(model),
53
+ });
54
+ }
55
+
56
+ async function render({ model, el }) {
57
+ const deck = await deckReady;
58
+
59
+ // Container
60
+ const container = document.createElement("div");
61
+ container.style.width = "100%";
62
+ container.style.height = model.get("map_height");
63
+ container.classList.add("deckgl-marimo-container");
64
+ el.appendChild(container);
65
+
66
+ // MapLibre map
67
+ const map = new maplibregl.Map({
68
+ container,
69
+ style: model.get("style_url"),
70
+ center: [model.get("center_lon"), model.get("center_lat")],
71
+ zoom: model.get("zoom"),
72
+ pitch: model.get("pitch"),
73
+ bearing: model.get("bearing"),
74
+ antialias: true,
75
+ });
76
+
77
+ map.addControl(new maplibregl.NavigationControl(), "top-right");
78
+
79
+ // deck.gl overlay
80
+ const overlay = new deck.MapboxOverlay({
81
+ layers: [buildLayer(deck, model)],
82
+ });
83
+ map.addControl(overlay);
84
+
85
+ // React to traitlet changes — rebuild layer
86
+ const layerTraitlets = ["positions", ...LAYER_PROPS];
87
+ for (const name of layerTraitlets) {
88
+ model.on(`change:${name}`, () => {
89
+ overlay.setProps({ layers: [buildLayer(deck, model)] });
90
+ });
91
+ }
92
+
93
+ // React to map_height changes
94
+ model.on("change:map_height", () => {
95
+ container.style.height = model.get("map_height");
96
+ map.resize();
97
+ });
98
+
99
+ // Write viewport back to Python on moveend
100
+ map.on("moveend", () => {
101
+ const center = map.getCenter();
102
+ model.set("viewport", {
103
+ longitude: center.lng,
104
+ latitude: center.lat,
105
+ zoom: map.getZoom(),
106
+ pitch: map.getPitch(),
107
+ bearing: map.getBearing(),
108
+ });
109
+ model.save_changes();
110
+ });
111
+
112
+ // Cleanup
113
+ return () => {
114
+ overlay.finalize();
115
+ map.remove();
116
+ };
117
+ }
118
+
119
+ export default { render };