From aaf66a339c784c1c415f177978cae057e9eefca2 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 13 Oct 2025 22:29:58 -0400 Subject: [PATCH 1/4] Define split renderers --- src/renderers/deck-first.tsx | 57 +++++++++++++++++++++++++++++ src/renderers/index.ts | 3 ++ src/renderers/overlay.tsx | 70 ++++++++++++++++++++++++++++++++++++ src/renderers/types.ts | 20 +++++++++++ 4 files changed, 150 insertions(+) create mode 100644 src/renderers/deck-first.tsx create mode 100644 src/renderers/index.ts create mode 100644 src/renderers/overlay.tsx create mode 100644 src/renderers/types.ts diff --git a/src/renderers/deck-first.tsx b/src/renderers/deck-first.tsx new file mode 100644 index 00000000..4664cfb4 --- /dev/null +++ b/src/renderers/deck-first.tsx @@ -0,0 +1,57 @@ +import DeckGL from "@deck.gl/react"; +import React from "react"; +import Map from "react-map-gl/maplibre"; +import type { MapRendererProps } from "./types"; + +/** + * DeckFirst renderer: DeckGL wraps Map component + * + * In this rendering mode, deck.gl is the parent component that manages + * the canvas and view state, with the map rendered as a child component. + * This is the traditional approach where deck.gl has full control over + * the rendering pipeline. + */ +const DeckFirst: React.FC = ({ + mapStyle, + customAttribution, + initialViewState, + layers, + deckRef, + getTooltip, + isDrawingBBoxSelection, + pickingRadius, + useDevicePixels, + parameters, + onMapClick, + onMapHover, + onViewStateChange, +}) => { + return ( + (isDrawingBBoxSelection ? "crosshair" : "grab")} + pickingRadius={pickingRadius} + onClick={onMapClick} + onHover={onMapHover} + // @ts-expect-error useDevicePixels should allow number + // https://github.com/visgl/deck.gl/pull/9826 + useDevicePixels={useDevicePixels} + // https://deck.gl/docs/api-reference/core/deck#_typedarraymanagerprops + _typedArrayManagerProps={{ + overAlloc: 1, + poolSize: 0, + }} + onViewStateChange={onViewStateChange} + parameters={parameters} + > + + + ); +}; + +export default DeckFirst; diff --git a/src/renderers/index.ts b/src/renderers/index.ts new file mode 100644 index 00000000..3b97f2ff --- /dev/null +++ b/src/renderers/index.ts @@ -0,0 +1,3 @@ +export { default as DeckFirst } from "./deck-first"; +export { default as Overlay } from "./overlay"; +export type { MapRendererProps } from "./types"; diff --git a/src/renderers/overlay.tsx b/src/renderers/overlay.tsx new file mode 100644 index 00000000..37cdb365 --- /dev/null +++ b/src/renderers/overlay.tsx @@ -0,0 +1,70 @@ +import React from "react"; +import Map, { useControl } from "react-map-gl/maplibre"; +import { MapboxOverlay, MapboxOverlayProps } from "@deck.gl/mapbox"; +import type { MapRendererProps } from "./types"; + +/** + * DeckGLOverlay component that integrates deck.gl with react-map-gl + * + * Uses the useControl hook to create a MapboxOverlay instance that + * renders deck.gl layers on top of the base map. + */ +function DeckGLOverlay(props: MapboxOverlayProps) { + const overlay = useControl(() => new MapboxOverlay(props)); + overlay.setProps(props); + return null; +} + +/** + * Overlay renderer: Map wraps DeckGLOverlay component + * + * In this rendering mode, the map is the parent component that controls + * the view state, with deck.gl layers rendered as an overlay using the + * MapboxOverlay. This approach gives the base map more control and can + * enable features like interleaved rendering between map and deck layers. + */ +const Overlay: React.FC = ({ + mapStyle, + customAttribution, + initialViewState, + layers, + getTooltip, + isDrawingBBoxSelection, + pickingRadius, + useDevicePixels, + parameters, + onMapClick, + onMapHover, + onViewStateChange, +}) => { + return ( + + (isDrawingBBoxSelection ? "crosshair" : "grab")} + pickingRadius={pickingRadius} + onClick={onMapClick} + onHover={onMapHover} + // @ts-expect-error useDevicePixels should allow number + // https://github.com/visgl/deck.gl/pull/9826 + useDevicePixels={useDevicePixels} + // https://deck.gl/docs/api-reference/core/deck#_typedarraymanagerprops + _typedArrayManagerProps={{ + overAlloc: 1, + poolSize: 0, + }} + onViewStateChange={onViewStateChange} + parameters={parameters} + /> + + ); +}; + +export default Overlay; diff --git a/src/renderers/types.ts b/src/renderers/types.ts new file mode 100644 index 00000000..62ef98d9 --- /dev/null +++ b/src/renderers/types.ts @@ -0,0 +1,20 @@ +import { MapViewState, PickingInfo, type Layer } from "@deck.gl/core"; +import { DeckGLRef } from "@deck.gl/react"; +import type { RefObject } from "react"; + +export interface MapRendererProps { + mapStyle: string; + customAttribution: string; + initialViewState: MapViewState; + layers: Layer[]; + deckRef?: RefObject; + showTooltip: boolean; + getTooltip?: ((info: PickingInfo) => string | null) | undefined; + isDrawingBBoxSelection: boolean; + pickingRadius: number; + useDevicePixels: number | boolean; + parameters: object; + onMapClick: (info: PickingInfo) => void; + onMapHover: (info: PickingInfo) => void; + onViewStateChange: (event: { viewState: MapViewState }) => void; +} From ca7bfd44d203a462e137931b2aa18f6a01a81dea Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 13 Oct 2025 22:59:37 -0400 Subject: [PATCH 2/4] Implement split renderers --- lonboard/_map.py | 2 + src/index.tsx | 106 +++++++++++++++++------------------ src/renderers/deck-first.tsx | 33 ++--------- src/renderers/overlay.tsx | 38 ++++--------- src/renderers/types.ts | 32 ++++++----- 5 files changed, 86 insertions(+), 125 deletions(-) diff --git a/lonboard/_map.py b/lonboard/_map.py index 9979e476..3164cb7d 100644 --- a/lonboard/_map.py +++ b/lonboard/_map.py @@ -179,6 +179,8 @@ def on_click(self, callback: Callable, *, remove: bool = False) -> None: Indicates if a click handler has been registered. """ + render_mode = t.Unicode(default_value="deck-first").tag(sync=True) + height = HeightTrait().tag(sync=True) """Height of the map in pixels, or valid CSS height property. diff --git a/src/index.tsx b/src/index.tsx index bc4aff25..911fdf5a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,11 +2,9 @@ import * as React from "react"; import { useEffect, useCallback, useState, useRef } from "react"; import { createRender, useModelState, useModel } from "@anywidget/react"; import type { Initialize, Render } from "@anywidget/types"; -import Map from "react-map-gl/maplibre"; -import DeckGL from "@deck.gl/react"; import { MapViewState, PickingInfo, type Layer } from "@deck.gl/core"; import { BaseLayerModel, initializeLayer } from "./model/index.js"; -import type { WidgetModel } from "@jupyter-widgets/base"; +import type { IWidgetManager, WidgetModel } from "@jupyter-widgets/base"; import { initParquetWasm } from "./parquet.js"; import { isDefined, loadChildModels } from "./util.js"; import { v4 as uuidv4 } from "uuid"; @@ -25,6 +23,9 @@ import throttle from "lodash.throttle"; import SidePanel from "./sidepanel/index"; import { getTooltip } from "./tooltip/index.js"; import { DeckGLRef } from "@deck.gl/react"; +import OverlayRenderer from "./renderers/overlay.js"; +import { MapRendererProps } from "./renderers/types.js"; +import DeckFirstRenderer from "./renderers/deck-first.js"; await initParquetWasm(); @@ -116,6 +117,7 @@ function App() { ); const [parameters] = useModelState("parameters"); const [customAttribution] = useModelState("custom_attribution"); + const [renderMode] = useModelState("render_mode"); // initialViewState is the value of view_state on the Python side. This is // called `initial` here because it gets passed in to deck's @@ -156,7 +158,7 @@ function App() { const loadAndUpdateLayers = async () => { try { const childModels = await loadChildModels( - model.widget_manager, + model.widget_manager as IWidgetManager, childLayerIds, ); @@ -229,6 +231,45 @@ function App() { [isOnMapHoverEventEnabled, justClicked], ); + const mapRenderProps: MapRendererProps = { + mapStyle: mapStyle || DEFAULT_MAP_STYLE, + customAttribution, + deckRef, + initialViewState: ["longitude", "latitude", "zoom"].every((key) => + Object.keys(initialViewState).includes(key), + ) + ? initialViewState + : DEFAULT_INITIAL_VIEW_STATE, + layers: bboxSelectPolygonLayer + ? layers.concat(bboxSelectPolygonLayer) + : layers, + getTooltip: (showTooltip && getTooltip) || undefined, + getCursor: () => (isDrawingBBoxSelection ? "crosshair" : "grab"), + pickingRadius: pickingRadius, + onClick: onMapClickHandler, + onHover: onMapHoverHandler, + // @ts-expect-error useDevicePixels should allow number + // https://github.com/visgl/deck.gl/pull/9826 + useDevicePixels: isDefined(useDevicePixels) ? useDevicePixels : true, + onViewStateChange: (event) => { + const { viewState } = event; + + // This condition is necessary to confirm that the viewState is + // of type MapViewState. + if ("latitude" in viewState) { + const { longitude, latitude, zoom, pitch, bearing } = viewState; + setViewState({ + longitude, + latitude, + zoom, + pitch, + bearing, + }); + } + }, + parameters: parameters || {}, + }; + return (
)}
- - Object.keys(initialViewState).includes(key), - ) - ? initialViewState - : DEFAULT_INITIAL_VIEW_STATE - } - controller={true} - layers={ - bboxSelectPolygonLayer - ? layers.concat(bboxSelectPolygonLayer) - : layers - } - getTooltip={(showTooltip && getTooltip) || undefined} - getCursor={() => (isDrawingBBoxSelection ? "crosshair" : "grab")} - pickingRadius={pickingRadius} - onClick={onMapClickHandler} - onHover={onMapHoverHandler} - useDevicePixels={ - isDefined(useDevicePixels) ? useDevicePixels : true - } - // https://deck.gl/docs/api-reference/core/deck#_typedarraymanagerprops - _typedArrayManagerProps={{ - overAlloc: 1, - poolSize: 0, - }} - onViewStateChange={(event) => { - const { viewState } = event; - - // This condition is necessary to confirm that the viewState is - // of type MapViewState. - if ("latitude" in viewState) { - const { longitude, latitude, zoom, pitch, bearing } = viewState; - setViewState({ - longitude, - latitude, - zoom, - pitch, - bearing, - }); - } - }} - parameters={parameters || {}} - > - - + {renderMode === "overlay" ? ( + + ) : ( + + )}
diff --git a/src/renderers/deck-first.tsx b/src/renderers/deck-first.tsx index 4664cfb4..8012d288 100644 --- a/src/renderers/deck-first.tsx +++ b/src/renderers/deck-first.tsx @@ -11,47 +11,24 @@ import type { MapRendererProps } from "./types"; * This is the traditional approach where deck.gl has full control over * the rendering pipeline. */ -const DeckFirst: React.FC = ({ - mapStyle, - customAttribution, - initialViewState, - layers, - deckRef, - getTooltip, - isDrawingBBoxSelection, - pickingRadius, - useDevicePixels, - parameters, - onMapClick, - onMapHover, - onViewStateChange, -}) => { +const DeckFirstRenderer: React.FC = (mapProps) => { + // Remove maplibre-specific props before passing to DeckGL + const { mapStyle, customAttribution, deckRef, ...deckProps } = mapProps; return ( (isDrawingBBoxSelection ? "crosshair" : "grab")} - pickingRadius={pickingRadius} - onClick={onMapClick} - onHover={onMapHover} - // @ts-expect-error useDevicePixels should allow number - // https://github.com/visgl/deck.gl/pull/9826 - useDevicePixels={useDevicePixels} // https://deck.gl/docs/api-reference/core/deck#_typedarraymanagerprops _typedArrayManagerProps={{ overAlloc: 1, poolSize: 0, }} - onViewStateChange={onViewStateChange} - parameters={parameters} + {...deckProps} > ); }; -export default DeckFirst; +export default DeckFirstRenderer; diff --git a/src/renderers/overlay.tsx b/src/renderers/overlay.tsx index 37cdb365..be95039a 100644 --- a/src/renderers/overlay.tsx +++ b/src/renderers/overlay.tsx @@ -23,20 +23,15 @@ function DeckGLOverlay(props: MapboxOverlayProps) { * MapboxOverlay. This approach gives the base map more control and can * enable features like interleaved rendering between map and deck layers. */ -const Overlay: React.FC = ({ - mapStyle, - customAttribution, - initialViewState, - layers, - getTooltip, - isDrawingBBoxSelection, - pickingRadius, - useDevicePixels, - parameters, - onMapClick, - onMapHover, - onViewStateChange, -}) => { +const OverlayRenderer: React.FC = (mapProps) => { + // Remove maplibre-specific props before passing to DeckGL + const { + mapStyle, + customAttribution, + initialViewState, + // deckRef, + ...deckProps + } = mapProps; return ( = ({ style={{ width: "100%", height: "100%" }} > (isDrawingBBoxSelection ? "crosshair" : "grab")} - pickingRadius={pickingRadius} - onClick={onMapClick} - onHover={onMapHover} - // @ts-expect-error useDevicePixels should allow number - // https://github.com/visgl/deck.gl/pull/9826 - useDevicePixels={useDevicePixels} + // ref={deckRef} // https://deck.gl/docs/api-reference/core/deck#_typedarraymanagerprops _typedArrayManagerProps={{ overAlloc: 1, poolSize: 0, }} - onViewStateChange={onViewStateChange} - parameters={parameters} + {...deckProps} /> ); }; -export default Overlay; +export default OverlayRenderer; diff --git a/src/renderers/types.ts b/src/renderers/types.ts index 62ef98d9..b699fe62 100644 --- a/src/renderers/types.ts +++ b/src/renderers/types.ts @@ -1,20 +1,22 @@ -import { MapViewState, PickingInfo, type Layer } from "@deck.gl/core"; -import { DeckGLRef } from "@deck.gl/react"; +import type { DeckProps, View } from "@deck.gl/core"; +import type { DeckGLRef } from "@deck.gl/react"; import type { RefObject } from "react"; -export interface MapRendererProps { +type ViewOrViews = View | View[] | null; +export type MapRendererProps = Pick< + DeckProps, + | "layers" + | "getTooltip" + | "getCursor" + | "pickingRadius" + | "useDevicePixels" + | "parameters" + | "initialViewState" + | "onClick" + | "onHover" + | "onViewStateChange" +> & { mapStyle: string; customAttribution: string; - initialViewState: MapViewState; - layers: Layer[]; deckRef?: RefObject; - showTooltip: boolean; - getTooltip?: ((info: PickingInfo) => string | null) | undefined; - isDrawingBBoxSelection: boolean; - pickingRadius: number; - useDevicePixels: number | boolean; - parameters: object; - onMapClick: (info: PickingInfo) => void; - onMapHover: (info: PickingInfo) => void; - onViewStateChange: (event: { viewState: MapViewState }) => void; -} +}; From a043703b75138cf3afb92758b8e2533e609b71e7 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 13 Oct 2025 23:11:40 -0400 Subject: [PATCH 3/4] alphabetize --- src/renderers/types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/renderers/types.ts b/src/renderers/types.ts index b699fe62..d6f806d8 100644 --- a/src/renderers/types.ts +++ b/src/renderers/types.ts @@ -5,16 +5,16 @@ import type { RefObject } from "react"; type ViewOrViews = View | View[] | null; export type MapRendererProps = Pick< DeckProps, - | "layers" - | "getTooltip" | "getCursor" - | "pickingRadius" - | "useDevicePixels" - | "parameters" + | "getTooltip" | "initialViewState" + | "layers" | "onClick" | "onHover" | "onViewStateChange" + | "parameters" + | "pickingRadius" + | "useDevicePixels" > & { mapStyle: string; customAttribution: string; From 8dd52abc3cde89b6f467ae41d15fbf1e8800bda7 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Tue, 14 Oct 2025 14:41:57 -0400 Subject: [PATCH 4/4] remove deckRef --- src/renderers/overlay.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/renderers/overlay.tsx b/src/renderers/overlay.tsx index be95039a..646a6f96 100644 --- a/src/renderers/overlay.tsx +++ b/src/renderers/overlay.tsx @@ -25,13 +25,8 @@ function DeckGLOverlay(props: MapboxOverlayProps) { */ const OverlayRenderer: React.FC = (mapProps) => { // Remove maplibre-specific props before passing to DeckGL - const { - mapStyle, - customAttribution, - initialViewState, - // deckRef, - ...deckProps - } = mapProps; + const { mapStyle, customAttribution, initialViewState, ...deckProps } = + mapProps; return ( = (mapProps) => { style={{ width: "100%", height: "100%" }} >