deck.gl ScreenGridLayer 完全指南:屏幕空间网格聚合图层的原理、参数与实战
2026/9/14 18:09:06 网站建设 项目流程

deck.gl ScreenGridLayer 完全指南:屏幕空间网格聚合图层的原理、参数与实战

【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl

ScreenGridLayer 是 deck.gl aggregation-layers 模块提供的屏幕空间聚合图层:它将输入数据点投影到屏幕坐标系后,按固定像素尺寸划分直方图网格(bin),对落入每个网格的数据对象权重执行求和、均值等聚合运算,并以彩色网格叠加在地图上。本文以官方 API 文档 docs/api-reference/aggregation-layers/screen-grid-layer.md 为主体骨架,结合仓库源码与测试,完整讲解其安装、全部配置参数、拾取交互以及 CPU/GPU 聚合的底层实现,读完后你可以在自己的地图应用中直接落地一个高性能的密度/强度热力网格图层。

什么是 ScreenGridLayer

ScreenGridLayer的核心思路是把"地理聚合"转化为"屏幕聚合":

  • 数据在屏幕空间(而非经纬度空间)被聚合为直方图分箱(histogram bins);
  • 每个分箱即一个固定像素尺寸的网格单元(cell),单元宽度/高度由cellSizePixels决定;
  • 所有落入同一网格的数据对象的权重按aggregation指定的运算(SUM / MEAN / MIN / MAX / COUNT)汇总为单元值;
  • 单元值再通过colorDomain+colorRange+colorScaleType映射为颜色,最终以叠加的彩色网格渲染出来。

由于聚合发生在屏幕空间,官方文档明确给出一个重要提示:只要地图发生缩放或平移,层就必须重新聚合数据(详见 docs/api-reference/aggregation-layers/screen-grid-layer.md)。这意味着该图层最适合中小规模数据集——配合合适的数据与配色,其视觉效果可以非常出色,常用于展示停车位分布、交通站点密度、事件热点等场景。

安装与引入

使用 npm 安装聚合图层依赖:

npm install deck.gl # 或按需拆分安装 npm install @deck.gl/core @deck.gl/layers @deck.gl/aggregation-layers

TypeScript 下同时引入类型定义:

import {ScreenGridLayer} from '@deck.gl/aggregation-layers'; import type {ScreenGridLayerProps, ScreenGridLayerPickingInfo} from '@deck.gl/aggregation-layers'; new ScreenGridLayer<DataT>(...props: ScreenGridLayerProps<DataT>[]);

使用预打包脚本(pre-bundled scripts)时:

<script src="https://unpkg.com/deck.gl@^9.0.0/dist.min.js"></script> <!-- 或 --> <script src="https://unpkg.com/@deck.gl/core@^9.0.0/dist.min.js"></script> <script src="https://unpkg.com/@deck.gl/layers@^9.0.0/dist.min.js"></script> <script src="https://unpkg.com/@deck.gl/aggregation-layers@^9.0.0/dist.min.js"></script>
new deck.ScreenGridLayer({});

仓库中该图层的类定义位于 modules/aggregation-layers/src/screen-grid-layer/screen-grid-layer.ts,static layerName = 'ScreenGridLayer',可直接在 JSON 配置(如 playground)中以"@@type": "ScreenGridLayer"引用。

快速上手:JavaScript / TypeScript / React

官方文档提供了三套等价的最小可运行示例(数据为旧金山自行车停车位数据)。JavaScript 版本:

import {Deck} from '@deck.gl/core'; import {ScreenGridLayer} from '@deck.gl/aggregation-layers'; const layer = new ScreenGridLayer({ id: 'ScreenGridLayer', data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json', gpuAggregation: true, cellSizePixels: 50, colorRange: [ [0, 25, 0, 25], [0, 85, 0, 85], [0, 127, 0, 127], [0, 170, 0, 170], [0, 190, 0, 190], [0, 255, 0, 255] ], getPosition: d => d.COORDINATES, getWeight: d => d.SPACES, opacity: 0.8 }); new Deck({ initialViewState: { longitude: -122.4, latitude: 37.74, zoom: 11 }, controller: true, getTooltip: ({object}) => object && `Count: ${object.value}`, layers: [layer] });

TypeScript 版本(带数据对象类型与拾取信息类型):

import {Deck} from '@deck.gl/core'; import {ScreenGridLayer, ScreenGridLayerPickingInfo} from '@deck.gl/aggregation-layers'; type BikeRack = { ADDRESS: string; SPACES: number; COORDINATES: [longitude: number, latitude: number]; }; const layer = new ScreenGridLayer<BikeRack>({ id: 'ScreenGridLayer', data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json', gpuAggregation: true, cellSizePixels: 50, colorRange: [ [0, 25, 0, 25], [0, 85, 0, 85], [0, 127, 0, 127], [0, 170, 0, 170], [0, 190, 0, 190], [0, 255, 0, 255] ], getPosition: (d: BikeRack) => d.COORDINATES, getWeight: (d: BikeRack) => d.SPACES, opacity: 0.8 }); new Deck({ initialViewState: {longitude: -122.4, latitude: 37.74, zoom: 11}, controller: true, getTooltip: ({object}: ScreenGridLayerPickingInfo<BikeRack>) => object && `Count: ${object.value}`, layers: [layer] });

React 版本(基于@deck.gl/reactDeckGL组件):

import React from 'react'; import {DeckGL} from '@deck.gl/react'; import {ScreenGridLayer, ScreenGridLayerPickingInfo} from '@deck.gl/aggregation-layers'; type BikeRack = { ADDRESS: string; SPACES: number; COORDINATES: [longitude: number, latitude: number]; }; function App() { const layer = new ScreenGridLayer<BikeRack>({ id: 'ScreenGridLayer', data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json', gpuAggregation: true, cellSizePixels: 50, colorRange: [ [0, 25, 0, 25], [0, 85, 0, 85], [0, 127, 0, 127], [0, 170, 0, 170], [0, 190, 0, 190], [0, 255, 0, 255] ], getPosition: (d: BikeRack) => d.COORDINATES, getWeight: (d: BikeRack) => d.SPACES, opacity: 0.8 }); return <DeckGL initialViewState={{longitude: -122.4, latitude: 37.74, zoom: 11}} controller getTooltip={({object}: ScreenGridLayerPickingInfo<BikeRack>) => object && `Count: ${object.value}`} layers={[layer]} />; }

ScreenGridLayer继承自聚合图层基类AggregationLayer(继承链为ScreenGridLayer → AggregationLayer → CompositeLayer → Layer),因此继承所有 Base Layer 属性iddataopacityvisiblepickableupdateTriggers等),详见 modules/aggregation-layers/src/common/aggregation-layer.ts。

聚合选项(Aggregation Options)

gpuAggregation(boolean,可选)

  • 默认值:true

当设为true且浏览器支持时,聚合在 GPU 上执行(通过WebGLAggregator)。源码 screen-grid-layer.ts 中的getAggregatorType()展示了真实的决策逻辑:

getAggregatorType(): string { return this.props.gpuAggregation && WebGLAggregator.isSupported(this.context.device) ? 'gpu' : 'cpu'; }

即在gpuAggregation: true当前设备支持 WebGL 聚合时走 GPU 路径;否则自动回退到CPUAggregator。在合适的场景下开启 GPU 聚合能显著加速应用,但它对输入数据性质和所需功能各有取舍,详见下文"CPU 与 GPU 聚合的取舍"以及 overview.md 的 CPU vs GPU Aggregation 章节。

cellSizePixels(number,可选,支持过渡动画)

  • 默认值:100

网格单元(bin)的像素宽/高。源码中该属性的类型定义为{type: 'number', value: 100, min: 1},即最小值被限制为 1 像素。设置过小会导致网格数量爆炸,GPU 聚合时尤其明显(见下文 binIdRange 推导);设置过大则会丢失空间细节。

aggregation(string,可选)

  • 默认值:'SUM'

定义把落入同一网格的所有数据对象权重汇总为单元值的运算。合法取值:

  • 'SUM':单元格内所有点权重的总和;
  • 'MEAN':单元格内所有点权重的均值;
  • 'MIN':单元格内所有点权重的最小值;
  • 'MAX':单元格内所有点权重的最大值;
  • 'COUNT':落入单元格的点数量。

getWeightaggregation共同决定每个网格单元的值(也即后续渲染的高度/强度依据)。源码中该值直接作为operations传给聚合器:aggregator.setProps({operations: [aggregation], ...}),聚合类型AggregationOperation定义于 modules/aggregation-layers/src/common/aggregator/index.ts。

渲染选项(Render Options)

cellMarginPixels(number,可选,支持过渡动画)

  • 默认值:2,会被钳制在[0, 5]区间

网格单元之间的间距(像素)。注意:设置该属性不会影响数据如何分箱——它只影响渲染时每个单元格的实际绘制尺寸。源码 screen-grid-cell-layer.ts 中体现为:

const cellSize = Math.max(gridSize - cellMarginPixels, 0);

即最终绘制单元尺寸 =cellSizePixels - cellMarginPixels(下限 0),再换算为裁剪空间尺寸cellSizeClipspace传给着色器。因此cellMarginPixels越大,网格之间缝隙越明显,视觉上越像"砖格"。

colorScaleType(string,可选)

  • 默认值:'linear'

颜色比例尺负责把连续的数值区间(colorDomain)映射为一组离散颜色(colorRange)。值为colorDomain[0]的单元格渲染为colorRange[0]的颜色,值为colorDomain[1]的单元格渲染为colorRange最后一个颜色。支持的取值:

  • 'linear':按值在colorDomain中的位置对colorRange做线性插值;
  • 'quantize':把colorDomain等分为colorRange.length段,每段映射到colorRange中的一个离散颜色。

实现层面,源码把colorRangecolorScaleType烘焙进一张 1D 纹理(createColorRangeTexture),并在顶点着色器里用texture(range, vec2(r, 0.5))采样取色;linear对应线性过滤、quantize对应最近邻过滤,参见 modules/aggregation-layers/src/common/utils/color-utils.ts 与 screen-grid-layer-vertex.glsl.ts。

colorDomain(number[2],可选)

  • 默认值:null(自动)

若未提供,图层在运行时把colorDomain设为所有网格单元的实际最小值与最大值(源码中通过aggregator.getResultDomain(0)draw()时刻求值:colorDomain: () => this.props.colorDomain || aggregator.getResultDomain(0))。

显式提供colorDomain可以控制数值到颜色的映射关系,适合希望用同一套配色渲染不同数据输入以便相互比较的场景。例如固定[0, 1000]后,两个数据集的相同数值会呈现相同颜色。

colorRange(Color[6],可选)

  • 默认值:colorbrewer 6 级顺序色板YlOrRd(黄-橙-红)

由 6 个颜色组成的数组[color1, ..., color6]。每个颜色是 3 或 4 个值的数组[R, G, B][R, G, B, A],分别表示红、绿、蓝和透明度通道强度,取值范围0~255;省略 Alpha 时视为255。源码中默认色板定义于 color-utils.ts:

export const defaultColorRange: Color[] = [ [255, 255, 178], [254, 217, 118], [254, 178, 76], [253, 141, 60], [240, 59, 32], [189, 0, 38] ];

展平时(colorRangeToFlatArray),缺失的 Alpha 通道统一补255。上面的示例代码使用了绿色系色板(深绿 → 亮绿)来呈现停车位数量,与本图层的密度主题非常契合。

数据访问器(Data Accessors)

访问器的通用约定见 开发者指南:Accessors。

getPosition(Accessor<Position>,可选)

  • 默认值:object => object.position

用于取回每个数据对象位置的函数,返回经纬度坐标。源码中该属性被注册为positions属性:size: 3type: 'float64',并在启用 64 位精度时使用fp64补偿,见 screen-grid-layer.ts 的initializeState()

getWeight(Accessor<number>,可选)

  • 默认值:1

每个数据对象的权重:

  • 传入数字时,该数字作为所有对象的统一权重;
  • 传入函数时,函数在每个对象上被调用以取回其权重。

该属性在 GPU 路径下注册为counts属性(size: 1),其值作为顶点着色器中的in float counts参与聚合。

拾取(Picking)与交互

ScreenGridLayer的 hover/click 事件返回的 PickingInfo.object 代表一个被聚合的网格单元(而非单个数据点)。官方文档与源码中的ScreenGridLayerPickingInfo<DataT>类型(screen-grid-layer.ts)定义了以下字段:

字段类型说明
colnumber被拾取单元的列索引,从视口最左侧的 0 开始
rownumber被拾取单元的行索引,从视口最顶部的 0 开始
valuenumber聚合值,由getWeightaggregation共同决定
countnumber落入该单元的数据点数量
pointIndicesnumber[]落入该单元的数据对象索引数组,仅 CPU 聚合时可用
pointsobject[]落入该单元的数据对象数组,仅 CPU 聚合且 layer data 为数组时可用

源码getPickingInfo()的实现(screen-grid-layer.ts)展示了字段来源:col = bin.id[0]row = bin.id[1]value = bin.value[0]count = bin.count;而pointIndices/points只有在bin.pointIndices存在(即 CPU 聚合路径)时才填充。前端示例中常见的getTooltip: ({object}) => object && \Count: ${object.value}`正是利用value` 字段。

底层原理:CPU 与 GPU 两条聚合路径

从源码结构看,ScreenGridLayer是一个复合图层:它先用聚合器(Aggregator)把原始数据折叠为"网格单元 + 单元权重"两个属性(getBingetWeight),再交给子图层ScreenGridCellLayer(screen-grid-cell-layer.ts)以 instanced 方式绘制(几何体为 triangle-strip 单位四边形)。

CPU 路径

createAggregator()中 CPU 分支使用CPUAggregator(维度 2),其分箱逻辑清晰展示了"屏幕空间"的含义:

const viewport = this.context.viewport; const p = viewport.project(positions); const cellSizePixels: number = opts.cellSizePixels; if (p[0] < 0 || p[0] >= viewport.width || p[1] < 0 || p[1] >= viewport.height) { // Not on screen return null; } return [Math.floor(p[0] / cellSizePixels), Math.floor(p[1] / cellSizePixels)];

即:先把经纬度经viewport.project投影为屏幕像素坐标,越界点被丢弃,然后按floor(像素坐标 / cellSizePixels)取整得到网格行列号。这也从实现层面印证了文档的提示——每次视口变化都必须重新聚合,源码在updateState()中检测到changeFlags.viewportChanged时会调用this.state.aggregator.setNeedsUpdate()强制重跑聚合。

GPU 路径

GPU 分支使用WebGLAggregator,配合project32binOptionsUniforms两个 shader 模块,顶点着色器内计算 bin:

void getBin(out ivec2 binId) { vec4 pos = project_position_to_clipspace(positions, positions64Low, vec3(0.0)); vec2 screenCoords = vec2(pos.x / pos.w + 1.0, 1.0 - pos.y / pos.w) / 2.0 * project.viewportSize.xy / project.devicePixelRatio; vec2 gridCoords = floor(screenCoords / binOptions.cellSizePixels); binId = ivec2(gridCoords); }

GPU 聚合需要预先声明网格总范围binIdRange,源码依据当前视口尺寸推导:

binIdRange: [ [0, Math.ceil(width / cellSizePixels)], [0, Math.ceil(height / cellSizePixels)] ]

注意这会覆盖所有可能存在的网格(包括空网格),其内存占用与"最大网格数量"成正比,而非与"有点的网格数量"成正比——这是判断 GPU 聚合是否划算的关键因素。

子图层渲染时,每个单元格被展开为单位四边形,顶点着色器(screen-grid-layer-vertex.glsl.ts)根据instanceWeightsscreenGrid.colorDomaincolorRange纹理插值出颜色,并对NaN权重(无数据网格)直接丢弃绘制;片段着色器(screen-grid-layer-fragment.glsl.ts)输出颜色并预留DECKGL_FILTER_COLOR钩子供扩展注入。

CPU 与 GPU 聚合的取舍(重要)

官方文档在 overview.md 中给出了深入分析,要点如下:

  • 兼容性:GPU 聚合所需的客户端特性已被主流常青浏览器普遍支持(覆盖全球 95%+ 市场),但个别设备/芯片的驱动差异可能影响结果;
  • 数据规模:CPU 聚合耗时与输入数据量大致呈线性关系;GPU 聚合有搭建 shader 与上传缓冲区的前置开销,但处理更多数据的边际成本很小。大数据集(>100K)GPU 明显更快,小数据集 GPU 可能反而更慢
  • 数据分布:CPU 聚合内存与"含点网格数"成正比,GPU 聚合内存与"全部可能网格数"(含空网格)成正比。数据密集集中时 GPU 表现更好,稀疏分散时 GPU 优势减弱
  • 过滤扩展:基于 GPU 的扩展如 DataFilterExtension、MaskExtension 仅与 GPU 聚合协同工作;
  • 精度:GPU shader 仅支持 32 位浮点。虽然本图层实现了缓解精度损失的补偿措施,但 GPU 聚合结果与 CPU 未必完全一致,仓库测试保证二者具有可接受的近似一致性;
  • 访问分箱内的点:GPU 聚合不暴露"某个单元包含哪些数据点"。若你需要(例如点击网格后列出位置清单),要么使用 CPU 聚合,要么自行对数据做即时过滤。

overview.md 中给出了基于随机数据的实测性能对比(2016 款 15 英寸 MacBook Pro,CPU 2.8 GHz Intel Core i7,GPU AMD Radeon R9 M370X 2 GB):

对象数量CPU(迭代/秒)GPU(迭代/秒)备注
25K535359GPU 慢约 33%
100K119437GPU 快约 267%
1M12.7158GPU 快约 1144%

JSON 配置用法(Playground 示例)

仓库的 playground JSON 示例 examples/playground/json-examples/screen-grid.json 展示了以声明式 JSON 使用该图层的方式(数据为加州公交站点,cellSizePixels: 20opacity: 0.8、6 级 YlOrRd 色板):

{ "layers": [ { "@@type": "ScreenGridLayer", "id": "grid", "data": "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/screen-grid/ca-transit-stops.json", "opacity": 0.8, "cellSizePixels": 20, "colorRange": [ [255, 255, 178, 25], [254, 217, 118, 85], [254, 178, 76, 127], [253, 141, 60, 170], [240, 59, 32, 212], [189, 0, 38, 255] ], "gpuAggregation": true } ] }

注意此例的colorRange每个颜色都带显式 Alpha 通道,实现从低密度到高密度"由淡到浓"的叠加效果。完整的交互式示例还可在 examples/website/screen-grid 中找到(React 实现 + 旧金山自行车数据)。

测试验证

仓库使用 Vitest 对该图层做了系统测试,见 test/modules/aggregation-layers/screen-grid-layer.spec.ts:

import {ScreenGridLayer, WebGLAggregator, CPUAggregator} from '@deck.gl/aggregation-layers'; import {testLayer, generateLayerTests} from '@deck.gl/test-utils/vitest'; test('ScreenGridLayer', () => { const testCases = generateLayerTests({ Layer: ScreenGridLayer, sampleProps: {data: FIXTURES.points.slice(0, 3), getPosition}, assert: (cond, msg) => expect(cond, msg).toBeTruthy(), onBeforeUpdate: ({testCase}) => console.log(testCase.title) }); testLayer({Layer: ScreenGridLayer, testCases, onError: err => expect(err).toBeFalsy()}); });

该测试通过generateLayerTests自动生成覆盖各属性组合的用例,再经testLayer逐条渲染验证,涵盖 WebGL 与 CPU 两条聚合路径,是排查自定义配置时的重要参考。

小结

ScreenGridLayer是 deck.gl 中"屏幕空间聚合"的代表实现,核心要点可归纳为:

  1. 空间语义:分箱在屏幕像素空间完成,视口变化必然触发重新聚合,适合中小规模数据;
  2. 三条控制链getPosition+cellSizePixels决定"怎么分箱",getWeight+aggregation决定"每个格子值多少",colorDomain+colorRange+colorScaleType决定"值映射成什么颜色";
  3. 双聚合引擎gpuAggregation默认开启,但会在设备不支持时自动回退 CPU;大数据集、数据集中、需要 GPU 扩展(DataFilter/Mask)时选 GPU,需要访问格内点列表(pointIndices/points)或追求小数据量性能时选 CPU;
  4. 拾取即单元:交互回调拿到的对象是聚合后的网格单元(col/row/value/count),而非原始数据点。

如需进一步深入,可继续阅读聚合图层家族的其他成员 ContourLayer、GridLayer、HeatmapLayer、HexagonLayer,以及高级用法中的 AggregationLayer、Aggregator 接口(由 CPUAggregator 与 WebGLAggregator 实现)。

【免费下载链接】deck.glWebGL2 powered visualization framework项目地址: https://gitcode.com/GitHub_Trending/de/deck.gl

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询