From a29515ce5c0f73ae83b8f248c99b22b3d3f94f49 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sun, 12 May 2024 10:24:41 -0700 Subject: [PATCH 1/6] add(examples): react widgets get started Signed-off-by: Chris Gervang --- .../get-started/pure-js/widgets/package.json | 2 +- examples/get-started/react/widgets/README.md | 17 ++++ examples/get-started/react/widgets/app.jsx | 87 +++++++++++++++++++ examples/get-started/react/widgets/index.html | 12 +++ .../get-started/react/widgets/package.json | 20 +++++ 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 examples/get-started/react/widgets/README.md create mode 100644 examples/get-started/react/widgets/app.jsx create mode 100644 examples/get-started/react/widgets/index.html create mode 100644 examples/get-started/react/widgets/package.json diff --git a/examples/get-started/pure-js/widgets/package.json b/examples/get-started/pure-js/widgets/package.json index f025ce2262b..3c762bf6815 100644 --- a/examples/get-started/pure-js/widgets/package.json +++ b/examples/get-started/pure-js/widgets/package.json @@ -1,5 +1,5 @@ { - "name": "deckgl-example-pure-js-basic", + "name": "deckgl-example-pure-js-widgets", "version": "0.0.0", "private": true, "license": "MIT", diff --git a/examples/get-started/react/widgets/README.md b/examples/get-started/react/widgets/README.md new file mode 100644 index 00000000000..277a76dc22d --- /dev/null +++ b/examples/get-started/react/widgets/README.md @@ -0,0 +1,17 @@ +## Example: Use deck.gl with widgets + +Uses [Vite](https://vitejs.dev/) to bundle and serve files. + +## Usage + +To install dependencies: + +```bash +npm install +# or +yarn +``` + +Commands: +* `npm start` is the development target, to serve the app and hot reload. +* `npm run build` is the production target, to create the final bundle and write to disk. diff --git a/examples/get-started/react/widgets/app.jsx b/examples/get-started/react/widgets/app.jsx new file mode 100644 index 00000000000..2331270dd3e --- /dev/null +++ b/examples/get-started/react/widgets/app.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import {createRoot} from 'react-dom/client'; +import DeckGL, {GeoJsonLayer, ArcLayer} from 'deck.gl'; +import { + CompassWidget, + ZoomWidget, + FullscreenWidget, + DarkGlassTheme, + LightGlassTheme +} from '@deck.gl/widgets'; +import '@deck.gl/widgets/stylesheet.css'; + +/* global window */ +const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); +const widgetTheme = prefersDarkScheme.matches ? DarkGlassTheme : LightGlassTheme; + +// source: Natural Earth http://www.naturalearthdata.com/ via geojson.xyz +const COUNTRIES = + 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_admin_0_scale_rank.geojson'; //eslint-disable-line +const AIR_PORTS = + 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson'; + +const INITIAL_VIEW_STATE = { + latitude: 51.47, + longitude: 0.45, + zoom: 4, + bearing: 0, + pitch: 30 +}; + +function Root() { + const onClick = info => { + if (info.object) { + // eslint-disable-next-line + alert(`${info.object.properties.name} (${info.object.properties.abbrev})`); + } + }; + + return ( + + + 11 - f.properties.scalerank} + getFillColor={[200, 0, 80, 180]} + pickable={true} + autoHighlight={true} + onClick={onClick} + /> + d.features.filter(f => f.properties.scalerank < 4)} + getSourcePosition={f => [-0.4531566, 51.4709959]} + getTargetPosition={f => f.geometry.coordinates} + getSourceColor={[0, 128, 200]} + getTargetColor={[200, 0, 80]} + getWidth={1} + /> + + ); +} + +/* global document */ +const container = document.body.appendChild(document.createElement('div')); +createRoot(container).render(); diff --git a/examples/get-started/react/widgets/index.html b/examples/get-started/react/widgets/index.html new file mode 100644 index 00000000000..38a008bc464 --- /dev/null +++ b/examples/get-started/react/widgets/index.html @@ -0,0 +1,12 @@ + + + + + deck.gl Example + + + + + diff --git a/examples/get-started/react/widgets/package.json b/examples/get-started/react/widgets/package.json new file mode 100644 index 00000000000..eb4458f7ce7 --- /dev/null +++ b/examples/get-started/react/widgets/package.json @@ -0,0 +1,20 @@ +{ + "name": "deckgl-example-react-widgets", + "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", + "@deck.gl/widgets": "^9.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "devDependencies": { + "vite": "^4.0.0" + } +} From 8dc72e587ea712963e90eb0fd95a3ca057394ea8 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sat, 17 Aug 2024 09:54:23 -0700 Subject: [PATCH 2/6] remove imperative widget usage from docs Signed-off-by: Chris Gervang --- docs/api-reference/core/widget.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/api-reference/core/widget.md b/docs/api-reference/core/widget.md index 6545c6fedc2..1b95db1d98a 100644 --- a/docs/api-reference/core/widget.md +++ b/docs/api-reference/core/widget.md @@ -12,7 +12,7 @@ You may find many ready-to-use widgets in the `@deck.gl/widgets` module. A widget is expected to implement the `Widget` interface. Here is a custom widget that shows a spinner while layers are loading: ```ts -import {Widget} from '@deck.gl/core'; +import {Deck, Widget} from '@deck.gl/core'; class LoadingIndicator implements Widget { element?: HTMLDivElement; @@ -43,7 +43,9 @@ class LoadingIndicator implements Widget { } } -deckgl.addWidget(new LoadingIndicator({size: 48})); +new Deck({ + widgets=[new LoadingIndicator({size: 48})] +}); ``` ## Widget Interface From efbeefca94425c5f4777e9e5ae6802b3e17a0d83 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sat, 17 Aug 2024 09:57:23 -0700 Subject: [PATCH 3/6] use vanilla js layers Signed-off-by: Chris Gervang --- examples/get-started/react/widgets/app.jsx | 73 ++++++++++++---------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/examples/get-started/react/widgets/app.jsx b/examples/get-started/react/widgets/app.jsx index 2331270dd3e..1fa50d09c6a 100644 --- a/examples/get-started/react/widgets/app.jsx +++ b/examples/get-started/react/widgets/app.jsx @@ -45,40 +45,45 @@ function Root() { new CompassWidget({style: widgetTheme}), new FullscreenWidget({style: widgetTheme}) ]} - > - - 11 - f.properties.scalerank} - getFillColor={[200, 0, 80, 180]} - pickable={true} - autoHighlight={true} - onClick={onClick} - /> - d.features.filter(f => f.properties.scalerank < 4)} - getSourcePosition={f => [-0.4531566, 51.4709959]} - getTargetPosition={f => f.geometry.coordinates} - getSourceColor={[0, 128, 200]} - getTargetColor={[200, 0, 80]} - getWidth={1} - /> - + layers={[ + new GeoJsonLayer({ + id: 'base-map', + data: COUNTRIES, + // Styles + stroked: true, + filled: true, + lineWidthMinPixels: 2, + opacity: 0.4, + getLineColor: [60, 60, 60], + getFillColor: [200, 200, 200] + }), + new GeoJsonLayer({ + id: 'airports', + data: AIR_PORTS, + // Styles + filled: true, + pointRadiusMinPixels: 2, + pointRadiusScale: 2000, + getPointRadius: f => 11 - f.properties.scalerank, + getFillColor: [200, 0, 80, 180], + // Interactive props + pickable: true, + autoHighlight: true, + onClick + }), + new ArcLayer({ + id: 'arcs', + data: AIR_PORTS, + dataTransform: d => d.features.filter(f => f.properties.scalerank < 4), + // Styles + getSourcePosition: f => [-0.4531566, 51.4709959], // London + getTargetPosition: f => f.geometry.coordinates, + getSourceColor: [0, 128, 200], + getTargetColor: [200, 0, 80], + getWidth: 1 + }) + ]} + /> ); } From 59c48dfd2f607b78b746e485980acdb8e01df997 Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Sat, 17 Aug 2024 14:59:26 -0700 Subject: [PATCH 4/6] demonstrate react-based widget Signed-off-by: Chris Gervang --- examples/get-started/react/widgets/app.jsx | 140 +++++++++++++-------- 1 file changed, 88 insertions(+), 52 deletions(-) diff --git a/examples/get-started/react/widgets/app.jsx b/examples/get-started/react/widgets/app.jsx index 1fa50d09c6a..539b3024e95 100644 --- a/examples/get-started/react/widgets/app.jsx +++ b/examples/get-started/react/widgets/app.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState} from 'react'; import {createRoot} from 'react-dom/client'; import DeckGL, {GeoJsonLayer, ArcLayer} from 'deck.gl'; import { @@ -9,6 +9,7 @@ import { LightGlassTheme } from '@deck.gl/widgets'; import '@deck.gl/widgets/stylesheet.css'; +import {createPortal} from 'react-dom'; /* global window */ const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); @@ -29,61 +30,96 @@ const INITIAL_VIEW_STATE = { }; function Root() { - const onClick = info => { - if (info.object) { - // eslint-disable-next-line - alert(`${info.object.properties.name} (${info.object.properties.abbrev})`); + const [widgetContainer, setWidgetContainer] = useState(); + + class ReactWidget { + constructor(props) { + this.id = props.id || 'react'; + this.placement = props.placement || 'top-left'; + this.viewId = props.viewId; + this.props = props; + } + + onAdd() { + const el = document.createElement('div'); + setWidgetContainer(el); + return el; } + + onRemove() { + setWidgetContainer(undefined); + } + + setProps(props) { + this.props = props; + } + } + + const reactWidget = new ReactWidget({}); + + const onClick = () => { + // eslint-disable-next-line + alert('React widget!'); }; + const widget = ( +
+
+ +
+
+ ); + return ( - 11 - f.properties.scalerank, - getFillColor: [200, 0, 80, 180], - // Interactive props - pickable: true, - autoHighlight: true, - onClick - }), - new ArcLayer({ - id: 'arcs', - data: AIR_PORTS, - dataTransform: d => d.features.filter(f => f.properties.scalerank < 4), - // Styles - getSourcePosition: f => [-0.4531566, 51.4709959], // London - getTargetPosition: f => f.geometry.coordinates, - getSourceColor: [0, 128, 200], - getTargetColor: [200, 0, 80], - getWidth: 1 - }) - ]} - /> + <> + {widgetContainer && createPortal(widget, widgetContainer)} + 11 - f.properties.scalerank, + getFillColor: [200, 0, 80, 180] + }), + new ArcLayer({ + id: 'arcs', + data: AIR_PORTS, + dataTransform: d => d.features.filter(f => f.properties.scalerank < 4), + // Styles + getSourcePosition: f => [-0.4531566, 51.4709959], // London + getTargetPosition: f => f.geometry.coordinates, + getSourceColor: [0, 128, 200], + getTargetColor: [200, 0, 80], + getWidth: 1 + }) + ]} + /> + ); } From 9d3aabffcb4f0d3b46478c5ea74e32e3bce1e3bf Mon Sep 17 00:00:00 2001 From: Chris Gervang Date: Fri, 30 Aug 2024 12:15:53 -0700 Subject: [PATCH 5/6] DeckWidget Component Wrapper Signed-off-by: Chris Gervang --- examples/get-started/react/widgets/app.jsx | 92 ++++++++++++++-------- modules/core/src/lib/widget-manager.ts | 2 + 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/examples/get-started/react/widgets/app.jsx b/examples/get-started/react/widgets/app.jsx index 539b3024e95..e276cdefa67 100644 --- a/examples/get-started/react/widgets/app.jsx +++ b/examples/get-started/react/widgets/app.jsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useState, forwardRef, useImperativeHandle, useMemo, useRef} from 'react'; import {createRoot} from 'react-dom/client'; import DeckGL, {GeoJsonLayer, ArcLayer} from 'deck.gl'; import { @@ -29,40 +29,66 @@ const INITIAL_VIEW_STATE = { pitch: 30 }; -function Root() { - const [widgetContainer, setWidgetContainer] = useState(); +function useWidget(props = {}) { + const [container, setContainer] = useState(null); class ReactWidget { - constructor(props) { - this.id = props.id || 'react'; - this.placement = props.placement || 'top-left'; - this.viewId = props.viewId; - this.props = props; - } - - onAdd() { - const el = document.createElement('div'); - setWidgetContainer(el); - return el; - } - - onRemove() { - setWidgetContainer(undefined); - } - - setProps(props) { - this.props = props; - } + constructor(props) { + this.id = props.id || 'react'; + this.placement = props.placement || 'top-left'; + this.viewId = props.viewId; + this.props = props; + } + + onAdd() { + const el = document.createElement('div'); + // Defer state update to avoid conflicts with rendering + requestAnimationFrame(() => setContainer(el)); + return el; + } + + onRemove() { + requestAnimationFrame(() => setContainer(null)); + } + + setProps(props) { + this.props = props; + this.placement = props.placement || this.placement; + this.viewId = props.viewId || this.viewId; + } } - const reactWidget = new ReactWidget({}); + const widget = useMemo(() => new ReactWidget(props), [props]); + + return { + widget, + container + }; +} + +function DeckWidgetWithRef(props, ref) { + const { widget, container } = useWidget(props); + + useImperativeHandle(ref, () => widget, [widget]); + + return container ? createPortal(props.children, container) : null; +} + +const DeckWidget = forwardRef(DeckWidgetWithRef); + +function Root() { + const [placement, setPlacement] = useState('top-left'); + const infoWidget = useWidget({id: 'hook'}); + const buttonWidget = useRef(null); + console.log(infoWidget, buttonWidget) const onClick = () => { // eslint-disable-next-line - alert('React widget!'); + // alert('React widget!'); + setPlacement('top-right'); }; - const widget = ( + const infoWidgetEl = (