From b1dd70648192e3a73794cbbf95e6eb7123ab3143 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Mon, 15 Jun 2026 15:21:43 -0700 Subject: [PATCH 1/2] example: pydeck playground --- examples/pydeck/index.html | 171 ++++++++++++++++++ examples/pydeck/package.json | 24 +++ examples/pydeck/src/app.tsx | 239 ++++++++++++++++++++++++++ examples/pydeck/src/file-drop.tsx | 182 ++++++++++++++++++++ examples/pydeck/src/icons.tsx | 77 +++++++++ examples/pydeck/src/pyodide-worker.ts | 180 +++++++++++++++++++ examples/pydeck/src/pyodide.ts | 107 ++++++++++++ examples/pydeck/vite.config.js | 9 + 8 files changed, 989 insertions(+) create mode 100644 examples/pydeck/index.html create mode 100644 examples/pydeck/package.json create mode 100644 examples/pydeck/src/app.tsx create mode 100644 examples/pydeck/src/file-drop.tsx create mode 100644 examples/pydeck/src/icons.tsx create mode 100644 examples/pydeck/src/pyodide-worker.ts create mode 100644 examples/pydeck/src/pyodide.ts create mode 100644 examples/pydeck/vite.config.js diff --git a/examples/pydeck/index.html b/examples/pydeck/index.html new file mode 100644 index 00000000000..3de3e3ffe4a --- /dev/null +++ b/examples/pydeck/index.html @@ -0,0 +1,171 @@ + + + + + pydeck Playground + + + + + +
+ + + diff --git a/examples/pydeck/package.json b/examples/pydeck/package.json new file mode 100644 index 00000000000..163795728b6 --- /dev/null +++ b/examples/pydeck/package.json @@ -0,0 +1,24 @@ +{ + "name": "deckgl-examples-playground", + "version": "0.0.0", + "private": true, + "license": "MIT", + "scripts": { + "start": "vite --open", + "start-local": "vite --config ../vite.config.local.mjs", + "build": "vite build" + }, + "dependencies": { + "deck.gl": "^9.0.0", + "monaco-editor": "^0.39.0", + "@monaco-editor/react": "^4.4.6", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-dropzone": "^15.0.0", + "react-virtualized-auto-sizer": "^1.0.2" + }, + "devDependencies": { + "typescript": "^4.6.0", + "vite": "^7.3.3" + } +} diff --git a/examples/pydeck/src/app.tsx b/examples/pydeck/src/app.tsx new file mode 100644 index 00000000000..c0bdd8b3cce --- /dev/null +++ b/examples/pydeck/src/app.tsx @@ -0,0 +1,239 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import * as React from 'react'; +import {useEffect, useRef, useState} from 'react'; +import {createRoot} from 'react-dom/client'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import Editor from '@monaco-editor/react'; +import {PlayIcon, SpinnerIcon} from './icons'; +import {FileDrop, type FileDropHandle} from './file-drop'; +import {pyodide, type PreloadedFileConfig, type WorkerStatus} from './pyodide'; + +const DEFAULT_SAMPLE = `\ +import pydeck +import pandas as pd + +UK_ACCIDENTS_DATA = pd.read_csv(uploaded_files["heatmap-data.csv"]) + +layer = pydeck.Layer( + 'HexagonLayer', + UK_ACCIDENTS_DATA, + get_position=['lng', 'lat'], + auto_highlight=True, + elevation_scale=50, + pickable=True, + elevation_range=[0, 3000], + extruded=True, + coverage=1) + +# Set the viewport location +view_state = pydeck.ViewState( + longitude=-1.415, + latitude=52.2323, + zoom=6, + min_zoom=5, + max_zoom=15, + pitch=40.5, + bearing=-27.36) + +# Combined all of it and render a viewport +r = pydeck.Deck(layers=[layer], initial_view_state=view_state) +r.to_html(as_string=True) +`; + +const PRELOADED_FILES: PreloadedFileConfig[] = [ + { + name: 'heatmap-data.csv', + url: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv' + } +]; + +type AppStatus = WorkerStatus | 'Loading files...' | null; + +export function App({preloadedFiles}: {preloadedFiles?: PreloadedFileConfig[]}) { + const editorRef = useRef(null); + const monacoRef = useRef(null); + const errorZoneRef = useRef(null); + const fileDropRef = useRef(null); + const [output, setOutput] = useState({content: 'Loading Python...', isHtml: false}); + const [status, setStatus] = useState('Loading packages...'); + + const setRenderedOutput = (value: unknown) => { + const content = String(value); + const isHtml = content.startsWith(''); + + setOutput({content, isHtml}); + }; + + const clearErrorWidget = () => { + if (!editorRef.current || !errorZoneRef.current) { + return; + } + + editorRef.current.changeViewZones(accessor => { + accessor.removeZone(errorZoneRef.current); + }); + errorZoneRef.current = null; + }; + + const showErrorWidget = (lineNumber: number, message: string) => { + if (!editorRef.current || !monacoRef.current) { + return; + } + + clearErrorWidget(); + + const domNode = document.createElement('div'); + domNode.className = 'inline-error'; + const contentNode = document.createElement('div'); + domNode.append(contentNode); + contentNode.textContent = message; + + editorRef.current.changeViewZones(accessor => { + errorZoneRef.current = accessor.addZone({ + afterLineNumber: lineNumber, + domNode, + heightInLines: message.split('\n').length + 1 + }); + }); + editorRef.current.revealLineInCenter(lineNumber); + }; + + useEffect(() => { + let cancelled = false; + + pyodide + .getVersion() + .then(result => { + if (!cancelled) { + setRenderedOutput(result); + } + }) + .catch(error => { + if (!cancelled) { + setRenderedOutput(error instanceof Error ? error.message : String(error)); + } + }); + + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + let cancelled = false; + + pyodide.ready + .then(() => { + if (!cancelled) { + setStatus(currentStatus => + currentStatus === 'Loading packages...' ? null : currentStatus + ); + } + }) + .catch(error => { + if (!cancelled) { + setRenderedOutput(error instanceof Error ? error.message : String(error)); + } + }); + + return () => { + cancelled = true; + }; + }, []); + + const runCode = async () => { + const source = editorRef.current?.getValue() ?? ''; + clearErrorWidget(); + + if (status) { + return; + } + + try { + if (fileDropRef.current?.isLoading) { + setStatus('Loading files...'); + await fileDropRef.current.loading; + } + + const result = await pyodide.runPython(source, nextStatus => { + setStatus(nextStatus); + }); + setRenderedOutput(result); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const lineMatch = message.match(/File "", line (\d+)/); + + if (lineMatch) { + const errorStart = message.slice(lineMatch.index).search(/\n\w/); + const errorMessage = message.slice(lineMatch.index! + errorStart + 1); + showErrorWidget(Number(lineMatch[1]), errorMessage); + } + + setRenderedOutput(message); + } finally { + setStatus(null); + } + }; + + return ( + <> + {/* Left Pane: Monaco Editor and Template Selector */} +
+
+ pydeck Playground + +
+
+ + {({width, height}) => ( + { + editorRef.current = editor; + monacoRef.current = monaco; + }} + theme="light" + options={{scrollBeyondLastLine: false, minimap: {enabled: false}}} + /> + )} + +
+ +
+ + {/* Right Pane: Output */} +
+ {output.isHtml ? ( +