Metadata-Version: 2.4
Name: shiny_deckgl
Version: 1.12.0
Summary: Shiny for Python -> deck.gl bridge (no Java), with packaged assets and conda recipe
Author-email: Arturas Baziukas <arturas.baziukas@ku.lt>
License: MIT License
        
        Copyright (c) 2026 Arturas Baziukas
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/razinka/shiny_deckgl
Project-URL: Repository, https://github.com/razinka/shiny_deckgl
Project-URL: Issues, https://github.com/razinka/shiny_deckgl/issues
Project-URL: Changelog, https://github.com/razinka/shiny_deckgl/blob/main/CHANGELOG.md
Keywords: shiny,deck.gl,maplibre,webgl,visualization
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: JavaScript
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: shiny>=1.6.3
Requires-Dist: htmltools
Provides-Extra: binary
Requires-Dist: numpy; extra == "binary"
Provides-Extra: geopandas
Requires-Dist: geopandas; extra == "geopandas"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-asyncio; extra == "test"
Requires-Dist: pytest-benchmark; extra == "test"
Requires-Dist: hypothesis; extra == "test"
Requires-Dist: numpy; extra == "test"
Requires-Dist: pandas; extra == "test"
Requires-Dist: pillow; extra == "test"
Requires-Dist: shiny>=1.8.0; extra == "test"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: pytest-benchmark; extra == "dev"
Requires-Dist: hypothesis; extra == "dev"
Requires-Dist: numpy; extra == "dev"
Requires-Dist: pandas; extra == "dev"
Requires-Dist: pillow; extra == "dev"
Requires-Dist: geopandas; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: shiny>=1.8.0; extra == "dev"
Dynamic: license-file

# shiny_deckgl

## Shiny for Python → deck.gl bridge (Java-free)

A lightweight library for integrating [deck.gl](https://deck.gl/) (v9.4.0) and
[MapLibre GL JS](https://maplibre.org/) (v6.11.2) into
[Shiny for Python](https://shiny.posit.co/py/) applications.
Built for marine science and GIS visualisation — WMS layers, EMODnet,
HELCOM, food web modelling — the package handles CDN asset injection, layer
construction, and bi-directional messaging between the Python server and the
browser, all without Java dependencies.

## Features

### Core (v0.1)

| Capability | Details |
| --- | --- |
| **Reusable `MapWidget`** | Drop-in widget with configurable initial view state, auto-render on connect, and `update()` to push new layers at any time. |
| **Layer helpers** | `scatterplot_layer()`, `geojson_layer()`, `tile_layer()`, `bitmap_layer()` — typed factory functions with sensible defaults and `**kwargs` pass-through. |
| **Generic `layer()`** | Any deck.gl layer class via `layer("HeatmapLayer", ...)`; the JS client resolves `deck[type]` dynamically. |
| **WMS / TileLayer** | First-class WMS support via `tile_layer()` — render EMODnet, GEBCO, or any `{bbox-epsg-3857}` service. |
| **Click & hover events** | Read click/hover results in Python via `input[widget.click_input_id]()` / `input[widget.hover_input_id]()`. |
| **Tooltips** | Configure `tooltip={"html": "<b>{name}</b>"}` on `MapWidget` for automatic hover tooltips with template interpolation. |
| **Viewport read-back** | `input[widget.view_state_input_id]()` gives lon/lat/zoom/pitch/bearing after every camera move. |
| **Basemap styles** | `CARTO_POSITRON`, `CARTO_DARK`, `CARTO_VOYAGER`, `OSM_LIBERTY` — or any MapLibre/Mapbox style URL. |
| **Mapbox API key** | `MapWidget(..., mapbox_api_key="pk.xxx")` to use `mapbox://` style URLs with automatic token injection. |
| **Color scales** | `color_range()`, `color_bins()`, `color_quantiles()` with 5 built-in palettes. |
| **Layer visibility** | `set_layer_visibility(session, {"layer1": False})` toggles layers without resending data. |
| **Draggable markers** | `add_drag_marker(session)` places a marker the user can reposition. |
| **DataFrame support** | Pass pandas DataFrames or GeoDataFrames directly as layer data — auto-converted to records / GeoJSON. |
| **Extensions** | `layer(..., extensions=["DataFilterExtension"])` — resolved on the client. |
| **Binary transport** | `encode_binary_attribute(np_array)` — base64-encodes numpy arrays for high-performance typed-array attributes. |
| **Lighting & effects** | Pass `effects=[{"type": "LightingEffect", ...}]` to `update()` for ambient/point/directional lighting. |
| **Multiple views** | `map_view()`, `orthographic_view()`, `first_person_view()` helpers for multi-view layouts. |
| **HTML export** | `widget.to_html(layers, path="map.html")` — standalone HTML file viewable in any browser. |
| **JSON spec** | `to_json()` / `from_json()` for serialising and restoring map configurations. |
| **`@@` accessor convention** | Python strings like `"@@d"` or `"@@d.position"` are resolved to JS arrow functions on the client. |
| **CDN-pinned assets** | deck.gl 9.4.0, MapLibre GL 6.11.2 — deterministic builds. Standalone `to_html()` exports pin MapLibre 5.24.0 (see below). |
| **Conda recipe** | Bundled `conda.recipe/meta.yaml` for micromamba / conda-build. |

### Phase 1 — Controls & Navigation (v0.2)

| Capability | Details |
| --- | --- |
| **MapLibre v5 upgrade** | Upgraded from MapLibre GL JS v3.6.2 to v5.24.0 (breaking API changes handled). |
| **Map controls** | `add_control(session, "NavigationControl")` — navigation, scale, fullscreen, geolocate, attribution, terrain. |
| **`remove_control()`** | Remove previously added controls by type. |
| **`fit_bounds()`** | Fly the camera to a bounding box with optional padding and animation. |
| **Map click events** | `input[widget.map_click_input_id]()` — clicks on the basemap (not on deck.gl layers). |

### Phase 2 — Native Sources & Layers (v0.3)

| Capability | Details |
| --- | --- |
| **`add_source()`** | Add native MapLibre sources (GeoJSON, vector, raster, raster-dem, image, video). |
| **`remove_source()`** | Remove a previously added source by id. |
| **`add_maplibre_layer()`** | Add native MapLibre style layers (fill, line, circle, symbol, heatmap, fill-extrusion, raster, hillshade). |
| **`remove_maplibre_layer()`** | Remove a native layer by id. |
| **`set_paint_property()` / `set_layout_property()`** | Mutate paint or layout properties on existing native layers. |
| **`set_filter()`** | Set a layer filter expression. |
| **`set_style()`** | Change the entire basemap style dynamically (warns that native layers will be lost). |
| **`add_image()` / `remove_image()`** | Load custom icons (PNG/SVG/WebP) for use with symbol layers; SDF recolouring supported. |

### Phase 3 — Globe, Terrain & Popups (v0.4)

| Capability | Details |
| --- | --- |
| **Globe projection** | `set_projection(session, "globe")` — switch between mercator and globe. |
| **Terrain** | `set_terrain(session, source, exaggeration)` — 3D terrain with DEM source. |
| **Popups** | `add_popup()` / `remove_popup()` — HTML popups on native MapLibre layers. |
| **Spatial queries** | `query_rendered_features()`, `query_at_lnglat()` — query visible features. |
| **Multiple markers** | `add_marker()`, `remove_marker()`, `clear_markers()` — programmatic markers with optional popups and drag support. |

### Phase 4 — Drawing, GeoPandas & Export (v0.5)

| Capability | Details |
| --- | --- |
| **Drawing tools** | `enable_draw()` / `disable_draw()` — MapboxDraw integration with point/line/polygon modes. |
| **Get/delete drawn features** | `get_drawn_features()`, `delete_drawn_features()` — manage drawn geometry. |
| **GeoPandas integration** | `add_geodataframe()` — directly add a GeoDataFrame as a GeoJSON source + native layers. |
| **Feature state** | `set_feature_state()` / `remove_feature_state()` — interactive styling without redrawing. |
| **Map export** | `export_image(format="png"/"jpeg"/"webp")` — screenshot the map as a base64 data URL. |

### Phase 5 — Control Helpers & Legend Plugins (v0.6)

| Capability | Details |
| --- | --- |
| **`set_controls()`** | Replace all MapLibre controls at once with a list of control specs. |
| **Control helpers** | `geolocate_control()`, `globe_control()`, `terrain_control()` — typed factory functions. |
| **Legend control** | `legend_control()` — native MapLibre layer legend with checkboxes (`@watergis/maplibre-gl-legend`). |
| **Opacity control** | `opacity_control()` — layer switcher with opacity sliders (`maplibre-gl-opacity`). |
| **deck.gl legend** | `deck_legend_control()` — custom legend panel for deck.gl overlay layers with 5 swatch shapes. |

### Phase 6 — Extended Layer Helpers (v0.7)

| Capability | Details |
| --- | --- |
| **10 new layer helpers** | `arc_layer()`, `icon_layer()`, `path_layer()`, `line_layer()`, `text_layer()`, `column_layer()`, `polygon_layer()`, `heatmap_layer()`, `hexagon_layer()`, `h3_hexagon_layer()`. |
| **Deck-level properties** | `parameters`, `effects`, `views` — full control over the deck.gl `Deck` instance through `update()`. |
| **Layer extensions** | `layer(..., extensions=["DataFilterExtension"])` with auto-resolution on the client. |

### Phase 7 — Widgets, Camera Transitions & Globe (v0.8)

| Capability | Details |
| --- | --- |
| **deck.gl widgets** | `set_widgets()` with 21 helpers: `zoom_widget()`, `compass_widget()`, `fullscreen_widget()`, `scale_widget()`, `gimbal_widget()`, `reset_view_widget()`, `screenshot_widget()`, `loading_widget()`, `timeline_widget()`, `geocoder_widget()`, `theme_widget()`, `popup_widget()`, `icon_widget()`, `toggle_widget()`, `selector_widget()`, `scrollbar_widget()`, `layer_legend_widget()` + 4 experimental; widget state changes arrive as `input[widget.widget_event_input_id]`. |
| **Camera transitions** | `fly_to(longitude, latitude, zoom, speed)` — smooth MapLibre flyTo animation. |
| | `ease_to(longitude, latitude, zoom, duration)` — linear easeTo animation. |
| **Transition helper** | `transition(duration, easing, type)` — animate layer property changes on data update. |
| **Globe view** | `globe_view()` — render the earth as a 3D globe with `GlobeView`. |
| **deck.gl 9.3.6** | CDN upgraded from 9.1.4 to 9.3.6 with widget support. |

### Phase 8 — Animation, Geo-Layers & Drawing Demo (v0.9)

| Capability | Details |
| --- | --- |
| **TripsLayer** | `trips_layer()` for animated vessel/vehicle tracks with `_tripsAnimation` config. |
| **GreatCircleLayer** | `great_circle_layer()` for geodesic arcs. |
| **ContourLayer** | `contour_layer()` for isoline/isoband visualisations. |
| **GridLayer** | `grid_layer()` for rectangular spatial binning with 3-D extrusion. |
| **ScreenGridLayer** | `screen_grid_layer()` for screen-space density grids. |
| **MVTLayer** | `mvt_layer()` for Mapbox Vector Tiles. |
| **WMSLayer** | `wms_layer()` for deck.gl 9.x native WMS layers. |
| **Interleaved rendering** | `MapWidget(interleaved=True)` for deck/MapLibre layer interleaving. |
| **Client-side animation** | `requestAnimationFrame`-based TripsLayer animation loop — no server polling. |
| **Drawing demo** | Tab 7 — MapboxDraw tools, named markers with popups, spatial query, live interaction log. |
| **Animation demo** | Tab 8 — Animated Baltic shipping tracks, GreatCircleLayer, GridLayer, speed/trail controls. |

### v1.12.0 — MapLibre 6.11, MapLibreOverlay, Widget Events & Reconnect

| Capability | Details |
| --- | --- |
| **MapLibre 6.11.2** | Served apps get 6.11.2 (worker-tile leak, `setStyle` terrain crash and `Map#once` fixes); exports stay on 5.24.0. |
| **MapLibreOverlay** | The deck.gl integration is now the one deck.gl recommends for MapLibre. `before_id=` on any layer orders it under a basemap layer in interleaved mode. |
| **Widget events** | `input[widget.widget_event_input_id]` reports timeline, toggle, selector, icon, popup, stats, theme, geocoder, zoom, fullscreen, loading and reset-view changes as `{"id", "widget", "event", "value"}`. |
| **New widgets** | `popup_widget()`, `icon_widget()`, `toggle_widget()`, `selector_widget()`, `scrollbar_widget()`; `zoom_widget(zoom_step=)`. `fps_widget()` / `view_selector_widget()` are deprecated. |
| **Map options** | `MapWidget(map_options={...})` passes `maxBounds`, `maxPitch`, `renderWorldCopies`, `antialias`, ... to MapLibre. |
| **Reconnect** | With `session.allow_reconnect()` (Shiny 1.8), `input[widget.reconnected_input_id]` fires and `resend_last_update(session)` restores the map. |
| **3-D picking** | `pickable="3d"` reaches deck.gl; the click/hover coordinate gains a z value. |
| **Headless demo test** | `tests/test_demo_headless.py` runs two demo tabs under `shiny.testserver`. |

### v1.11.1 — Follow-ups

| Capability | Details |
| --- | --- |
| **Per-session controller** | `set_controller()` is stored per session, so exports match the live map's interaction settings. |
| **Seal IBM demo** | Superseded simulation runs are cancelled; no stale result flashes before the new one. |
| **Colour helpers** | Integers too large for a float count as missing instead of raising. |
| **Tests** | The parser tests no longer all skip when `pyproj` is missing. |

### v1.11.0 — Security, Legends, Data Safety & One Animation Loop

| Capability | Details |
| --- | --- |
| **Accessor sandbox closed** | `@@=` expressions could reach `new Function()` through `f?.(...)` plus a string-built key. Optional calls are now rejected and computed keys must be literals or `d.a.b` paths. |
| **Sanitiser hardening** | SVG animation elements and `<math>` are stripped; output is re-sanitised until stable. |
| **Layer legend widget** | Unticked layers stay listed; `include_hidden=True`; toggles reported via `legend_visibility_input_id`; collapse state survives updates; real colour ramps for aggregation layers. |
| **MapLibre legend** *(changed)* | `legend_control()` without `targets` lists the app's native layers, not the basemap's; `targets={}` restores the old behaviour. `reverse_order` defaults to `True`. |
| **Native layers & styles** | Layers added after load are no longer stranded; `set_style(diff=False)` is honoured. |
| **Data safety** | numpy/pandas/date values no longer crash the session; projected GeoDataFrames are reprojected to EPSG:4326; SHYFEM parsers take `crs=`; colour helpers handle NaN and Series. |
| **Per-session state** *(changed)* | `update_tooltip()` / `set_cooperative_gestures()` no longer mutate the shared widget; use `current_tooltip(session)`. |
| **Exports** | `to_html()` / `to_json()` keep controls, controller and interaction settings. |
| **Performance** | Layers are resolved once and reused; one animation loop per map; animation frames no longer repaint the basemap. |
| **Removed** | The unused `_mixins` package. |

### v1.10.1 — `data:` URI Hardening & CI Repair

| Capability | Details |
| --- | --- |
| **`data:` URIs restricted** | Sanitised tooltip/popup HTML now allows `data:` only for inert raster images (`png`, `jpe?g`, `gif`, `webp`, `bmp`, `avif`, `x-icon`). `data:text/html` executes its markup when navigated to, and `data:image/svg+xml` runs its own `<script>` and event handlers — both are rejected. Layer config such as `ICON_ATLAS` never passes through the sanitiser and is unaffected. |
| **CI restored** | `hypothesis`, `pytest-benchmark` and `pillow` were used by the suite but declared nowhere, so `pytest` aborted during collection on every push. All three are now declared. |
| **`test` extra** | Runs the suite without the geospatial stack. `pandas` is listed explicitly — it previously arrived only as a `geopandas` dependency, so dropping `geopandas` alone silently skipped every pandas-guarded test. |
| **Branch coverage in CI** | `python-package` also runs on `release/**` and `fix/**`, so a branch is tested before it can reach `main`. |
| **Benchmarks execute** | `tests/test_benchmarks.py` had never run on any machine — `pytest-benchmark` was absent everywhere. Its 36 tests now run and pass. |

### v1.10.0 — Review Fixes, MapLibre 6 & deck.gl 9.4

| Capability | Details |
| --- | --- |
| **MapLibre GL JS 6.7.0** | Served apps load v6 (ESM, dynamic `import()`). Standalone `to_html()` exports deliberately stay on 5.24.0 — v6's module worker dies on `file://`, leaving the basemap silently unloaded. |
| **deck.gl 9.4.0** | Upgraded from 9.3.6, together with `@deck.gl/widgets`. |
| **`CoordinateSystem` is a string enum** *(breaking)* | deck.gl 9 identifies coordinate systems by name; the enum still carried deck.gl 8's integers, so the default `scatterplot_layer()` silently failed to render. Python-side comparisons such as `cs == 1` no longer hold. |
| **Python ≥ 3.10** *(breaking)* | Shiny has required 3.10 since 1.6.2. |
| **Maps initialised on the first tab** | The client waited on a `shiny:connected` event that never fired. |
| **Widget helpers restored** | deck.gl promoted several widgets out of experimental; six of eighteen helpers resolved to nothing. `fps_widget` and `view_selector_widget` remain unavailable — deck.gl ships no such class in any 9.x release. |
| **Non-ASCII text labels** | `TextLayer` used the default ASCII atlas, so "Klaipėda" rendered a gap. `characterSet="auto"` builds the atlas from the supplied labels. |
| **`javascript:` filter hardened** | The guard was a substring regex, so `java<TAB>script:` survived it — browsers strip TAB/LF/CR before parsing the scheme. |
| **`normalize_rows()`** | An all-zero row is an absorbing state and normalises to a self-loop (`P[k, k] = 1`); it previously produced a row that `rng.choice` rejects. Non-square matrices still fall back to uniform. |
| **Demo console is clean** | The always-built Google 3D Tiles layer answered 403 without an API key on every page load; it is now built only while its toggle is on. |

### v1.9.2 — Robustness & Correctness Fixes

| Capability | Details |
| --- | --- |
| **`partial_update()` safety** | No longer mutates the caller's layer dicts in-place — uses shallow copies so original DataFrames survive repeated calls. |
| **`to_html()` reliability** | Standalone HTML export now checks `isStyleLoaded()` before pushing layers, preventing silent layer loss when the style is already cached. |
| **Style-swap robustness** | `whenStyleReady` queues all deferred callbacks (not just the first); timeout and handler cleanup prevents stale listeners on rapid `set_style()` calls. |
| **TripsLayer resume** | Pause/resume correctly preserves playback position instead of silently restarting from time 0. |
| **SVG atlas in partial updates** | `deck_partial_update` now pre-rasterises SVG icon atlases before building layers (matches `deck_update`). |
| **Binary decode performance** | `decodeBinaryValue` uses `Uint8Array.from()` — eliminates per-byte string copies for large binary attributes. |
| **Mapbox API key safety** | `transformRequest` restricted to `mapbox://` and `api.mapbox.com` URLs — no longer leaks tokens to third-party tiles containing "mapbox" in the path. |
| **Zero-value view state** | All `parseFloat \|\| default` patterns replaced with `isNaN` guards — `zoom=0`, `longitude=0`, `latitude=0` no longer silently coerced. |
| **`color_bins()` formula** | Corrected equal-width binning from `n_bins-1` to `n_bins` scale factor. |
| **Antimeridian handling** | `in_bounds()` now correctly filters points when the viewport straddles the 180°/-180° line. |
| **Viewport debounce** | Generation check before AND after `fn()` prevents wasted computation on rapid pan/zoom. |
| **`.grd` parser hardening** | UTF-8 encoding with replace fallback; bounds checks prevent `IndexError` on malformed lines. |
| **Thread-safe demo data** | `random.seed()` replaced with `random.Random()` instances — no global RNG mutation in cached demo functions. |

### v1.9.0 — Security Hardening, Lazy Init & Expanded Sprite Library

| Capability | Details |
| --- | --- |
| **XSS hardening** | `sanitizeHtml` DOM-based sanitizer (replaces regex) strips 12 tag types, all `on*` event handlers, and `javascript:` URIs. Applied to tooltip `innerHTML`, marker popups, and native popups. |
| **Tooltip validation** | `MapWidget` constructor and `update_tooltip()` raise `ValueError` when the `tooltip` dict is missing the required `"html"` key. Shared `_validate_tooltip()` helper enforces the same rules in both places. |
| **Lazy map initialization** | Maps in hidden Bootstrap tabs defer WebGL context creation until the tab is first shown. Deferred messages are queued and replayed automatically, preventing wasted GPU contexts and blank-canvas bugs. |
| **Sprite library — 10 dorsal silhouettes** | `ICON_ATLAS` and `ICON_MAPPING` expanded from 3 side-view SVG seals to 10 top-down PNG species: Grey seal, Ringed seal, Harbour seal, Harbour porpoise, Bottlenose dolphin, White-beaked dolphin, Atlantic cod, Baltic herring, Atlantic salmon, European smelt. Species-specific scaling reflects real body size proportions. |
| **`sprites/build_atlas.py`** | Sprite build pipeline — drop source PNGs into `sprites/src/`, run the script, and it auto-generates the atlas PNG and `ICON_MAPPING` dict. |
| **Error visibility** | `initMap` and CDN timeout failures now surface a styled in-map error banner instead of only logging to the console. |
| **Robustness** | `replayDeferredMessages` uses per-handler try/catch so one bad message cannot abort the queue. `isInVisibleTab` walks the full Bootstrap ancestor chain. `cloneLayersData` performs a 2-level deep clone of transition specs. |
| **Test infrastructure** | `_FakeSession` consolidated into shared `conftest.py`. 10 new XSS/validation tests in `test_tooltip_xss.py`. |

### v1.7.0 — Layer Legend Widget, Animation API & Color Ramps

| Capability | Details |
| --- | --- |
| **`layer_legend_widget()`** | Auto-introspecting deck.gl legend widget — reads active layers at runtime, detects layer type → swatch shape, extracts colors from props/data. Supports manual entries, visibility checkboxes, collapsible header, `exclude_layers`, `label_map`. |
| **Layer Gallery auto-legend** | Tab 1 demo now uses `layer_legend_widget(auto_introspect=True)` with live updates as layers are toggled. |
| **Color ramp constants** | 5 new palettes (`PALETTE_BLUES`, `PALETTE_GREENS`, `PALETTE_REDS`, `PALETTE_YELLOW_RED`, `PALETTE_BLUE_WHITE`) + 7 short-name aliases (`VIRIDIS`, `OCEAN_DEPTH`, etc.). |
| **Client-side animation API** | `animate_prop()` + `set_animation()` for `requestAnimationFrame`-based visual animations with zero server round-trips. |
| **Performance patterns guide** | New `docs/performance-patterns.md` with static/dynamic split pattern and data visualization recipes. |

### v1.6.2 — Security & Error Handling

| Capability | Details |
| --- | --- |
| **Accessor expression validation** | Regex whitelist + prototype pollution blocklist before `new Function()` — prevents code injection. |
| **Error logging** | All silent `catch` blocks now log `console.warn` (tooltip, controller, controls, WMS, map instance). |
| **HTML escaping** | Title and widget ID properly escaped in `to_html()` export to prevent XSS. |
| **COORDINATE_SYSTEM consolidation** | Single source of truth in `enums.py` with backwards-compatible alias. |
| **Input validation** | `encode_binary_attribute()` raises `TypeError` for non-numpy input. |

### v1.6.1 — Demo Update

| Capability | Details |
| --- | --- |
| **Demo gallery expansion** | All 9 new layer types showcased in Layer Gallery tab with UI toggles. |
| **Updated layer count** | Demo UI reflects "33 layer helpers" (was 24). |
| **Data generator caching** | New layer data generators use `@functools.lru_cache` for efficiency. |

### v1.6.0 — Complete Layer Coverage (33 Layer Types)

| Capability | Details |
| --- | --- |
| **9 new layer helpers** | `grid_cell_layer()`, `solid_polygon_layer()`, `a5_layer()`, `geohash_layer()`, `h3_cluster_layer()`, `quadkey_layer()`, `s2_layer()`, `tile_3d_layer()`, `scenegraph_layer()`. |
| **100% deck.gl coverage** | shiny_deckgl now provides typed helpers for all 33 deck.gl layer types. |
| **LayerType enum** | 9 new enum values for type-safe layer type constants. |
| **Demo gallery update** | Layer Gallery tab showcases all 33 layer types with toggles and live visualization. |

### v1.5.0 — Enum Types & TypedDict Definitions

| Capability | Details |
| --- | --- |
| **12 enum classes** | `ControlPosition`, `WidgetPlacement`, `EasingFunction`, `TransitionType`, `ControlType`, `Projection`, `CoordinateSystem`, `DrawMode`, `LayerType`, `ViewType`, `LightType`, `PostProcessShader`. |
| **TypedDict definitions** | Structured type hints for configuration dicts: `ControlSpec`, `WidgetSpec`, `ViewSpec`, `LayerSpec`, `EffectSpec`, `LightSpec`, `TooltipSpec`, `TransitionSpec`, `ViewState`. |
| **H3HexagonLayer fix** | Correctly normalizes H3 cell IDs to lowercase for deck.gl compatibility. |
| **214 new tests** | Expanded test suite covering enums, TypedDicts, and edge cases. |

### v1.4.0 — Partial Updates, Patch Layer & JS Modernisation

| Capability | Details |
| --- | --- |
| **`partial_update()`** | Push sparse layer patches — only changed properties are sent, avoiding full-stack resend. Auto-serialises DataFrames. |
| **`patch_layer()`** | Convenience wrapper to patch a single layer by ID: `await widget.patch_layer(session, "my_layer", opacity=0.5)`. |
| **Tooltip dismiss** | Map tooltips auto-hide when cursor moves to empty space; Bootstrap switch tooltips no longer persist after toggle. |
| **JS modernisation** | `structuredClone()` replaces `JSON.parse(JSON.stringify())`; all `var` declarations converted to `const`/`let`. |
| **TripsLayer partial fix** | `partial_update` now correctly restarts TripsLayer animation after patching. |

### v1.3.0 — Mesh Parsers, Colour Scales & Library Extraction

| Capability | Details |
| --- | --- |
| **`parsers.py` module** | `parse_shyfem_grd()` and `parse_shyfem_mesh()` — parse SHYFEM `.grd` finite-element meshes into deck.gl layer-ready data. Auto-detects UTM Zone 33N → WGS84 projection. |
| **`custom_geometry()`** | Helper to convert parsed mesh geometry into `simple_mesh_layer()` kwargs with inline `luma.Geometry` vertex arrays. |
| **`COORDINATE_SYSTEM`** | Enum-style constants mirroring deck.gl's coordinate systems: `DEFAULT`, `LNGLAT`, `METER_OFFSETS`, `LNGLAT_OFFSETS`, `CARTESIAN`. |
| **`depth_color()`** | Bathymetric blue-gradient RGBA colour function with configurable max depth and alpha. |
| **Colour Scales tab** | Demo Tab 4 — interactive palette picker (5 palettes × 3 modes), swatch preview, statistics, code examples, and live-coloured bathymetry map. |
| **Demo data factories** | 7 gallery data factories, fish species colours, SHYFEM helpers, and legend metadata extracted from `app.py` into reusable library modules. |
| **Demo refactored** | 10 tabs (was 9); inline helpers extracted into `_demo_data.py`, `_demo_css.py`, `_version.py`, `colors.py`. |

### v1.0.0 — Extensions, Clusters & Modular Architecture

| Capability | Details |
| --- | --- |
| **Module split** | 3 000-line `components.py` monolith split into 8 focused modules with backward-compatible re-export shim. |
| **Extension helpers** | `brushing_extension()`, `collision_filter_extension()`, `data_filter_extension()`, `mask_extension()`, `clip_extension()`, `terrain_extension()`, `fill_style_extension()`, `path_style_extension()` — convenience functions for deck.gl layer extensions. |
| **Cluster layers** | `add_cluster_layer()` / `remove_cluster_layer()` — MapLibre GeoJSON clustering with click-to-zoom, size-stepped circles, and count labels. |
| **Cooperative gestures** | `MapWidget(cooperative_gestures=True)` — require Ctrl+scroll to zoom, two-finger drag on mobile. |
| **Style diff** | `set_style(session, url, diff=True)` — preserves sources/layers across basemap changes. |
| **Demo Tab 9** | Interactive showcase: BrushingExtension, DataFilterExtension, MapLibre clustering, cooperative gestures. |

### v1.1.0 — IBM Helpers, Trips Animation Module & Audit Fixes

| Capability | Details |
| --- | --- |
| **`format_trips()`** | Normalise raw `[lon, lat]` or `[lon, lat, time]` paths into `trips_layer()` dict format with auto-timestamps and per-trip property merge. |
| **`trips_animation_ui()`** | Reusable Shiny module — Play/Pause/Reset buttons + speed & trail sliders, drop into any sidebar. |
| **`trips_animation_server()`** | Wires animation buttons to `MapWidget.trips_control()`, exposes `speed` / `trail` reactive values. |
| **`Extension` type alias** | Unified return type for all 8 extension helpers (`str \| list[str \| dict]`). |
| **`wms_layer()` validation** | Now raises `ValueError` when the required `layers` kwarg is omitted. |
| **Thread-safe MPA cache** | HELCOM GeoJSON loader switched to `@functools.lru_cache`. |
| **Demo Tab 12 rewired** | Seal IBM tab now uses `trips_animation_ui/server` — ~40 lines of boilerplate removed. |

### v1.2.0 — Effects Module, New Layers & Demo Consolidation

| Capability | Details |
| --- | --- |
| **`effects.py` module** | 6 helpers for deck.gl lighting & post-processing: `ambient_light()`, `point_light()`, `directional_light()`, `sun_light()`, `lighting_effect()`, `post_process_effect()`. |
| **`point_cloud_layer()`** | Typed helper for 3-D point cloud rendering. |
| **`simple_mesh_layer()`** | Place 3-D OBJ/PLY models on the map. |
| **`terrain_layer()`** | Reconstruct mesh from Terrain-RGB height-map tiles. |
| **`fp64_extension()`** | Double-precision GPU rendering for extreme zoom levels. |
| **`orbit_view()`** | `OrbitView` — orbit a camera around a 3-D target. |
| **Demo refactored** | 14 → 11 tabs: Effects merged into 3D Visualisation, Clusters into MapLibre Controls, Extensions into Advanced, Animation into Seal IBM; new Layer Gallery tab (all 33 layer helpers). |
| **JS `buildEffects`** | Updated for `SunLight` support via `@@sunLight` marker. |

## Environment & Prerequisites

### MapLibre versions: 6.11.2 served, 5.24.0 exported

Shiny apps load **MapLibre GL JS 6.11.2**. v6 is ESM-only — it ships no
UMD/IIFE build — so it is loaded with a dynamic `import()` rather than a
`<script src>` tag. The module URL is published in an inert
`<script type="application/json" id="shiny-deckgl-cdn">` block, which a strict
`script-src` Content-Security-Policy allows without `'unsafe-inline'`.

Standalone `to_html()` exports deliberately stay on **MapLibre 5.24.0**.
MapLibre 6 starts its tile worker as a *module* worker created from a blob URL;
on a `file://` page the origin is opaque, so that worker cannot resolve its own
imports and dies without raising a map error. The deck.gl layers still draw but
the basemap never loads — a silent failure. MapLibre 5's classic worker has no
such restriction, so exports opened by double-click keep working.

If you serve an export over HTTP rather than opening it from disk, v6 works
there too; the pin is in `shiny_deckgl._cdn.MAPLIBRE_EXPORT_VERSION`.

Note that MapLibre 5.24.0 is the final v5 release and receives no further
updates.

- **Python ≥ 3.10** and **Shiny ≥ 1.6.3** are required.
  (Shiny 1.6.2 and later require Python 3.10.)
- Any standard Python environment works (venv, conda, micromamba, system).

## Installation

```bash
pip install -e .          # editable install (local package, not on PyPI yet)
```

For binary transport support (numpy):

```bash
pip install -e ".[binary]"
```

For GeoPandas integration:

```bash
pip install -e ".[geopandas]"
```

For the test suite (no geospatial stack) or the full contributor environment:

```bash
pip install -e ".[test]"   # pytest, hypothesis, pytest-benchmark, numpy, pandas, pillow
pip install -e ".[dev]"    # the above plus geopandas and ruff
```

Or install the published conda package:

```bash
micromamba install -c razinka -c conda-forge shiny-deckgl
# or: conda install -c razinka -c conda-forge shiny-deckgl
```

Note the conda package is named `shiny-deckgl` (hyphen); the import name is
`shiny_deckgl` (underscore). To build it yourself:

```bash
conda build conda.recipe/   # or: micromamba build conda.recipe/
```

## Quick Start

```bash
shiny_deckgl-demo
```

This launches a comprehensive demo app centred on the Baltic Sea with scatter
points, WMS layers, controls, drawing tools, markers, popups, terrain, and
HTML export.

## Usage in your own apps

```python
from shiny import App, ui, reactive, Session, render
from shiny_deckgl import MapWidget, scatterplot_layer, head_includes, CARTO_DARK

map_widget = MapWidget(
    "mymap",
    view_state={"longitude": 21.1, "latitude": 55.7, "zoom": 8},
    tooltip={"html": "<b>Point</b><br/>lon: {0}, lat: {1}"},
    style=CARTO_DARK,
)

app_ui = ui.page_fluid(
    head_includes(),
    map_widget.ui(height="80vh"),
    ui.output_text_verbatim("click_info"),
)

def server(input, output, session: Session):
    @reactive.Effect
    async def _push_layers():
        layers = [scatterplot_layer("pts", [[21.12, 55.70], [21.15, 55.72]])]
        await map_widget.update(session, layers)

    @render.text
    def click_info():
        val = input[map_widget.click_input_id]()
        return str(val) if val else "Click a point…"

my_app = App(app_ui, server)
```

```bash
shiny run my_app.py
```

### Using WMS tile layers

```python
from shiny_deckgl import tile_layer

wms_url = (
    "https://ows.emodnet-bathymetry.eu/wms?"
    "SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap"
    "&FORMAT=image/png&TRANSPARENT=true"
    "&LAYERS=emodnet:mean_atlas_land"
    "&CRS=EPSG:3857&WIDTH=256&HEIGHT=256"
    "&BBOX={bbox-epsg-3857}"
)
layers.append(tile_layer("emodnet-bathy", wms_url))
```

### Color scales

```python
from shiny_deckgl import color_bins, PALETTE_OCEAN

depths = [10.5, 50.2, 120.0, 300.7, 15.3]
colors = color_bins(depths, n_bins=4, palette=PALETTE_OCEAN)
# colors[i] is [R, G, B, 255] for each depth value
```

### HTML export

```python
layers = [scatterplot_layer("pts", [[21.12, 55.70]])]
map_widget.to_html(layers, path="my_map.html", title="Exported Map")
```

### deck.gl legend for overlay layers

```python
from shiny_deckgl import deck_legend_control, scatterplot_layer, arc_layer, heatmap_layer

await widget.set_controls(session, [
    {"type": "navigation", "position": "top-right"},
    deck_legend_control(
        entries=[
            {"layer_id": "ports", "label": "Baltic Ports",
             "color": [65, 182, 196], "shape": "circle"},
            {"layer_id": "routes", "label": "Shipping Routes",
             "color": [255, 140, 0], "color2": [200, 0, 80], "shape": "arc"},
            {"layer_id": "density", "label": "Observation Density",
             "colors": [[0, 25, 0, 255], [0, 209, 0, 255], [255, 255, 0, 255], [255, 0, 0, 255]],
             "shape": "gradient"},
        ],
        position="bottom-right",
        title="Deck.gl Layers",
    ),
])
```

### Widgets and camera transitions

```python
from shiny_deckgl import zoom_widget, compass_widget, fullscreen_widget

await widget.set_widgets(session, [
    zoom_widget(), compass_widget(), fullscreen_widget(),
])

await widget.fly_to(session, longitude=20.0, latitude=55.5, zoom=8, pitch=45)
```

## Codebase Structure

| File | Purpose |
| --- | --- |
| `src/shiny_deckgl/map_widget.py` | `MapWidget` class — core widget with session methods, HTML export, JSON serialisation. |
| `src/shiny_deckgl/layers.py` | Generic `layer()` + 33 typed layer helpers (scatter, arc, trips, grid, point cloud, mesh, terrain, scenegraph, tile3d, …). |
| `src/shiny_deckgl/colors.py` | Color scales (`color_range`, `color_bins`, `color_quantiles`), palettes, basemap constants. |
| `src/shiny_deckgl/views.py` | View helpers (`map_view`, `orthographic_view`, `first_person_view`, `globe_view`, `orbit_view`). |
| `src/shiny_deckgl/widgets.py` | 21 deck.gl widget helpers (zoom, compass, fullscreen, timeline, toggle, selector, …) plus two deprecated ones. |
| `src/shiny_deckgl/controls.py` | MapLibre control helpers, legend, opacity, deck.gl legend. |
| `src/shiny_deckgl/extensions.py` | 9 extension helpers (brushing, collision filter, data filter, mask, fp64, …). |
| `src/shiny_deckgl/_data_utils.py` | DataFrame/GeoDataFrame serialisation, binary transport. |
| `src/shiny_deckgl/_transitions.py` | `transition()` helper for layer property animations. |
| `src/shiny_deckgl/components.py` | Backward-compatible re-export shim (imports from split modules). |
| `src/shiny_deckgl/effects.py` | Effect helpers: `lighting_effect()`, `post_process_effect()`, and 4 light-source factories. |
| `src/shiny_deckgl/parsers.py` | SHYFEM `.grd` mesh parsers: `parse_shyfem_grd()` (PolygonLayer data) and `parse_shyfem_mesh()` (SimpleMeshLayer geometry). |
| `src/shiny_deckgl/app.py` | Demo `app` instance — 10-tab sidebar UI showcasing all features. |
| `src/shiny_deckgl/ui.py` | `head_includes()` — injects pinned CDN scripts and local JS/CSS. |
| `src/shiny_deckgl/_cdn.py` | CDN URL constants — single source of truth for all external asset URLs. |
| `src/shiny_deckgl/_version.py` | Package version — single source of truth. |
| `src/shiny_deckgl/cli.py` | `shiny_deckgl-demo` CLI entry point. |
| `src/shiny_deckgl/_demo_data.py` | Sample Baltic Sea data (ports, routes, MPA GeoJSON) for the demo app. |
| `src/shiny_deckgl/_demo_css.py` | Marine-themed CSS and UI helpers for the demo app. |
| `src/shiny_deckgl/ibm.py` | IBM (Individual-Based Model) visual assets, `format_trips()` data helper, and `trips_animation_ui/server` Shiny module. |
| `src/shiny_deckgl/resources/deckgl-init.js` | Frontend: MapLibre init, deck.gl overlay, message handlers, draw tools, popups, terrain. |
| `src/shiny_deckgl/resources/styles.css` | Minimal layout + tooltip styles for `.deckgl-map` containers. |
| `conda.recipe/meta.yaml` | Conda build recipe (version synced with `_version.py`). |
| `src/shiny_deckgl/_sealmove.py` | Seal movement simulation engine for the IBM demo tab. |
| `sprites/build_atlas.py` | Sprite build pipeline — drop source PNGs into `sprites/src/`, auto-generates atlas PNG and `ICON_MAPPING` dict. |
| `tests/test_basic.py` | 1 078 unit tests covering all features. |
| `tests/test_tooltip_xss.py` | 10 XSS/validation tests covering `sanitizeHtml`, tooltip validation, and hyphenated field templates. |

## Performance Patterns

See [docs/performance-patterns.md](docs/performance-patterns.md) for guidance on:

- **Static / Dynamic Layer Split** — use `partial_update()` for frequently-changing layers to reduce serialisation cost by 80–90 %
- **Categorical Data** — use `scatterplot_layer` with pre-computed colors for discrete data classes

## Running Tests

```bash
pip install -e ".[dev]"   # or ".[test]" to skip the geospatial tests
pytest tests/ -v
```

A few suites skip themselves when an optional tool is missing rather than
failing: the JS helper tests (`tests/_js_harness.py`) need `node` on PATH, and
the end-to-end tests (`tests/test_e2e_playwright.py`) need Playwright with
Chromium installed. The end-to-end file is memory-hungry — on a 16 GB machine
run it per class rather than all at once.

## License

MIT — see [LICENSE](LICENSE).
