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.
- deckgl_marimo-0.1.0/.github/workflows/ci.yml +47 -0
- deckgl_marimo-0.1.0/.github/workflows/publish.yml +46 -0
- deckgl_marimo-0.1.0/.gitignore +37 -0
- deckgl_marimo-0.1.0/PKG-INFO +35 -0
- deckgl_marimo-0.1.0/README.md +3 -0
- deckgl_marimo-0.1.0/examples/__marimo__/session/hexagon_example.py.json +99 -0
- deckgl_marimo-0.1.0/examples/hexagon_example.py +113 -0
- deckgl_marimo-0.1.0/pyproject.toml +46 -0
- deckgl_marimo-0.1.0/src/deckgl_marimo/__init__.py +5 -0
- deckgl_marimo-0.1.0/src/deckgl_marimo/_data.py +37 -0
- deckgl_marimo-0.1.0/src/deckgl_marimo/widget.css +21 -0
- deckgl_marimo-0.1.0/src/deckgl_marimo/widget.js +119 -0
- deckgl_marimo-0.1.0/src/deckgl_marimo/widget.py +94 -0
- deckgl_marimo-0.1.0/tests/test_data.py +31 -0
- deckgl_marimo-0.1.0/tests/test_widget.py +40 -0
- deckgl_marimo-0.1.0/uv.lock +1311 -0
|
@@ -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,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='"<span class=\"markdown prose dark:prose-invert contents\"><span class=\"paragraph\">Radius</span></span>"' data-start='200' data-stop='5000' data-step='100' data-steps='[]' data-debounce='false' data-disabled='false' data-orientation='"horizontal"' 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='"<span class=\"markdown prose dark:prose-invert contents\"><span class=\"paragraph\">Coverage</span></span>"' data-start='0.1' data-stop='1.0' data-step='0.1' data-steps='[]' data-debounce='false' data-disabled='false' data-orientation='"horizontal"' 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='"<span class=\"markdown prose dark:prose-invert contents\"><span class=\"paragraph\">Elevation Scale</span></span>"' data-start='10' data-stop='500' data-step='10' data-steps='[]' data-debounce='false' data-disabled='false' data-orientation='"horizontal"' 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='{"model_id":"dc5e9da0e699447a942a8a94268ce515"}' data-label='null' data-js-url='"./@file/3198-157436-phLLaEna.js"' data-js-hash='"32edf553288349a3f7e95f75f1ed45ae"'></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,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 };
|