Skip to content

Commit fd63031

Browse files
authored
pydeck: support Maplibre including Globe projection (#9896)
1 parent 3ab97e8 commit fd63031

File tree

9 files changed

+175
-11
lines changed

9 files changed

+175
-11
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
GlobeView
3+
=========
4+
5+
Over 33,000 power plants of the world plotted by their production capacity (given by height)
6+
and fuel type (green if renewable) on a MapLibre globe view.
7+
8+
This example demonstrates using MapLibre's globe projection with deck.gl layers by setting
9+
map_provider='maplibre' and map_projection='globe'. The globe view uses MapLibre's
10+
MapboxOverlay with interleaved rendering for optimal performance.
11+
"""
12+
import pydeck as pdk
13+
import pandas as pd
14+
15+
POWER_PLANTS = "https://raw.githubusercontent.com/ajduberstein/geo_datasets/master/global_power_plant_database.csv"
16+
17+
df = pd.read_csv(POWER_PLANTS)
18+
19+
20+
def is_green(fuel_type):
21+
"""Return a green RGB value if a facility uses a renewable fuel type"""
22+
if fuel_type.lower() in ("nuclear", "water", "wind", "hydro", "biomass", "solar", "geothermal"):
23+
return [10, 230, 120]
24+
return [230, 158, 10]
25+
26+
27+
df["color"] = df["primary_fuel"].apply(is_green)
28+
29+
# Use MapView with a globe projection
30+
view_state = pdk.ViewState(latitude=51.47, longitude=0.45, zoom=0)
31+
32+
layers = [
33+
pdk.Layer(
34+
"ColumnLayer",
35+
id="power-plant",
36+
data=df,
37+
get_elevation="capacity_mw",
38+
get_position=["longitude", "latitude"],
39+
elevation_scale=100,
40+
pickable=True,
41+
auto_highlight=True,
42+
radius=20000,
43+
get_fill_color="color",
44+
),
45+
]
46+
47+
deck = pdk.Deck(
48+
initial_view_state=view_state,
49+
tooltip={"text": "{name}, {primary_fuel} plant, {country}"},
50+
layers=layers,
51+
# Use MapLibre with globe projection
52+
map_provider="maplibre",
53+
map_style="https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
54+
map_projection="globe",
55+
)
56+
57+
deck.to_html("maplibre_globe.html", css_background_color="black", offline=True)

bindings/pydeck/pydeck/bindings/base_map_provider.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ class BaseMapProvider(Enum):
77
MAPBOX = "mapbox"
88
GOOGLE_MAPS = "google_maps"
99
CARTO = "carto"
10+
MAPLIBRE = "maplibre"

bindings/pydeck/pydeck/bindings/deck.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def __init__(
4545
parameters=None,
4646
widgets=None,
4747
show_error=False,
48+
map_projection=None,
4849
):
4950
"""This is the renderer and configuration for a deck.gl visualization, similar to the
5051
`Deck <https://deck.gl/docs/api-reference/core/deck>`_ class from deck.gl.
@@ -63,7 +64,7 @@ def __init__(
6364
``MAPBOX_API_KEY``, ``GOOGLE_MAPS_API_KEY``, and ``CARTO_API_KEY`` can be set instead of hardcoding the key here.
6465
map_provider : str, default 'carto'
6566
If multiple API keys are set (e.g., both Mapbox and Google Maps), inform pydeck which basemap provider to prefer.
66-
Values can be ``carto``, ``mapbox`` or ``google_maps``
67+
Values can be ``carto``, ``mapbox``, ``google_maps``, or ``maplibre``.
6768
map_style : str or dict, default 'dark'
6869
One of 'light', 'dark', 'road', 'satellite', 'dark_no_labels', and 'light_no_labels', a URI for a basemap
6970
style, which varies by provider, or a dict that follows the Mapbox style `specification <https://docs.mapbox.com/mapbox-gl-js/style-spec/>`_.
@@ -87,6 +88,10 @@ def __init__(
8788
show_error : bool, default False
8889
If ``True``, will display the error in the rendered output.
8990
Otherwise, will only show error in browser console.
91+
map_projection : str, default None
92+
Map projection to use with ``map_provider='maplibre'``.
93+
Values can be ``'globe'`` or ``'mercator'``. Defaults to ``'mercator'`` if not specified.
94+
Only supported with ``map_provider='maplibre'``.
9095
9196
.. _Deck:
9297
https://deck.gl/docs/api-reference/core/deck
@@ -108,6 +113,7 @@ def __init__(
108113
self.description = description
109114
self.effects = effects
110115
self.map_provider = str(map_provider).lower() if map_provider else None
116+
self.map_projection = map_projection
111117
self._tooltip = tooltip
112118
self._show_error = show_error
113119

bindings/pydeck/pydeck/bindings/map_styles.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,19 @@
2323
GOOGLE_ROAD = "roadmap"
2424

2525
styles = {
26-
DARK: {"mapbox": MAPBOX_DARK, "carto": CARTO_DARK},
27-
DARK_NO_LABELS: {"carto": CARTO_DARK_NO_LABELS},
28-
LIGHT: {"mapbox": MAPBOX_LIGHT, "carto": CARTO_LIGHT},
29-
LIGHT_NO_LABELS: {"carto": CARTO_LIGHT_NO_LABELS},
30-
ROAD: {"carto": CARTO_ROAD, "google_maps": GOOGLE_ROAD, "mapbox": MAPBOX_ROAD},
26+
DARK: {"mapbox": MAPBOX_DARK, "carto": CARTO_DARK, "maplibre": CARTO_DARK},
27+
DARK_NO_LABELS: {"carto": CARTO_DARK_NO_LABELS, "maplibre": CARTO_DARK_NO_LABELS},
28+
LIGHT: {"mapbox": MAPBOX_LIGHT, "carto": CARTO_LIGHT, "maplibre": CARTO_LIGHT},
29+
LIGHT_NO_LABELS: {"carto": CARTO_LIGHT_NO_LABELS, "maplibre": CARTO_LIGHT_NO_LABELS},
30+
ROAD: {"carto": CARTO_ROAD, "google_maps": GOOGLE_ROAD, "mapbox": MAPBOX_ROAD, "maplibre": CARTO_ROAD},
3131
SATELLITE: {"mapbox": MAPBOX_SATELLITE, "google_maps": GOOGLE_SATELLITE},
3232
}
3333

3434
_default_map_identifers = {
3535
BaseMapProvider.CARTO: DARK,
3636
BaseMapProvider.MAPBOX: DARK,
3737
BaseMapProvider.GOOGLE_MAPS: GOOGLE_ROAD,
38+
BaseMapProvider.MAPLIBRE: DARK,
3839
}
3940

4041

bindings/pydeck/pydeck/io/templates/index.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
{% endif %}
1111
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css" />
1212
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" />
13+
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.14.0/dist/maplibre-gl.css" />
1314
{{ deckgl_jupyter_widget_bundle }}
1415
<link rel="stylesheet" href={{ deckgl_widget_css_url }} />
1516
<style>

modules/jupyter-widget/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@deck.gl/google-maps": "9.2.0-beta.4",
3636
"@deck.gl/json": "9.2.0-beta.4",
3737
"@deck.gl/layers": "9.2.0-beta.4",
38+
"@deck.gl/mapbox": "9.2.0-beta.4",
3839
"@deck.gl/mesh-layers": "9.2.0-beta.4",
3940
"@deck.gl/widgets": "9.2.0-beta.4",
4041
"@jupyter-widgets/base": "^1.1.10 || ^2 || ^3 || ^4",
@@ -45,7 +46,8 @@
4546
"@luma.gl/core": "^9.2.2",
4647
"@luma.gl/webgl": "^9.2.2",
4748
"d3-dsv": "^1.0.8",
48-
"mapbox-gl": "^1.13.2"
49+
"mapbox-gl": "^1.13.2",
50+
"maplibre-gl": "^5.14.0"
4951
},
5052
"jupyterlab": {
5153
"extension": "src/plugin",

modules/jupyter-widget/src/playground/create-deck.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import makeTooltip from './widget-tooltip';
1515
import mapboxgl, {modifyMapboxElements} from './utils/mapbox-utils';
1616
import {loadScript} from './utils/script-utils';
1717
import {createGoogleMapsDeckOverlay} from './utils/google-maps-utils';
18+
import {createMapLibreDeckOverlay} from './utils/maplibre-utils';
1819

1920
import {addSupportComponents} from '../lib/components/index';
2021

@@ -127,7 +128,6 @@ function missingProps(oldProps, newProps) {
127128
}
128129

129130
function createStandaloneFromProvider({
130-
mapProvider,
131131
props,
132132
mapboxApiKey,
133133
googleMapsKey,
@@ -161,7 +161,7 @@ function createStandaloneFromProvider({
161161
container
162162
};
163163

164-
switch (mapProvider) {
164+
switch (props.mapProvider) {
165165
case 'mapbox':
166166
log.info('Using Mapbox base maps')();
167167
return new DeckGL({
@@ -185,6 +185,12 @@ function createStandaloneFromProvider({
185185
...props,
186186
googleMapsKey
187187
});
188+
case 'maplibre':
189+
log.info('Using MapLibre')();
190+
return createMapLibreDeckOverlay({
191+
...sharedProps,
192+
...props
193+
});
188194
default:
189195
log.info('No recognized map provider specified')();
190196
return new DeckGL({
@@ -240,10 +246,8 @@ function createDeck({
240246
const layersToLoad = missingProps(oldLayers, convertedLayers);
241247
const widgetsToLoad = missingProps(oldWidgets, convertedWidgets);
242248
const getTooltip = makeTooltip(tooltip);
243-
const {mapProvider} = props;
244249

245250
deckgl = createStandaloneFromProvider({
246-
mapProvider,
247251
props,
248252
mapboxApiKey,
249253
googleMapsKey,
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
/* eslint-disable import/namespace */
6+
import {log} from '@deck.gl/core';
7+
import {MapboxOverlay} from '@deck.gl/mapbox';
8+
import maplibregl from 'maplibre-gl';
9+
10+
export function createMapLibreDeckOverlay({
11+
container,
12+
onClick,
13+
onHover,
14+
onResize,
15+
onViewStateChange,
16+
onDragStart,
17+
onDrag,
18+
onDragEnd,
19+
onError,
20+
getTooltip,
21+
layers,
22+
mapStyle = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
23+
initialViewState = {latitude: 0, longitude: 0, zoom: 1},
24+
mapProjection = 'mercator'
25+
}) {
26+
log.info('Using MapLibre')();
27+
28+
// Create MapLibre map
29+
const map = new maplibregl.Map({
30+
container,
31+
style: mapStyle,
32+
center: [initialViewState.longitude, initialViewState.latitude],
33+
zoom: initialViewState.zoom,
34+
pitch: initialViewState.pitch || 0,
35+
bearing: initialViewState.bearing || 0
36+
});
37+
38+
// Create deck overlay with interleaved mode for globe
39+
const deckOverlay = new MapboxOverlay({
40+
interleaved: mapProjection === 'globe',
41+
layers,
42+
getTooltip,
43+
onClick,
44+
onHover,
45+
onDragStart,
46+
onDrag,
47+
onDragEnd,
48+
onError
49+
});
50+
51+
// Set up projection and add overlay when map loads
52+
map.on('load', () => {
53+
if (mapProjection === 'globe') {
54+
map.setProjection({type: 'globe'});
55+
}
56+
map.addControl(deckOverlay);
57+
map.addControl(new maplibregl.NavigationControl());
58+
});
59+
60+
// Handle view state change events
61+
if (onViewStateChange) {
62+
map.on('move', () => {
63+
const center = map.getCenter();
64+
const viewState = {
65+
longitude: center.lng,
66+
latitude: center.lat,
67+
zoom: map.getZoom(),
68+
pitch: map.getPitch(),
69+
bearing: map.getBearing()
70+
};
71+
onViewStateChange({viewState});
72+
});
73+
}
74+
75+
// Handle resize events
76+
if (onResize) {
77+
map.on('resize', () => {
78+
const canvas = map.getCanvas();
79+
onResize({width: canvas.width, height: canvas.height});
80+
});
81+
}
82+
83+
// Expose setProps method to update layers
84+
deckOverlay.setProps = function (props) {
85+
if (props.layers) {
86+
deckOverlay.setProps({layers: props.layers});
87+
}
88+
};
89+
90+
return deckOverlay;
91+
}

modules/jupyter-widget/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
{"path": "../google-maps"},
1515
{"path": "../json"},
1616
{"path": "../layers"},
17+
{"path": "../mapbox"},
1718
{"path": "../mesh-layers"}
1819
]
1920
}

0 commit comments

Comments
 (0)