deck.gl widgets 示例应用完全指南:从零搭建带交互控件的可视化应用
【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl
本文基于 deck.gl 仓库中的 widgets 示例应用 展开,该示例使用 Vite 作为构建与开发服务器,通过 6 个独立 Demo 系统演示了 deck.gl widgets 模块(@deck.gl/widgets)的用法。读完本文,你将掌握如何安装依赖、启动开发服务器、构建生产产物,并理解 Zoom、Compass、Info、Popup、ContextMenu 等 20 余个内置控件的真实配置方式,以及它们如何在纯 JavaScript 与 React 两种 API 下组织使用。
示例应用概览与目录结构
该示例位于仓库的 test/apps/widgets 目录,是一个用于验证与演示 widgets 模块能力的独立 Vite 应用。整个目录包含以下文件:
| 文件 | 作用 |
|---|---|
| README.md | 应用说明与使用命令 |
| package.json | 依赖声明与 npm scripts |
| index.html | 入口页,列出 6 个 Demo 的跳转链接 |
| geospatial.ts / geospatial.html | 纯 JS 地理空间场景(MapView) |
| multiview.ts / multiview.html | 纯 JS 多视图分割场景(SplitterWidget) |
| infovis.ts / infovis.html | 纯 JS 信息可视化场景(Orbit/Orthographic) |
| react-geospatial.tsx / react-geospatial.html | React 地理空间场景 |
| react-multiview.tsx / react-multiview.html | React 多视图分割场景 |
| react-infovis.tsx / react-infovis.html | React 信息可视化场景 |
入口 index.html 是一个纯链接导航页,在<main>中列出了六个 Demo 的入口:Pure JS - Geospatial、Pure JS - Multiview、Pure JS - Infovis,以及对应的三个 React 版本。这 6 个 Demo 分别对应「地图地理空间」「多视图分割」「非地理信息可视化」三大典型使用场景,且每种场景都同时给出纯 JS 与 React 两种写法,便于对照学习。
快速开始:安装、启动与构建
按 README.md 中的说明,该应用使用 Vite 进行打包与本地伺服。安装依赖:
npm install # 或者使用 yarn yarn项目脚本定义在 package.json 中:
| 命令 | 作用 |
|---|---|
npm start | 开发模式:启动 Vite 开发服务器并自动打开浏览器(vite --open),支持热更新 |
npm start-local | 以仓库根目录的 vite.config.local.mjs 作为配置启动(vite --config ../vite.config.local.mjs),用于以本地源码方式调试 deck.gl 模块 |
npm run build | 生产构建:生成最终 bundle 并写入磁盘(vite build) |
其中start-local是 deck.gl 仓库开发者常用的一条命令——它通过指向仓库根目录的本地 Vite 配置,将@deck.gl/*模块解析到本地源码(modules 目录),从而无需发布即可联调核心库改动。普通使用者直接使用npm start即可。
依赖分析
package.json 中的依赖揭示了 widgets 示例的构成:
- dependencies:
@deck.gl/core、@deck.gl/layers、@deck.gl/widgets(版本^9.3.0,本仓库当前源码版本为 9.4.0-beta.4)。核心渲染依赖 core,图层使用 layers,控件体系来自 widgets; - devDependencies:
vite(^7.3.1),仅用于构建与开发服务器。
注意type: "module"声明,应用以 ESM 方式运行,示例源码均为.ts/.tsx,可直接被 Vite 原生编译。
场景一:地理空间应用(Geospatial)
geospatial.ts 构建了一个以伦敦(经纬度[-0.45, 51.47],zoom 4,pitch 30)为中心的地图应用,叠加了 WMS 底图、国家边界、机场点与飞行弧线四类图层:
const INITIAL_VIEW_STATE = { latitude: 51.47, longitude: 0.45, zoom: 4, bearing: 0, pitch: 30 }; const deck = new Deck({ parent: document.getElementById('map') as HTMLDivElement, views: new MapView({repeat: true}), // 平铺重复地图 initialViewState: INITIAL_VIEW_STATE, controller: true, layers: getLayers(), widgets: [ /* 见下文 */ ] });图层数据来自 Natural Earth 公开 GeoJSON(国家边界与机场点),其中机场层启用了pickable与autoHighlight,并结合 DataFilterExtension 的getFilterValue/filterRange按机场等级(scalerank)做数据过滤,为后续 Timeline 控件联动提供基础。
widgets 数组:一次性装配 16 个控件
该 Demo 在Deck构造参数的widgets数组中一次性注册了 16 个控件,是 widgets 模块能力的集中展示:
widgets: [ new _GeocoderWidget({geocoder: 'coordinates', _geolocation: true}), new ZoomWidget(), new CompassWidget(), new FullscreenWidget(), new ScreenshotWidget(), new ResetViewWidget(), new LoadingWidget(), new _ScaleWidget({placement: 'bottom-right'}), new ThemeWidget(), new ContextMenuWidget({ /* ... */ }), new InfoWidget({mode: 'hover', getTooltip, arrow: 10, offset: 10}), new PopupWidget({ /* ... */ }), new _TimelineWidget({ /* ... */ }), new _StatsWidget({type: 'deck'}), new IconWidget({ /* ... */ }), new ToggleWidget({ /* ... */ }), new SelectorWidget({ /* ... */ }) ]对照 @deck.gl/widgets 的导出清单,这些控件可分为几类:
- 导航类:
ZoomWidget(缩放)、CompassWidget(指南针/方位)、ResetViewWidget(重置视角)、GimbalWidget(三维旋转指示,见 infovis 场景); - 地理空间类:
_GeocoderWidget(地理编码搜索)、_ScaleWidget(比例尺); - 视图类:
FullscreenWidget(全屏)、_SplitterWidget(视图分割,见 multiview 场景); - 信息类:
InfoWidget(悬停/点击信息浮层)、PopupWidget(固定位置弹窗)、ContextMenuWidget(右键菜单)、ScrollbarWidget(滚动条); - 控制类:
IconWidget(自定义图标按钮)、ToggleWidget(开关按钮)、SelectorWidget(选项切换器)、_TimelineWidget(时间轴播放器); - 工具类:
ScreenshotWidget(截图导出)、ThemeWidget(明暗主题切换)、LoadingWidget(加载指示)、_StatsWidget(渲染统计)。
控件参数详解
ContextMenuWidget —— 按拾取对象生成右键菜单。getMenuItems接收PickingInfo,当拾取到机场点时动态生成菜单项:
new ContextMenuWidget({ getMenuItems: (info: PickingInfo) => { const name = info.layer?.id === 'airports' && info.object?.properties.name; return ( name && [ {label: `Airport: ${name}`}, {value: 'open', label: 'Open in new tab'}, {value: 'favorite', label: 'Set as favorite'}, {value: 'filter', label: 'Exclude from filter'} ] ); }, onMenuItemSelected: console.log })onMenuItemSelected接收用户点击的菜单项 value,示例中直接打印到控制台,实际应用中可在此执行打开链接、收藏、过滤等业务逻辑。
InfoWidget —— 悬停/点击信息浮层。通过mode: 'hover'指定悬停模式(也可用'click'),getTooltip(info, widget)返回浮层内容与定位:
new InfoWidget({mode: 'hover', getTooltip, arrow: 10, offset: 10}) function getTooltip(info: PickingInfo, widget: InfoWidget) { if (!info.object || info.layer?.id !== 'airports') { return null; // 未命中机场点时返回 null,不显示浮层 } let text: string; switch (widget.props.mode) { case 'hover': text = `${info.object.properties.name} (${info.object.properties.abbrev})`; break; case 'click': text = `${info.object.properties.name} (${info.object.properties.abbrev})\n...`; break; } return { position: info.object.geometry.coordinates, // 浮层锚定在地理坐标处 text, style: {minWidth: '200px'} }; }arrow控制箭头尺寸(像素)、offset控制浮层与锚点的偏移距离。
PopupWidget —— 固定内容弹窗。可指定世界坐标position、marker(自定义锚点 DOM 元素,示例用createPin()生成 SVG 图钉)、placement: 'top'、offset: 20与closeOnClickOutside: true:
new PopupWidget({ position: [-5, 52], marker: {element: createPin()}, placement: 'top', offset: 20, content: `I'm here!`, closeOnClickOutside: true })TimelineWidget —— 时间轴与数据联动。这是示例中最具交互价值的控件:timeRange: [2, 9]定义时间区间,step: 1为步长,playInterval: 500为播放间隔(毫秒);onTimeChange回调中通过deck.setProps重设图层的数据过滤区间,实现随时间轴播放动态过滤机场点的效果:
new _TimelineWidget({ _container: document.getElementById('controls') as HTMLDivElement, timeRange: [2, 9], step: 1, playInterval: 500, onTimeChange: time => deck.setProps({ layers: getLayers([2, time]) // 更新 filterRange 上界 }) })IconWidget / ToggleWidget / SelectorWidget —— 自定义按钮组。三者均支持placement('top-right' 等)、SVG data URL 图标与label。ToggleWidget额外提供onIcon/onLabel/onColor定义激活态,onChange(checked)回调开关事件;SelectorWidget通过initialValue与options数组提供选项切换,示例给出了单视图/水平分割/垂直分割三选项,onChange打印当前值。
场景二:信息可视化(Infovis)
infovis.ts 展示了非地理坐标系下的控件应用:窗口左半为OrbitView(轨道视角),右半为OrthographicView(正射视角),共享一个 500 个随机三维散点的ScatterplotLayer:
new Deck({ views: [ new OrbitView({id: 'orbit-view', x: 0, width: '50%', controller: true}), new OrthographicView({ id: 'ortho-view', x: '50%', width: '50%', controller: {maxBounds: [[-50, -50, -50], [50, 50, 50]]} // 限制平移边界 }) ], initialViewState: INITIAL_VIEW_STATE, layers: [ /* ScatterplotLayer */ ], widgets: [ new FullscreenWidget(), new GimbalWidget(), new ResetViewWidget({id: 'reset-orbit', viewId: 'orbit-view', placement: 'top-right'}), new ScrollbarWidget({ id: 'scroll-orbit', viewId: 'orbit-view', orientation: 'horizontal', contentBounds: [[-50, -50], [50, 50]] }), new ResetViewWidget({id: 'reset-ortho', viewId: 'ortho-view', placement: 'top-right'}), new ZoomWidget({viewId: 'ortho-view'}), new ThemeWidget({darkModeTheme: DarkTheme, lightModeTheme: LightTheme}), new InfoWidget({viewId: 'ortho-view', mode: 'hover', getTooltip: ({object}) => (object ? 'point' : null)}), new ScrollbarWidget({id: 'scrollbar-v', viewId: 'ortho-view', placement: 'bottom-right', orientation: 'vertical'}), new ScrollbarWidget({id: 'scrollbar-h', viewId: 'ortho-view', placement: 'bottom-right', orientation: 'horizontal'}) ] })该场景引入了两个关键概念:
- viewId 绑定:在多视图场景下,控件通过
viewId属性精确绑定到某个视图——例如两个ResetViewWidget分别用viewId: 'orbit-view'与viewId: 'ortho-view'独立重置各自视角,ZoomWidget与InfoWidget也只作用于正射视图; - ScrollbarWidget 滚动条:
orientation指定水平/垂直方向,contentBounds定义内容世界的坐标范围,配合placement(如 'bottom-right')放置;代码中还注释展示了decorations装饰(contentBounds + color + title)的用法。
此外GimbalWidget用于显示并操控轨道视角的旋转姿态,是三维信息可视化(点云、模型浏览)中的常用控件。
场景三:多视图分割(Multiview)
multiview.ts 是_SplitterWidget的用法示范:通过一个viewLayout描述树状视图布局——外层水平分割出左右两区,右区再垂直分割为上、下两个MapView,形成「左 + 右上 + 右下」的联动地图:
const VIEW_LAYOUT: SplitterWidgetProps['viewLayout'] = { orientation: 'horizontal', views: [ new MapView({id: 'left', controller: true}), { orientation: 'vertical', views: [ new MapView({id: 'right-top', controller: true}), new MapView({id: 'right-bottom', controller: true}) ] } ] }; new Deck({ initialViewState: INITIAL_VIEW_STATE, layers: LAYERS, widgets: [new SplitterWidget({viewLayout: VIEW_LAYOUT})] });SplitterWidget会自动从viewLayout编译出实际的 view 组合(对应 view-layout/build-views-from-view-layout.ts 的实现),并渲染可拖拽的分割条;三个子视图复用同一份 GeoJSON/Arc 图层数据,但可独立平移缩放,适合多视角对比类应用(如左右眼立体、上下游对比)。
Pure JS 与 React 双 API 对照
三个场景均提供 React 版本(react-geospatial.tsx、react-infovis.tsx、react-multiview.tsx),二者差异体现在 API 形态上:
1. 导入来源不同。纯 JS 从@deck.gl/core导入Deck,从@deck.gl/widgets导入控件;React 版从@deck.gl/react导入DeckGL与控件。这是因为 @deck.gl/react 重新导出了 widgets 的全部控件,并额外提供useWidgetHook 与各控件 props 类型。示例中 React 版还显式使用createRoot(...).render(<App />)挂载应用。
2. 声明方式不同。纯 JS 用new Deck({...})的widgets数组声明;React 版将控件作为<DeckGL>的子组件 JSX 声明,例如:
<DeckGL views={new MapView({repeat: true})} initialViewState={INITIAL_VIEW_STATE} controller layers={layers}> <_GeocoderWidget geocoder="coordinates" _geolocation /> <ZoomWidget /> <CompassWidget /> {/* ... */} </DeckGL>3. 状态管理方式不同。React 版利用组件化优势管理动态状态:react-geospatial.tsx用useState(() => getLayers())持有图层,_TimelineWidget的onTimeChange中调用setLayers(getLayers([2, time]))触发重渲染;react-infovis.tsx用viewState+onViewStateChange受控管理双视图状态:
const onViewStateChange = useCallback(({viewId, viewState}) => { setViewState(curr => ({...curr, [viewId]: viewState})); }, []);react-multiview.tsx则先用useState维护一个单一MapView,配合SplitterWidget在交互后切换到完整布局。
主题、样式与实验性控件说明
所有 Demo 都在模块代码中引入了默认样式表:
import '@deck.gl/widgets/stylesheet.css';该样式表由 stylesheet.css 经 PostCSS 构建后发布为@deck.gl/widgets/stylesheet.css(见 modules/widgets/package.json 的 build 脚本)。不引入它将导致控件无样式渲染。
主题切换:ThemeWidget支持明暗主题。默认使用内置主题;也可如 infovis 示例所示显式传入自定义主题:
new ThemeWidget({darkModeTheme: DarkTheme, lightModeTheme: LightTheme})DarkTheme/LightTheme(以及LightGlassTheme/DarkGlassTheme)由 themes.ts 导出,类型为DeckWidgetTheme,开发者可据此自定义配色。
下划线前缀(实验性):源码导出清单中_GeocoderWidget、_ScaleWidget、_SplitterWidget、_TimelineWidget、_StatsWidget等均带下划线前缀(见 modules/widgets/src/index.ts 中export {X as _X}的写法),表示其 API 尚未稳定、后续版本可能调整,生产环境使用需谨慎。该清单还导出了_ButtonGroup、_IconButton、_Tooltip、_DropdownMenu、_RangeInput等基于 Preact 的实验性基础组件,以及_GoogleGeocoder、_MapboxGeocoder、_OpenCageGeocoder、_CoordinatesGeocoder、_CurrentLocationGeocoder等实验性地理编码器(源码位于 modules/widgets/src/lib/geocode)。
底层实现要点:@deck.gl/widgets 依赖preact与@floating-ui/dom(见 package.json),前者负责控件 UI 渲染,后者负责浮层(Tooltip/Popup/Dropdown)的定位计算;其 peerDependencies 要求@deck.gl/core与@luma.gl/core版本匹配,构建示例时需保证依赖版本一致。
延伸阅读
- 控件 API 完整参考:docs/api-reference/widgets(含 24 个控件文档)
- 其他官方示例:本仓库 examples/website 下各展示页同样大量使用 widgets
- React 集成方式:docs/get-started/using-with-react.md
- 更多渲染回归测试:本示例属于 test/apps 系列,仓库另有
widgets-layerlist等 widgets 相关测试应用可供参考
【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考