Skip to content
Draft
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 docs/developer-guide/views.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,8 @@ new Deck({
});
```

For a larger standalone TypeScript example, see the multi-canvas cities test app in the [repository](https://github.com/visgl/deck.gl/tree/master/test/apps/multi-canvas-cities).

### Using Multiple Views with View States

When using multiple views, each `View` can either have its own independent view state, or share the same view state as other views. To define the view state of a specific view, add a key to the `viewState` object that matches its view id.
Expand Down
177 changes: 177 additions & 0 deletions test/apps/multi-canvas-cities/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {Deck, MapView} from '@deck.gl/core';
import {ScatterplotLayer} from '@deck.gl/layers';
import {BasemapLayer} from '@deck.gl-community/basemap-layers';
import {ZoomWidget} from '@deck.gl/widgets';
import '@deck.gl/widgets/stylesheet.css';

type Landmark = {
id: string;
cityId: string;
name: string;
position: [number, number];
};

type CityPanel = {
id: string;
title: string;
subtitle: string;
mapStyle: string;
viewState: {
longitude: number;
latitude: number;
zoom: number;
pitch: number;
bearing: number;
};
landmarks: Landmark[];
};

const CITY_PANELS: CityPanel[] = [
{
id: 'new-york',
title: 'New York',
subtitle: 'Midtown lights and waterfront routes',
mapStyle: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
viewState: {longitude: -73.9857, latitude: 40.7484, zoom: 10.8, pitch: 35, bearing: -12},
landmarks: [
{id: 'times-square', cityId: 'new-york', name: 'Times Square', position: [-73.9851, 40.758]},
{id: 'central-park', cityId: 'new-york', name: 'Central Park', position: [-73.9712, 40.7831]},
{
id: 'brooklyn-bridge',
cityId: 'new-york',
name: 'Brooklyn Bridge',
position: [-73.9969, 40.7061]
}
]
},
{
id: 'london',
title: 'London',
subtitle: 'River crossings and west end clusters',
mapStyle: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
viewState: {longitude: -0.1276, latitude: 51.5072, zoom: 10.8, pitch: 40, bearing: 18},
landmarks: [
{id: 'soho', cityId: 'london', name: 'Soho', position: [-0.1337, 51.5138]},
{id: 'tower-bridge', cityId: 'london', name: 'Tower Bridge', position: [-0.0754, 51.5055]},
{id: 'greenwich', cityId: 'london', name: 'Greenwich', position: [0.0005, 51.4826]}
]
},
{
id: 'tokyo',
title: 'Tokyo',
subtitle: 'Station density across the eastern core',
mapStyle: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
viewState: {longitude: 139.7588, latitude: 35.6762, zoom: 10.7, pitch: 45, bearing: -22},
landmarks: [
{id: 'shibuya', cityId: 'tokyo', name: 'Shibuya', position: [139.7016, 35.6595]},
{id: 'tokyo-station', cityId: 'tokyo', name: 'Tokyo Station', position: [139.7671, 35.6812]},
{id: 'asakusa', cityId: 'tokyo', name: 'Asakusa', position: [139.7967, 35.7148]}
]
},
{
id: 'sydney',
title: 'Sydney',
subtitle: 'Harbor landmarks with coastal spillover',
mapStyle: 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json',
viewState: {longitude: 151.2093, latitude: -33.8688, zoom: 10.9, pitch: 42, bearing: 24},
landmarks: [
{id: 'opera-house', cityId: 'sydney', name: 'Opera House', position: [151.2153, -33.8568]},
{id: 'bondi', cityId: 'sydney', name: 'Bondi Beach', position: [151.2743, -33.8915]},
{id: 'newtown', cityId: 'sydney', name: 'Newtown', position: [151.179, -33.8981]}
]
}
];

const VIEWS = CITY_PANELS.map(
city =>
new MapView({
id: city.id,
canvasId: city.id,
controller: true
})
);

const INITIAL_VIEW_STATE = Object.fromEntries(CITY_PANELS.map(city => [city.id, city.viewState]));
const CITY_TITLES = Object.fromEntries(CITY_PANELS.map(city => [city.id, city.title])) as Record<
string,
string
>;
const hoverStatus = document.getElementById('hover-status');
let deck: Deck;
let hoveredLandmark: Landmark | null = null;

function getLayers() {
return CITY_PANELS.flatMap(city => [
new BasemapLayer({
id: `${city.id}-basemap`,
style: city.mapStyle
}),
new ScatterplotLayer<Landmark>({
id: `${city.id}-landmarks`,
data: city.landmarks,
pickable: true,
autoHighlight: true,
parameters: {depthTest: false},
radiusUnits: 'pixels',
radiusMinPixels: 18,
radiusMaxPixels: 36,
stroked: true,
lineWidthMinPixels: 3,
getPosition: landmark => landmark.position,
getRadius: landmark => (hoveredLandmark?.id === landmark.id ? 28 : 20),
getFillColor: landmark =>
hoveredLandmark?.id === landmark.id
? [255, 215, 110]
: hoveredLandmark?.cityId === landmark.cityId
? [255, 122, 89]
: [84, 196, 255],
getLineColor: [255, 255, 255],
onHover: info => {
hoveredLandmark = (info.object as Landmark) || null;
updateHoverStatus();
deck.setProps({layers: getLayers()});
}
})
]);
}

function updateHoverStatus() {
if (!hoverStatus) {
return;
}

hoverStatus.textContent = hoveredLandmark
? `${hoveredLandmark.name} in ${CITY_TITLES[hoveredLandmark.cityId]}`
: 'Hover any highlighted landmark';
}

const mapGrid = document.getElementById('map-grid') as HTMLDivElement | null;
if (!mapGrid) {
throw new Error('Map grid not found');
}

deck = new Deck({
parent: mapGrid,
_canvases: CITY_PANELS.map(city => city.id),
views: VIEWS,
initialViewState: INITIAL_VIEW_STATE,
layers: getLayers(),
widgets: CITY_PANELS.map(
city => new ZoomWidget({id: `${city.id}-zoom`, viewId: city.id, placement: 'top-right'})
),
getTooltip: ({object}) => {
const landmark = object as Landmark | null;
return landmark ? `${landmark.name}\n${CITY_TITLES[landmark.cityId]}` : null;
},
layerFilter: ({layer, viewport}) => Boolean(viewport && layer.id.startsWith(`${viewport.id}-`))
});

updateHoverStatus();

export function finalize() {
deck.finalize();
}
148 changes: 148 additions & 0 deletions test/apps/multi-canvas-cities/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>deck.gl Multi-Canvas Cities</title>
<style>
* {
box-sizing: border-box;
}

html,
body {
height: 100%;
overflow: hidden;
}

body {
margin: 0;
font-family: Inter, system-ui, sans-serif;
background: #081018;
color: #f5f7fb;
}

#app {
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
gap: 16px;
height: 100%;
max-width: 1240px;
margin: 0 auto;
padding: 24px;
}

h1 {
margin: 10px 0 8px;
font-size: 36px;
line-height: 1.05;
}

p {
max-width: 760px;
margin: 0;
font-size: 16px;
line-height: 1.5;
opacity: 0.86;
}

.eyebrow {
font-size: 12px;
letter-spacing: 0.4px;
opacity: 0.7;
text-transform: uppercase;
}

.status {
min-height: 48px;
padding: 12px 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.06);
font-weight: 700;
}

#map-grid {
position: relative;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr));
gap: 16px;
min-height: 0;
}

.deck-events-root {
position: relative;
min-height: 0;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
background: #111923;
}

canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}

.panel-label {
position: absolute;
z-index: 2;
top: 12px;
left: 12px;
max-width: 220px;
padding: 10px 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
background: rgba(8, 16, 24, 0.72);
pointer-events: none;
}

.panel-label strong,
.panel-label span {
display: block;
}

.panel-label strong {
font-size: 18px;
}

.panel-label span {
margin-top: 3px;
font-size: 13px;
line-height: 1.35;
opacity: 0.82;
}
</style>
</head>
<body>
<main id="app">
<header>
<div class="eyebrow">deck.gl multi-canvas</div>
<h1>Four live city views, one Deck instance</h1>
<p>Each panel has its own DOM canvas, event root, controller, view-bound widget, and filtered layer stack.</p>
</header>
<div id="hover-status" class="status"></div>
<div id="map-grid">
<section class="deck-events-root">
<canvas id="new-york"></canvas>
<div class="panel-label"><strong>New York</strong><span>Midtown lights and waterfront routes</span></div>
</section>
<section class="deck-events-root">
<canvas id="london"></canvas>
<div class="panel-label"><strong>London</strong><span>River crossings and west end clusters</span></div>
</section>
<section class="deck-events-root">
<canvas id="tokyo"></canvas>
<div class="panel-label"><strong>Tokyo</strong><span>Station density across the eastern core</span></div>
</section>
<section class="deck-events-root">
<canvas id="sydney"></canvas>
<div class="panel-label"><strong>Sydney</strong><span>Harbor landmarks with coastal spillover</span></div>
</section>
</div>
</main>
<script type="module" src="app.ts"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions test/apps/multi-canvas-cities/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "deckgl-example-multi-canvas-cities",
"version": "0.0.0",
"private": true,
"type": "module",
"license": "MIT",
"scripts": {
"start": "vite --open",
"start-local": "vite --config ../vite.config.local.mjs",
"build": "vite build"
},
"dependencies": {
"@deck.gl-community/basemap-layers": "^9.3.1",
"@deck.gl/core": "^9.3.0-alpha.11",
"@deck.gl/layers": "^9.3.0-alpha.11",
"@deck.gl/widgets": "^9.3.0-alpha.11"
},
"devDependencies": {
"vite": "^7.3.1"
}
}
Loading