deck.gl HeatmapLayer 入门实战:基于 React 与 MapLibre 构建 Uber 出行热力图
【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl
本指南以仓库中的最小独立示例 examples/website/heatmap 为核心,完整讲解如何在 React + Vite 项目中用@deck.gl/aggregation-layers的HeatmapLayer渲染空间分布热力图:从工程搭建、数据格式、图层参数调优,到 GPU 聚合的底层原理与平台限制。读完本文,你将能够独立复制该示例、替换自己的数据集,并针对radiusPixels、intensity、threshold、colorDomain等关键参数做出符合场景的取舍。
示例概览:一个最小可运行的 HeatmapLayer 应用
examples/website/heatmap是 deck.gl 官网上 HeatmapLayer 示例的最小独立版本(README 首句即说明 "This is a minimal standalone version of the HeatmapLayer example")。整个示例只包含 5 个文件:
| 文件 | 作用 |
|---|---|
| app.tsx | React 组件:组装DeckGL、HeatmapLayer与底图 |
| index.html | HTML 入口,挂载#app根节点并加载 MapLibre CSS |
| package.json | 依赖与启动脚本(vite) |
| tsconfig.json | TypeScript 编译配置 |
| README.md | 用法说明(本文所依据的主文档) |
示例展示的是纽约市 Uber 打车点位的空间分布热力图,数据来源为 FiveThirtyEight 的公开响应数据,底图由 CARTO 免费底图服务提供。它演示了 heatmap 场景中最典型的组合方式:聚合图层(HeatmapLayer)+ 矢量底图(MapLibre GL)+ React 集成(@deck.gl/react)。
快速运行:三步启动示例
按照 README.md 的指引,将该文件夹内容复制到你的项目中,然后执行:
# 安装依赖 npm install # 或使用 yarn yarn # 使用 vite 打包并启动开发服务器 npm startnpm start实际执行的是vite --open(见 package.json),启动后会自动打开浏览器加载应用。Vite 自带热更新,修改app.tsx后页面会即时刷新,非常适合边调参数边看效果。
项目还提供了两个额外脚本:
npm run start-local # 使用仓库根目录的 vite.config.local.mjs,便于在 monorepo 内调试本地源码 npm run build # 执行 vite build,产出生产构建依赖清单
package.json 中声明的核心依赖如下,版本以当前仓库为准:
deck.gl(^9.0.0):聚合包,其中包含@deck.gl/aggregation-layers@deck.gl/react(随 deck.gl 9.x 提供):React 绑定,提供DeckGL组件react/react-dom(^18.0.0):React 运行时react-map-gl(^8.0.0):React 版 MapLibre/Mapbox 封装maplibre-gl(^5.0.0):MapLibre GL 引擎typescript(^4.6.0)与vite(^7.3.3):开发与构建工具
注意:仓库为 monorepo 结构,若想直接引用本地源码进行调试,可运行
npm run start-local,它会读取仓库根目录下的 vite.config.local.mjs 完成模块别名解析。
数据格式:position + weight 的点集
HeatmapLayer的输入是带权重的空间点集合。每个数据对象只需提供两个信息:
- 位置(position):由
getPosition访问器返回,通常为[经度, 经度]的经纬度数组; - 权重(weight):由
getWeight访问器返回,表示该点对热力值的贡献量,默认为1。
示例使用远程 JSON 数据,见 app.tsx:
const DATA_URL = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/screen-grid/uber-pickup-locations.json';示例中还通过 TypeScript 类型明确了数据形态(app.tsx):
type DataPoint = [longitude: number, latitude: number, count: number];即每个元素是一个三元组[经度, 纬度, 上车点数量],其中第三个值正是热力图的权重来源。
接入你自己的数据
更换数据只需做两件事:
- 替换
data:App组件的data属性既可接收 URL 字符串(内部自动加载),也可直接传入DataPoint[]数组(见 app.tsx 的类型定义); - 对齐访问器:根据你的数据字段改写
getPosition与getWeight,例如:
const layers = [ new HeatmapLayer<MyRecord>({ data: myRecords, getPosition: (d: MyRecord) => d.coordinates, // 返回 [lng, lat] getWeight: (d: MyRecord) => d.value, // 返回数值权重 radiusPixels: 30, intensity: 1, threshold: 0.03 }) ];关于 HeatmapLayer 全部属性与数据访问器的权威说明,可查阅 docs/api-reference/aggregation-layers/heatmap-layer.md。
核心实现剖析:从入口到图层配置
app.tsx 是示例的完整实现,整体结构如下:
const INITIAL_VIEW_STATE: MapViewState = { longitude: -73.75, // 纽约市中心 latitude: 40.73, zoom: 9, maxZoom: 16, pitch: 0, bearing: 0 }; const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json'; export default function App({ device, data = DATA_URL, intensity = 1, threshold = 0.03, radiusPixels = 30, mapStyle = MAP_STYLE }) { const layers = [ new HeatmapLayer<DataPoint>({ data, id: 'heatmap-layer', pickable: false, getPosition: d => [d[0], d[1]], getWeight: d => d[2], radiusPixels, intensity, threshold }) ]; return ( <DeckGL device={device} initialViewState={INITIAL_VIEW_STATE} controller={true} layers={layers}> <Map reuseMaps mapStyle={mapStyle} /> </DeckGL> ); }几个值得注意的要点:
DeckGL组件:来自@deck.gl/react,通过initialViewState设定初始相机,controller={true}开启平移/缩放交互,layers传入图层实例;Map子组件:来自react-map-gl/maplibre,reuseMaps允许 DeckGL 与 MapLibre 共享同一 canvas 渲染管线,mapStyle指定 CARTO 的 dark-matter 无标签样式;device属性:@luma.gl/core的Device类型,可选传入,用于在宿主应用中共享 GPU 设备(示例index.html的挂载脚本会调用renderToDOM完成渲染,见 index.html)。
图层属性精讲:把热力图调到恰到好处
示例将radiusPixels、intensity、threshold暴露为组件的可调参数,下面结合 heatmap-layer.md 与 heatmap-layer.ts 的默认值逐一说明。
radiusPixels:热力晕圈半径
- 默认值:
30(文档)/源码默认50(示例传 30) - 含义:单个数据点权重所分布到的圆形区域的像素半径。半径越大,热力点越"晕开",反之越尖锐。
- 源码约束:
{type: 'number', min: 1, max: 100, value: 50},即合法范围 1–100。
intensity:强度缩放
- 默认值:
1 - 含义:与像素处总权重相乘得到最终权重的乘数。大于 1 会把输出颜色向色谱高端(更热)偏移,小于 1 则向低端偏移。
- 适合场景:数据整体权重偏低或偏高时,用
intensity做全局亮度补偿,示例默认取1。
threshold:边缘淡出阈值
- 默认值:
0.05(示例取 0.03) - 含义:低权重像素透明度的削减比例。定义为淡出权重与最大权重的比值(0–1),例如
0.1表示影响所有权重低于最大值 10% 的像素。threshold越大,色块边界越平滑,但低权重像素因 alpha 过低更难看清。 - 注意:当指定了
colorDomain时,threshold会被忽略。
colorDomain:颜色映射域
- 默认值:
null(自动) - 含义:
[minValue, maxValue]二元组,控制权重如何映射到colorRange。minValue对应colorRange的第一个颜色,maxValue对应最后一个颜色,中间线性插值;低于minValue的像素逐渐淡出至全透明(表示 0),高于maxValue的被截断到最后一个颜色。 - 聚合模式差异:
aggregation: 'SUM'时colorDomain按每平方米权重解释;'MEAN'时按权重解释。 - 为何要手动指定:未指定时,最大值由当前视口自动决定,域为
[maxValue * threshold, maxValue],因此同一位置的颜色会随视口内其他数据点变化。如需稳定的颜色映射(例如展示图例),必须提供自定义colorDomain。
aggregation:聚合操作
- 默认值:
'SUM',可选'SUM'/'MEAN',非法值回退为'SUM' - 含义:决定像素颜色值的聚合方式。每个数据点的权重被分配到以该点为中心的圆形区域,像素接收的权重与到中心的距离成反比:
'SUM':落入多个圆圈的像素,权重为所有来源之和;'MEAN':落入多个圆圈的像素,权重为所有邻近点的加权平均。
- 源码实现:
AGGREGATION_MODE = {SUM: 0, MEAN: 1}(heatmap-layer.ts),在子图层渲染时经aggregationMode传入着色器(heatmap-layer.ts)。
colorRange:色带
- 默认值:ColorBrewer 的
6-class YlOrRd(黄-橙-红渐变) - 含义:热力图使用的调色板,形如
[color1, color2, ...],每个颜色为[r, g, b, [a]],通道值 0–255,a缺省为 255。颜色数量即色带采样数,中间颜色按权重线性插值。
weightsTextureSize:权重纹理尺寸(性能关键)
- 默认值:
2048,合法范围 128–2048 - 含义:权重纹理的大小。纹理越小渲染性能越好:官方文档给出的实测参考是 2048×2048 纹理计算最大权重约需 50–100 ms,而 512×512 仅需 5–7 ms;代价是可见的像素化。
- 源码细节:实际纹理尺寸还会被设备上限裁剪:
Math.min(weightsTextureSize, device.limits.maxTextureDimension2D)(heatmap-layer.ts)。
debounceTimeout:交互防抖
- 默认值:
500(毫秒),合法范围 0–1000 - 含义:视口变化后延迟触发重新聚合的间隔。大数据集配合大
radiusPixels时,交互过程中的聚合更新容易造成卡顿;设置正数debounceTimeout可推迟聚合、避免冻结,副作用是交互结束后需要等待片刻才能看到更新结果。 - 源码实现:
_debouncedUpdateWeightmap在 zoom 变化时用setTimeout延迟debounceTimeout后强制更新权重图(heatmap-layer.ts)。
数据访问器:getPosition 与 getWeight
getPosition:默认object => object.position,返回每个点的位置(经纬度数组)。getWeight:默认1,返回每个点的权重。不提供时所有点权重相同,热力值只反映点密度。
在示例中,getPosition: d => [d[0], d[1]]从三元组取经纬度,getWeight: d => d[2]取上车点数量,最终热力值即"某区域的上车密度"。
底层原理:GPU 上的高斯核密度估计
HeatmapLayer 与普通图层最大的不同在于聚合发生在 GPU 上。从 heatmap-layer.ts 的源码可以梳理出完整渲染管线:
- 权重图生成(weights pass):
_createWeightsTransform创建TextureTransform,把每个数据点按其radiusPixels与getWeight通过weights-vs.glsl/weights-fs.glsl(WebGPU 下为weights.wgsl)绘制到一张权重纹理上,混合模式为加法混合(blendColorOperation: 'add'),从而天然实现多圆叠加求和(heatmap-layer.ts); - 最大权重归约(max pass):
maxWeightTransform把权重纹理归约为 1×1 的 max 纹理,采用blendColorOperation: 'max',得到当前视口内的最大权重(heatmap-layer.ts); - 颜色映射(triangle pass):
TriangleLayer渲染一个覆盖视口的四边形,片元着色器中结合权重纹理、max 纹理与colorTexture(由colorRange生成的一维纹理,见_updateColorTexture),按intensity、threshold、colorDomain计算最终颜色并叠加到底图上。
视口驱动的优化也体现在源码中:_updateBounds会把当前屏幕四个角unproject成世界坐标,计算出需要处理的可视世界边界,仅对可视范围做聚合(heatmap-layer.ts),这正是colorDomain未指定时颜色随视口变化的原因。
说明:官方文档将 HeatmapLayer 描述为"内部实现高斯核密度估计(Gaussian Kernel Density Estimation)"来渲染热力图,上述源码路径可以印证其 GPU 三趟(weights → max → triangle)的实现结构。
平台限制与 WebGPU 支持
HeatmapLayer 依赖 GPU 浮点纹理渲染,并非所有平台都完整支持。官方文档 heatmap-layer.md 明确列出:
- WebGPU:使用实例化四边形与 16 位浮点渲染目标,无需依赖 WebGL 点精灵即可保留高斯核密度估计精度;源码中对应
rgba16float格式与实例化属性布局(heatmap-layer.ts)。 - WebGL:在主流桌面浏览器(evergreen)上完整支持;但在iOS Safari上,WebGL 上下文不支持渲染到浮点纹理,图层回退到 8 位低精度模式——此时权重必须是整数,且任意像素累积权重不能超过 255。
- 判定逻辑:源码通过
FLOAT_TARGET_FEATURES(float32-renderable-webgl与texture-blend-float-webgl)探测能力,不支持时把纹理格式降级为rgba8unorm并输出警告日志(heatmap-layer.ts)。
因此,如果你的目标平台包含 iOS 移动端,需要留意权重数值范围,或考虑改用 CPU 聚合的图层方案。
更换底图服务
示例默认使用 CARTO 免费底图服务(https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json)。如需替换:
- 直接修改
App组件的mapStyle属性,传入任意 MapLibre Style JSON 的 URL 或内联对象; - 更完整的备选方案(Mapbox、MapTiler、自定义瓦片服务等)可参考仓库文档 docs/get-started/using-with-map.md 中关于其他底图服务的说明。
延伸阅读
- 完整 API 参考:HeatmapLayer 官方文档
- 图层源码:modules/aggregation-layers/src/heatmap-layer/heatmap-layer.ts(含 weights/max/triangle 三个 GPU pass 的 GLSL/WGSL 着色器与工具函数)
- 聚合图层总览:docs/api-reference/aggregation-layers/overview.md
- 更多同风格示例:仓库 examples/website 目录下还有 screen-grid、contour、hexagon 等聚合类图层的独立示例,可与本文的 HeatmapLayer 实现相互对照。
【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考