Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lonboard/_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be further improved on from the Python side to connect with #908


height = HeightTrait().tag(sync=True)
"""Height of the map in pixels, or valid CSS height property.

Expand Down
106 changes: 50 additions & 56 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();

Expand Down Expand Up @@ -116,6 +117,7 @@ function App() {
);
const [parameters] = useModelState<object>("parameters");
const [customAttribution] = useModelState<string>("custom_attribution");
const [renderMode] = useModelState<string>("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
Expand Down Expand Up @@ -156,7 +158,7 @@ function App() {
const loadAndUpdateLayers = async () => {
try {
const childModels = await loadChildModels(
model.widget_manager,
model.widget_manager as IWidgetManager,
childLayerIds,
);

Expand Down Expand Up @@ -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,
Comment on lines +251 to +253
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using a more specific type assertion instead of @ts-expect-error to make the intent clearer and safer.

Suggested change
// @ts-expect-error useDevicePixels should allow number
// https://github.com/visgl/deck.gl/pull/9826
useDevicePixels: isDefined(useDevicePixels) ? useDevicePixels : true,
// useDevicePixels should allow number (see https://github.com/visgl/deck.gl/pull/9826)
useDevicePixels: (isDefined(useDevicePixels) ? useDevicePixels : true) as boolean | number,

Copilot uses AI. Check for mistakes.

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 (
<div
className="lonboard"
Expand All @@ -252,58 +293,11 @@ function App() {
/>
)}
<div className="bg-red-800 h-full w-full relative">
<DeckGL
ref={deckRef}
style={{ width: "100%", height: "100%" }}
initialViewState={
["longitude", "latitude", "zoom"].every((key) =>
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 || {}}
>
<Map
mapStyle={mapStyle || DEFAULT_MAP_STYLE}
customAttribution={customAttribution}
></Map>
</DeckGL>
{renderMode === "overlay" ? (
<OverlayRenderer {...mapRenderProps} />
) : (
<DeckFirstRenderer {...mapRenderProps} />
)}
</div>
</div>
</div>
Expand Down
34 changes: 34 additions & 0 deletions src/renderers/deck-first.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 DeckFirstRenderer: React.FC<MapRendererProps> = (mapProps) => {
// Remove maplibre-specific props before passing to DeckGL
const { mapStyle, customAttribution, deckRef, ...deckProps } = mapProps;
return (
<DeckGL
ref={deckRef}
style={{ width: "100%", height: "100%" }}
controller={true}
// https://deck.gl/docs/api-reference/core/deck#_typedarraymanagerprops
_typedArrayManagerProps={{
overAlloc: 1,
poolSize: 0,
}}
{...deckProps}
>
<Map mapStyle={mapStyle} customAttribution={customAttribution}></Map>
</DeckGL>
);
};

export default DeckFirstRenderer;
3 changes: 3 additions & 0 deletions src/renderers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as DeckFirst } from "./deck-first";
export { default as Overlay } from "./overlay";
export type { MapRendererProps } from "./types";
50 changes: 50 additions & 0 deletions src/renderers/overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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 OverlayRenderer: React.FC<MapRendererProps> = (mapProps) => {
// Remove maplibre-specific props before passing to DeckGL
const { mapStyle, customAttribution, initialViewState, ...deckProps } =
mapProps;
return (
<Map
reuseMaps
initialViewState={initialViewState}
mapStyle={mapStyle}
attributionControl={{ customAttribution }}
style={{ width: "100%", height: "100%" }}
>
<DeckGLOverlay
// https://deck.gl/docs/api-reference/core/deck#_typedarraymanagerprops
_typedArrayManagerProps={{
overAlloc: 1,
poolSize: 0,
}}
{...deckProps}
/>
</Map>
);
};

export default OverlayRenderer;
22 changes: 22 additions & 0 deletions src/renderers/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { DeckProps, View } from "@deck.gl/core";
import type { DeckGLRef } from "@deck.gl/react";
import type { RefObject } from "react";

type ViewOrViews = View | View[] | null;
export type MapRendererProps<ViewsT extends ViewOrViews = null> = Pick<
DeckProps<ViewsT>,
| "getCursor"
| "getTooltip"
| "initialViewState"
| "layers"
| "onClick"
| "onHover"
| "onViewStateChange"
| "parameters"
| "pickingRadius"
| "useDevicePixels"
> & {
mapStyle: string;
customAttribution: string;
deckRef?: RefObject<DeckGLRef | null>;
};