Skip to content

Commit eaf4148

Browse files
committed
example: pydeck playground (#10383)
1 parent 6e9486e commit eaf4148

17 files changed

Lines changed: 1644 additions & 3 deletions

examples/pydeck/index.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>pydeck Playground</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<link href="https://unpkg.com/deck.gl@^9.0.0/dist/stylesheet.css" rel='stylesheet' />
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
</body>
12+
<script type="module">
13+
import {renderToDOM} from './src/index.tsx';
14+
renderToDOM(document.getElementById('app'));
15+
</script>
16+
</html>

examples/pydeck/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "deckgl-examples-playground",
3+
"version": "0.0.0",
4+
"private": true,
5+
"license": "MIT",
6+
"scripts": {
7+
"start": "vite --open",
8+
"start-local": "vite --config ../vite.config.local.mjs",
9+
"build": "vite build"
10+
},
11+
"dependencies": {
12+
"@monaco-editor/react": "^4.4.6",
13+
"monaco-editor": "^0.39.0",
14+
"react": "^18.0.0",
15+
"react-dom": "^18.0.0",
16+
"react-dropzone": "^15.0.0",
17+
"react-virtualized-auto-sizer": "^1.0.2",
18+
"split.js": "^1.6.5",
19+
"styled-components": "^5.3"
20+
},
21+
"devDependencies": {
22+
"typescript": "^4.6.0",
23+
"vite": "^7.3.3"
24+
}
25+
}

examples/pydeck/src/app.tsx

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
import * as React from 'react';
6+
import {useEffect, useMemo, useRef, useState} from 'react';
7+
import Split from 'split.js';
8+
import {Editor, type EditorHandle} from './editor';
9+
import {PlayIcon, SpinnerIcon} from './icons';
10+
import {FileDrop, type FileDropHandle} from './file-drop';
11+
import {
12+
PyodideClient,
13+
type PreloadedFileConfig,
14+
type RunUpdate,
15+
type WorkerStatus
16+
} from './pyodide';
17+
import {
18+
EditorPane,
19+
GlobalStyle,
20+
HtmlPane,
21+
LeftPane,
22+
OutputEmpty,
23+
RightPane,
24+
Root,
25+
RunButton,
26+
TextOutput,
27+
TextOutputPart as TextOutputPartView,
28+
TextPane,
29+
Toolbar,
30+
ToolbarLink,
31+
ToolbarTitle
32+
} from './styles';
33+
34+
const DEFAULT_SAMPLE = `\
35+
import pydeck
36+
import pandas as pd
37+
38+
UK_ACCIDENTS_DATA = pd.read_csv(uploaded_files["heatmap-data.csv"])
39+
print(UK_ACCIDENTS_DATA)
40+
41+
layer = pydeck.Layer(
42+
'HexagonLayer',
43+
UK_ACCIDENTS_DATA,
44+
get_position=['lng', 'lat'],
45+
auto_highlight=True,
46+
elevation_scale=50,
47+
pickable=True,
48+
elevation_range=[0, 3000],
49+
extruded=True,
50+
coverage=1)
51+
52+
# Set the viewport location
53+
view_state = pydeck.ViewState(
54+
longitude=-1.415,
55+
latitude=52.2323,
56+
zoom=6,
57+
min_zoom=5,
58+
max_zoom=15,
59+
pitch=40.5,
60+
bearing=-27.36)
61+
62+
# Combined all of it and render a viewport
63+
r = pydeck.Deck(layers=[layer], initial_view_state=view_state)
64+
r.to_html('output.html')
65+
`;
66+
67+
const PRELOADED_FILES: PreloadedFileConfig[] = [
68+
{
69+
name: 'heatmap-data.csv',
70+
url: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv'
71+
}
72+
];
73+
74+
type AppStatus = WorkerStatus | 'Loading files...' | null;
75+
type TextOutputPart = {
76+
type: 'stderr' | 'stdout';
77+
text: string;
78+
};
79+
type OutputState = {
80+
html: string;
81+
textParts: TextOutputPart[];
82+
};
83+
84+
export default function App({
85+
preloadedFiles = PRELOADED_FILES,
86+
workerUrl
87+
}: {
88+
preloadedFiles?: PreloadedFileConfig[];
89+
workerUrl: string | URL;
90+
}) {
91+
const pyodide = useMemo(() => new PyodideClient({workerUrl}), [workerUrl]);
92+
const editorRef = useRef<EditorHandle>(null);
93+
const fileDropRef = useRef<FileDropHandle>(null);
94+
const rootRef = useRef<HTMLDivElement | null>(null);
95+
const [output, setOutput] = useState<OutputState>({
96+
html: '',
97+
textParts: [{text: 'Loading Python...', type: 'stdout'}]
98+
});
99+
const [status, setStatus] = useState<AppStatus>('Loading packages...');
100+
101+
const setTextOutput = (value: unknown) => {
102+
setOutput({html: '', textParts: [{text: String(value), type: 'stdout'}]});
103+
};
104+
105+
const formatRunTime = (timeElapsed: number) => `\nRun time: ${timeElapsed.toFixed(1)} ms`;
106+
107+
const applyRunUpdate = (update: RunUpdate) => {
108+
if (update.status) {
109+
setStatus(update.status);
110+
}
111+
112+
if (!update.stdout && !update.stderr) {
113+
return;
114+
}
115+
116+
setOutput(currentOutput => {
117+
const nextParts = [...currentOutput.textParts];
118+
119+
const appendPart = (type: 'stderr' | 'stdout', text: string) => {
120+
if (!text) {
121+
return;
122+
}
123+
124+
const lastPart = nextParts[nextParts.length - 1];
125+
if (lastPart && lastPart.type === type) {
126+
lastPart.text += text;
127+
} else {
128+
nextParts.push({text, type});
129+
}
130+
};
131+
132+
appendPart('stdout', update.stdout ?? '');
133+
appendPart('stderr', update.stderr ?? '');
134+
135+
return {...currentOutput, textParts: nextParts};
136+
});
137+
};
138+
139+
const setRunOutput = async (value: {files: Blob[]; result: string; timeElapsed: number}) => {
140+
const htmlFile = value.files.find(file => file.type === 'text/html');
141+
const html = htmlFile
142+
? await htmlFile.text()
143+
: value.result.startsWith('<!DOCTYPE html>')
144+
? value.result
145+
: '';
146+
const timeText = formatRunTime(value.timeElapsed);
147+
148+
setOutput(currentOutput => ({
149+
html,
150+
textParts:
151+
currentOutput.textParts.length > 0
152+
? [...currentOutput.textParts, {text: timeText, type: 'stdout'}]
153+
: html
154+
? [{text: timeText, type: 'stdout'}]
155+
: [{text: `${value.result}${timeText}`, type: 'stdout'}]
156+
}));
157+
};
158+
159+
useEffect(() => {
160+
let cancelled = false;
161+
162+
pyodide
163+
.getVersion()
164+
.then(result => {
165+
if (!cancelled) {
166+
setTextOutput(result);
167+
}
168+
})
169+
.catch(error => {
170+
if (!cancelled) {
171+
setTextOutput(error instanceof Error ? error.message : String(error));
172+
}
173+
});
174+
175+
return () => {
176+
cancelled = true;
177+
};
178+
}, []);
179+
180+
useEffect(() => {
181+
if (!rootRef.current) {
182+
return undefined;
183+
}
184+
185+
const leftPane = rootRef.current.querySelector<HTMLDivElement>('#left-pane');
186+
const rightPane = rootRef.current.querySelector<HTMLDivElement>('#right-pane');
187+
const htmlPane = rootRef.current.querySelector<HTMLElement>('.html-pane');
188+
const textPane = rootRef.current.querySelector<HTMLElement>('.text-pane');
189+
190+
if (!leftPane || !rightPane || !htmlPane || !textPane) {
191+
return undefined;
192+
}
193+
194+
const mainSplit = Split([leftPane, rightPane], {
195+
sizes: [40, 60],
196+
minSize: [320, 320],
197+
gutterSize: 4
198+
});
199+
const outputSplit = Split([htmlPane, textPane], {
200+
direction: 'vertical',
201+
sizes: [50, 50],
202+
minSize: [160, 160],
203+
gutterSize: 4
204+
});
205+
206+
return () => {
207+
outputSplit.destroy();
208+
mainSplit.destroy();
209+
};
210+
}, []);
211+
212+
useEffect(() => {
213+
let cancelled = false;
214+
215+
pyodide.ready
216+
.then(() => {
217+
if (!cancelled) {
218+
setStatus(currentStatus =>
219+
currentStatus === 'Loading packages...' ? null : currentStatus
220+
);
221+
}
222+
})
223+
.catch(error => {
224+
if (!cancelled) {
225+
setTextOutput(error instanceof Error ? error.message : String(error));
226+
}
227+
});
228+
229+
return () => {
230+
cancelled = true;
231+
};
232+
}, []);
233+
234+
const runCode = async () => {
235+
const source = editorRef.current?.getValue() ?? '';
236+
editorRef.current?.showError(null);
237+
238+
if (status) {
239+
return;
240+
}
241+
242+
try {
243+
if (fileDropRef.current?.isLoading) {
244+
setStatus('Loading files...');
245+
await fileDropRef.current.loading;
246+
}
247+
248+
setOutput(currentOutput => ({...currentOutput, html: '', textParts: []}));
249+
const result = await pyodide.runPython(source, applyRunUpdate);
250+
await setRunOutput(result);
251+
} catch (error) {
252+
const message = error instanceof Error ? error.message : String(error);
253+
const lineMatch = message.match(/File "<exec>", line (\d+)/);
254+
255+
if (lineMatch) {
256+
const errorStart = message.slice(lineMatch.index).search(/\n\w/);
257+
const errorMessage = message.slice(lineMatch.index! + errorStart + 1);
258+
editorRef.current?.showError({lineNumber: Number(lineMatch[1]), message: errorMessage});
259+
}
260+
261+
setTextOutput(message);
262+
} finally {
263+
setStatus(null);
264+
}
265+
};
266+
267+
return (
268+
<>
269+
<GlobalStyle />
270+
<Root id="root" ref={rootRef}>
271+
{/* Left Pane: Monaco Editor and Template Selector */}
272+
<LeftPane>
273+
<Toolbar>
274+
<ToolbarTitle>
275+
<strong>pydeck playground</strong>
276+
<ToolbarLink
277+
href="https://deckgl.readthedocs.io/en/latest/"
278+
rel="noreferrer"
279+
target="_blank"
280+
>
281+
Docs
282+
</ToolbarLink>
283+
</ToolbarTitle>
284+
<RunButton
285+
disabled={status !== null}
286+
onClick={() => {
287+
void runCode();
288+
}}
289+
type="button"
290+
>
291+
{status ? (
292+
<>
293+
<SpinnerIcon />
294+
{status}
295+
</>
296+
) : (
297+
<PlayIcon />
298+
)}
299+
</RunButton>
300+
</Toolbar>
301+
<EditorPane>
302+
<Editor defaultValue={DEFAULT_SAMPLE} ref={editorRef} />
303+
</EditorPane>
304+
<FileDrop engine={pyodide} preloadedFiles={preloadedFiles} ref={fileDropRef} />
305+
</LeftPane>
306+
307+
{/* Right Pane: Output */}
308+
<RightPane>
309+
<HtmlPane>
310+
{output.html ? (
311+
<iframe srcDoc={output.html} title="Python HTML output" />
312+
) : (
313+
<OutputEmpty>No HTML output</OutputEmpty>
314+
)}
315+
</HtmlPane>
316+
<TextPane>
317+
<TextOutput>
318+
{output.textParts.map((part, index) => (
319+
<TextOutputPartView $type={part.type} key={index}>
320+
{part.text}
321+
</TextOutputPartView>
322+
))}
323+
</TextOutput>
324+
</TextPane>
325+
</RightPane>
326+
</Root>
327+
</>
328+
);
329+
}

0 commit comments

Comments
 (0)