Skip to content

RFC: Expand pydeck API surface (effects, GlobeView, SplitterWidget, ViewLayout) #10452

Description

@chrisgervang

RFC: Expand pydeck API surface

RFC details

Summary

An audit of the deck.gl API surface against what pydeck exposes found four gaps that the extension-binding pattern from #10447 closes cheaply:

  1. Effects are accidentally half-supported today — raw @@type dicts already hydrate into LightingEffect/lights, but there is no typed wrapper, no docs, no examples, and sun/camera lights are unreachable under canonical names.
  2. GlobeView needs only a one-line alias to shed its _GlobeView experimental name — implemented in feat(pydeck): register GlobeView canonical alias in jupyter-widget #10451.
  3. Interactive split views already work today via SplitterWidget — undocumented.
  4. deck.gl 9.4's declarative ViewLayout maps naturally onto pydeck — layout containers are already plain JSON and leaves serialize exactly like pydeck's existing View binding.

Motivation

pydeck serializes Python objects to a JSON payload consumed by @deck.gl/json's JSONConverter inside modules/jupyter-widget. Two mechanisms make new bindings nearly free:

  • Every binding subclasses JSONMixin (bindings/pydeck/pydeck/bindings/json_tools.py); json.dumps(..., default=default_serialize) applies recursively, so wrapper objects nest inside other wrappers' kwargs with no extra machinery. Class discriminator is the "@@type" attribute; kwargs are camelCased.
  • The JS catalog (modules/jupyter-widget/src/playground/create-deck.js) auto-registers every uppercase export of the deck bundle, and jsonConverter.convert(jsonInput) hydrates the entire payload recursively — any nested {"@@type": X} becomes a class instance if X is registered.

The catch: experimental underscore exports (_GlobeView, _SunLight, ...) are only registered under their underscore names and need manual canonical aliases — the precedent being the widget alias block and TerrainExtension in #10447.

Proposal 1: Effects — a pdk.Effect wrapper

pdk.Deck has carried an effects passthrough parameter for years, and LightingEffect, AmbientLight, DirectionalLight, PointLight are already auto-registered (uppercase @deck.gl/core exports). LightingEffect's constructor takes a Record mapping arbitrary string keys to light instances, discriminated at runtime — which matches the converter's new Class(props) convention exactly. This renders a lit scene today, with zero code changes:

pdk.Deck(
    layers=[...],
    effects=[{
        "@@type": "LightingEffect",
        "ambientLight": {"@@type": "AmbientLight", "color": [255, 255, 255], "intensity": 1.0},
        "sun": {"@@type": "DirectionalLight", "direction": [-1, -3, -1], "intensity": 2.0},
    }],
)

It is, however, undocumented, untested, and undiscoverable.

Public API

One generic pdk.Effect(type, **kwargs) mirroring pdk.Extension/pdk.Widget, used for both effects and lights:

lighting = pdk.Effect(
    "LightingEffect",
    ambient_light=pdk.Effect("AmbientLight", color=[255, 255, 255], intensity=1.0),
    sun_light=pdk.Effect("SunLight", timestamp=1554927200000, intensity=1.2),
)
deck = pdk.Deck(layers=[...], effects=[lighting])

Rationale: identical to the established one-generic-wrapper-per-converter-concept precedent, zero per-class wrappers to keep in sync with JS. The "a light is not an effect" naming awkwardness is cosmetic; raw dicts remain a documented equivalent, and a pdk.Light = Effect alias can be added later without breakage. The shadow-enabling _shadow key survives camelCasing (leading underscore preserved).

JS side

  • Add canonical aliases in create-deck.js: SunLight: deckExports._SunLight, CameraLight: deckExports._CameraLight (same block as the widget aliases). Until a frontend publish carries them, "_SunLight" works as a type string.
  • No other JS change: props.effects arrives hydrated at the deck via the whole-payload conversion.

Exclusion

PostProcessEffect is not JSON-constructible — its constructor takes a luma.gl shader-module object as the first positional argument and would crash under new Class(props). Document as unsupported (reachable only via settings.custom_libraries).

Cleanup

pydeck/bindings/light_settings.py is a deck.gl 8.x-era leftover not wired into Deck at all; deprecate its docstring in favor of Effect.

Proposal 2: ViewLayout (deck.gl 9.4 declarative views), phase 1

The 9.4 View Layout system (#10269) is a JSON-friendly declarative description of multi-view arrangements: plain container objects ({type: 'row' | 'column' | 'overlay' | 'spacer', children: [...]} plus a split form) with View instances as leaves, compiled by buildViewsFromViewLayout({layout, width, height, ...}) into the View[] that core's views prop requires.

It maps onto pydeck unusually cleanly: containers are plain JSON (the converter recurses through them untouched) and leaves are exactly what pdk.View already serializes — hydrated in place. No converter changes needed, only a compile step.

Public API

layout = pdk.ViewLayout(
    type="row",
    children=[
        pdk.View(type="MapView", id="main", controller=True),
        pdk.ViewLayout(
            type="column",
            width="30%",
            min_pixels=200,
            children=[
                pdk.View(type="OrthographicView", id="chart", controller=True),
                pdk.ViewLayout(type="spacer", height=40),
            ],
        ),
    ],
)
deck = pdk.Deck(views=layout, ...)
  • Serialization difference from every other pydeck binding: containers are discriminated by literal "type" (not "@@type"), so ViewLayout stores type as a normal attribute. Leaf pdk.Views keep @@type; camelCasing (split_idsplitId) and None-dropping come free from JSONMixin.
  • Validate in __init__: type in the allowed set XOR the orientation+views split form; row/column/overlay require children, spacer forbids them; every leaf View must have an id (the JS compiler throws otherwise — fail early in Python).
  • Payload key: ride the existing views key. Deck(views=pdk.ViewLayout(...)) serializes to an object where arrays serialize today; JS discriminates with Array.isArray vs isViewLayout. No new Deck parameter; the default views array is unchanged.

JS side (modules/jupyter-widget)

  • Pre-req: export isViewLayout/assertViewLayout from @deck.gl/widgets (currently internal — only the compiler and the type are exported).
  • New playground/view-layout-support.js: after conversion, if props.views is a hydrated layout object, compile with buildViewsFromViewLayout using the container's client size; compose an onResize that recompiles with previous (structural view reuse keeps controllers/view state alive) and setProps({views}). Note the standalone HTML path currently wires no onResize — the composed handler must be injected in both HTML and widget modes. Keep per-deck compile state outside the converter (it short-circuits on shallow-equal input) and reuse it in updateDeck.

Works today: SplitterWidget recipe (document it)

_SplitterWidget is already aliased to SplitterWidget in the catalog and manages deck.props.views itself, deferring when views are managed externally. So this works in pydeck 0.9.x now:

pdk.Deck(
    views=None,  # let the widget manage views
    widgets=[pdk.Widget(
        "SplitterWidget",
        view_layout={
            "orientation": "horizontal",
            "views": [
                {"@@type": "MapView", "id": "left", "controller": True},
                {"@@type": "MapView", "id": "right", "controller": True},
            ],
        },
    )],
)

Document it (widget docs + gallery example), including that combining a SplitterWidget with externally-set views is unsupported by design.

Version gating

ViewLayout exists only on the 9.4 line. Bump modules/jupyter-widget/package.json deck deps to 9.4.0-alpha.1 (already true on master; re-verify after #10447 merges) and bindings/pydeck/pydeck/frontend_semver.py DECKGL_SEMVER from "~9.3.*" to the 9.4 line — noting jsdelivr may not resolve ~9.4.* to prerelease tags, in which case pin the exact alpha (PUBLISH.md documents the manual-edit escape hatch). Local dev doesn't wait for a publish: make fast-build && make copy-bundle + to_html(offline=True) or PYDECK_DEV_PORT.

Non-Goals

Implementation Plan

  1. Now (works on the published 9.3 frontend): pdk.Effect + tests + gallery example (examples/effects/lighting_effect.py in feat(jupyter-widget, pydeck): register @deck.gl/extensions for JSON @@type resolution #10447's grouped layout) + effect.rst; document the SplitterWidget recipe; deprecate LightSettings.
  2. Next frontend publish: SunLight/CameraLight aliases (and GlobeView, already in feat(pydeck): register GlobeView canonical alias in jupyter-widget #10451).
  3. 9.4-gated: pdk.ViewLayout (pydeck/bindings/view_layout.py) + isViewLayout export from @deck.gl/widgets + jupyter-widget compile step + frontend semver bump.

Each item branches from master independently of #10447 and lands after it merges (shared files: create-deck.js, deck-bundle.js, both __init__.pys, examples/docs gallery structure, create-deck.spec.ts).

Test Plan

  • Python (cd bindings/pydeck && make test): test_effect.py — nested @@type serialization, camelCasing, Deck(effects=...) round-trip; test_view_layout.py — plain "type" vs leaf @@type, validation errors, Deck.to_json placement under views.
  • JS (yarn vitest run test/modules/jupyter-widget): effects hydrate to LightingEffect instances with hydrated lights; views-layout payload compiles to View[] with correct rects/splitters; array payloads pass through unchanged.
  • End-to-end: run the new gallery examples against a local bundle and confirm lighting/shadows render and layouts recompile on window resize.

Open questions

  • Initial container size before the first onResize in the iframe/HTML template (needs visual verification).
  • jsdelivr prerelease semver resolution for the 9.4 alpha frontend.
  • Basemap sync uses the first viewport — recommend map_provider=None for multi-view layouts, or document first-view behavior.
  • Widget-mode updateDeck relies on size captured from onResize; confirm ordering in the ipywidget flow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions