vega-geo 地理数据变换实战指南:从投影、GeoJSON 到密度等值线完整解析
【免费下载链接】vegaA visualization grammar.项目地址: https://gitcode.com/gh_mirrors/ve/vega
导读
vega-geo是 Vega 可视化语法(vega 项目)中负责地理数据处理的核心包,为 Vega 数据流提供一组地理变换(transform),涵盖制图投影(projection)、GeoJSON 数据加工、经纬度投影为像素坐标、经纬网(graticule)生成,以及基于二维核密度估计(KDE)的等值线(isocontour)与热力图(heatmap)可视化。阅读完本文,你将掌握vega-geo全部 8 个公开变换与 1 个内部变换的用途、参数、实现原理,并能用真实仓库中的示例规格(如 contour-plot.vg.json、world-map.vg.json)快速搭建地图与密度可视化。
vega-geo 包概览:为 Vega 数据流提供的地理变换集合
vega-geo(packages/vega-geo/README.md)的定位是 "Geographic data transforms for Vega dataflows"。它不直接渲染图形,而是作为 Vega 数据流(dataflow)中的变换算子,把地理数据加工成下游 mark 可以直接消费的形式(如 SVG path 字符串、shape 实例、GeoJSON 几何、Canvas 位图等)。
从包的入口文件 packages/vega-geo/index.js 可以看到它导出 10 个变换:
| 变换 | 类型 | 一句话用途 |
|---|---|---|
contour | 公开变换(已废弃) | 基于核密度估计生成等值线,功能已被kde2d+isocontour取代 |
geojson | 公开变换 | 把经纬度点或 GeoJSON 要素合并为单个 FeatureCollection |
geopath | 公开变换 | 把 GeoJSON 要素映射为 SVG path 字符串 |
geopoint | 公开变换 | 把 (longitude, latitude) 投影为 (x, y) 像素坐标 |
geoshape | 公开变换 | 为数据项附加一个 shape 绘制实例(延迟到渲染期执行投影) |
graticule | 公开变换 | 生成经纬网 GeoJSON 参考网格 |
heatmap | 公开变换 | 把栅格数据渲染为 Canvas 位图 |
isocontour | 公开变换 | 基于栅格数据生成等值线 GeoJSON |
kde2d | 公开变换 | 对点数据执行二维核密度估计,输出栅格矩阵 |
projection | 内部变换 | 维护制图投影实例,供其他地理变换引用 |
其中projection在 README 中被明确标注为internal transform,即普通用户不会直接在数据流中书写它,而是通过 Vega 规格顶层的projections声明来配置(详见下文)。
从 packages/vega-geo/package.json 可以看到该包的依赖关系,这直接反映了它的实现基石:
d3-geo:提供各类投影构造器、geoPath与geoGraticule;d3-array/d3-color:分别用于极值计算与热力图像素颜色解析;vega-dataflow:提供Transform基类与脉冲(pulse)机制;vega-projection:提供投影注册表与getProjectionPath;vega-canvas:用于热力图离屏 Canvas 位图创建;vega-statistics:提供密度估计相关统计工具。
Projection 内部变换:一切地图变换的地基
投影类型注册表
Projection变换(packages/vega-geo/src/Projection.js)负责"维护一个制图投影实例"。它的核心逻辑是:根据type参数从vega-projection包的注册表中取到对应构造器,创建投影,并把规格中出现的投影属性逐一设置到该实例上。
投影注册表位于 packages/vega-projection/src/projections.js,Vega 内置的投影类型全部来自d3-geo,外加d3-geo-projection的mollweide:
albers、albersUsa、azimuthalEqualArea、azimuthalEquidistant、conicConformal、conicEqualArea、conicEquidistant、equalEarth、equirectangular、gnomonic、identity、mercator、mollweide、naturalEarth1、orthographic、stereographic、transverseMercator。
从源码看,类型名是大小写不敏感的((type || 'mercator').toLowerCase()),默认为mercator;若传入未注册的类型,会抛出Unrecognized projection type错误。每个创建的投影都会被附上type属性,并预绑定一个geoPath().projection(p)的路径生成器p.path——这正是GeoPath/GeoShape变换通过getProjectionPath(proj)(见 packages/vega-projection/src/projections.js)直接取用的对象。
投影属性:直接透传 d3-geo
projectionProperties数组(packages/vega-projection/src/projections.js)定义了 Vega 透传给投影实例的标准属性,覆盖 d3-geo 常规属性与 d3-geo-projection 扩展属性:
- 常规:
clipAngle、clipExtent、scale、translate、center、rotate、parallels、precision、reflectX、reflectY; - 扩展:
coefficient、distance、fraction、lobes、parallel、radius、ratio、spacing、tilt。
这些属性在 docs/docs/projections.md 中有完整说明,关键几点:
scale:默认值因投影而异,且不同投影之间的 scale 数值不可直接比较;translate:默认[480, 250],把 (0°, 0°) 置于 960×500 区域中心;rotate:两或三个元素[λ, φ, γ],依次对应 yaw、pitch、roll;pointRadius:绘制 GeoJSONPoint/MultiPoint时的默认点半径(像素),默认 4.5;precision:自适应重采样的 Douglas–Peucker 阈值,默认 √0.5 ≈ 0.70710;fit/extent/size:让投影自动适配给定 GeoJSON 数据(见下节)。
fit 自动适配与 collectGeoJSON
Projection变换还支持fit参数——给定 GeoJSON 数据后自动计算translate和scale,使数据正好落入指定的extent或size像素区域(packages/vega-geo/src/Projection.js 中的fit()函数:extent存在则用fitExtent,否则用fitSize)。
fit的输入会被collectGeoJSON()归一化:接受单个 Feature/FeatureCollection,也接受数组(数组中的FeatureCollection会被展开,裸几何对象会被包装成Feature),最终合并为一个FeatureCollection。这一能力有测试直接覆盖——packages/vega-geo/test/projection-test.js 中验证了orthographic投影在size: [500, 500]且fit传入经纬网与Sphere时,scale()变为 250、translate落在 (250, 250),并验证了fit输入包含null数据时依然稳健。
fit与geojson变换的搭配是常见用法:先用geojson变换把散落的经纬度点合并成 GeoJSON,再通过信号把结果喂给投影的fit,实现"让地图自动缩放到数据范围"。
GeoJSON 变换:把点与要素合并为 FeatureCollection
geojson变换(packages/vega-geo/src/GeoJSON.js)把"经纬度点数组"或"既有 GeoJSON 要素"整合进一个FeatureCollection,输出作为该变换的 value(可通过signal绑定为信号值)。它最典型的用途就是给投影的fit参数供数。
参数(定义见 docs/docs/transforms/geojson.md):
| 属性 | 类型 | 说明 |
|---|---|---|
fields | Field[] | 分别包含经度、纬度值的两个字段 |
geojson | Field | 包含 GeoJSON Feature 对象的字段;当fields与geojson均未指定时,默认使用恒等函数(把输入数据对象本身当作 GeoJSON Feature) |
从源码实现(packages/vega-geo/src/GeoJSON.js)可以看到处理细节:若提供fields,遍历数据时会对每个点做数值校验((x = +x) === x排除 NaN/字符串),把有效的[lon, lat]点收集进points,最后追加一个MultiPoint要素到特征数组;若提供geojson字段,则把每个 tuple 取出的对象作为独立要素压入数组。最终输出{type: 'FeatureCollection', features: ...}。
官方用法示例(docs/docs/transforms/geojson.md):把经纬度字段合并并通过signal绑定:
{ "type": "geojson", "fields": ["longitude", "latitude"], "signal": "geodata" }随后在投影声明中引用:
"projections": [ { "name": "proj", "type": "mercator", "fit": {"signal": "geodata"} } ]GeoPath 与 GeoShape:两种 GeoJSON 渲染策略
GeoPath:直接产出 SVG path 字符串
geopath变换(packages/vega-geo/src/GeoPath.js)把 GeoJSON 要素映射为SVG path 字符串,适用于pathmark。它依赖 d3-geo 的geoPath完成投影与路径生成。
参数(docs/docs/transforms/geopath.md):
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
projection | String | — | 投影名;不指定则不对 GeoJSON 做投影 |
field | Field | 恒等 | 存放 GeoJSON 数据的字段;不指定则把整个数据对象当作 GeoJSON |
pointRadius | Number|Expr | — | 绘制Point/MultiPoint时的默认半径(像素),支持表达式按要素属性动态设定 |
as | String | "path" | 输出字段名 |
用法:
{ "type": "geopath", "projection": "projection" }源码实现要点:变换会缓存this.value中的 path 生成器;当参数变化(_.modified())时重建路径并reflow全部数据;否则仅处理新增/修改的 tuple。pointRadius在遍历前后会被临时设置并恢复(initPath),保证多轮处理互不干扰。输出写入t[as],并声明modifies元数据。
GeoShape:延迟到渲染期的 shape 实例
geoshape变换(packages/vega-geo/src/GeoShape.js)与geopath功能类似,但不立即生成 SVG 字符串,而是为每个数据项附加一个"形状生成器"(shape instance),渲染时再发出绘制命令。由于投影被延迟到渲染阶段,在 Canvas 渲染的动态地图场景下通常性能更好。它专用于shapemark。
参数(docs/docs/transforms/geoshape.md)与geopath几乎一致,默认输出字段为"shape":
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
projection | String | — | 投影名;不指定则不做投影 |
field | Field | "datum" | 存放 GeoJSON 数据的字段 |
pointRadius | Number|Expr | — | Point/MultiPoint默认半径 |
as | String | "shape" | 输出字段名 |
源码中,shapeGenerator()(packages/vega-geo/src/GeoShape.js)返回的 shape 函数还带context方法,把渲染上下文透传给底层geoPath,这正是 Canvas 直接绘制的基础。其metadata同时声明了modifies与nomod。
如何选择
按官方文档(docs/docs/transforms/geopath.md、docs/docs/transforms/geoshape.md)的说明:需要立即拿到 SVG path 字符串(如静态导出、SVG 渲染)用geopath;Canvas 动态地图优先geoshape。world-map.vg.json 就是一个典型:世界地图与国家边界都使用geoshape+shapemark,经纬网同样如此。
GeoPoint 变换:经纬度点到像素坐标
geopoint变换(packages/vega-geo/src/GeoPoint.js)把 (longitude, latitude) 对投影为 (x, y) 像素坐标,适合把"散点式地理位置"放到 symbol 等 mark 上。
参数(docs/docs/transforms/geopoint.md):
| 属性 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
projection | String | ✔ | — | 投影名 |
fields | Field[] | ✔ | — | 依次为经度、纬度字段 |
as | String[] | — | ["x", "y"] | 输出字段名 |
用法:
{ "type": "geopoint", "projection": "myprojection", "fields": ["lon", "lat"] }实现要点(packages/vega-geo/src/GeoPoint.js):调用proj([lon(t), lat(t)]),若投影返回结果则写入t[x]/t[y],否则置为undefined——这一分支保证了超出投影范围(如裁剪圈外)的点不会产生无效坐标。geopoint.vg.json(docs/docs/transforms/geopoint.vg.json)展示了它与albersUsa投影配合,把美国州府经纬度(lon/lat字段)投影为x/y并绘制 symbol 的完整流程。
Graticule 变换:生成经纬网参考网格
graticule变换(packages/vega-geo/src/Graticule.js)生成地图经纬网:一组均匀的经线(meridians)与纬线(parallels),用于展示投影变形。它内部直接使用 d3-geo 的geoGraticule()生成器,输出一个包含单个 GeoJSON 对象的新数据流,可交给geopath/geoshape绘制。
默认网格:±80° 纬度之间每 10° 一条经纬线,极地区域经线每 90° 一条。
参数(docs/docs/transforms/graticule.md):
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
extentMajor | Array[] | — | 主要经纬网的边界(两元素坐标数组) |
extentMinor | Array[] | — | 次要经纬网的边界 |
extent | Array[] | — | 同时设置 major 与 minor 边界 |
stepMajor | Number[] | [90, 360] | 主要经纬网步长(度) |
stepMinor | Number[] | [10, 10] | 次要经纬网步长(度) |
step | Number[] | — | 同时设置 major 与 minor 步长 |
precision | Number | 2.5 | 经纬网精度(度) |
用法:
{"type": "graticule", "stepMinor": [15, 15]}源码实现(packages/vega-geo/src/Graticule.js)非常直白:遍历参数对象,凡是对应geoGraticule生成器上存在同名函数(如extent、step、precision)就调用设置;然后执行gen()得到新的 GeoJSON,用replace/ingest更新数据流中的唯一数据对象。
密度可视化管线:KDE2D → Isocontour / Heatmap
从 Vega 5.8 起,vega-geo提供了一套"点数据 → 密度栅格 → 等值线/热力图"的完整管线,官方文档(docs/docs/transforms/kde2d.md、docs/docs/transforms/isocontour.md、docs/docs/transforms/heatmap.md)统一用 contour-plot 示例 演示。这套管线尤其适合海量 2D 点数据的可缩放表达。
KDE2D:二维核密度估计
kde2d变换(packages/vega-geo/src/KDE2D.js)对输入点数据执行二维核密度估计,输出一个或多个栅格矩阵({width, height, values, scale, translate, ...}),供isocontour/heatmap消费。
参数:
| 属性 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
size | Number[] | ✔ | — | 密度估计的空间范围[width, height](像素) |
x | Field | ✔ | — | x 坐标字段 |
y | Field | ✔ | — | y 坐标字段 |
weight | Field | — | — | 数据点权重;不指定则每个点权重为 1 |
groupby | Field[] | — | — | 分组字段;不指定则全部数据作为一组 |
cellSize | Number | — | 4 | 空间近似粒度:默认 4 意味着宽高各缩减 2 倍;设为 1 则输出栅格与size完全一致 |
bandwidth | Number[] | — | 自动 | KDE 核带宽(像素);两元素数组分别指定 x/y 带宽,单元素同时作用于两轴;小于 0 或未指定时自动确定 |
counts | Boolean | — | false | false输出概率估计,true输出平滑计数 |
as | String | — | "grid" | 输出栅格字段名 |
源码要点(packages/vega-geo/src/KDE2D.js):partition()按groupby字段把数据切成若干组(组内附带dims记录分组键值,输出时回填到 tuple 上);每组独立执行density2D()估计。当输入未变化且参数未修改时,直接StopPropagation短路,避免无谓重算。
Isocontour:从栅格到等值线 GeoJSON
isocontour变换(packages/vega-geo/src/Isocontour.js)以一个或多个数值栅格为输入,生成等值线(isoline)GeoJSON 几何流,可用geoshape/geopath绘制。每条等值线是常数值的等值面边界。
参数(docs/docs/transforms/isocontour.md):
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
field | Field | 恒等 | 存放栅格数据的字段;不指定则数据对象本身视为栅格 |
thresholds | Number[] | — | 显式等值线阈值数组;一旦指定,levels/nice/resolve/zero均被忽略 |
levels | Number | — | 期望的等间距等值线数量(有thresholds时忽略) |
nice | Boolean | false | 是否把阈值对齐到"友好"整数值(可能使实际条数偏离levels) |
resolve | String | "independent" | 多栅格阈值求解方式:'independent'每个栅格单独计算;'shared'所有栅格共用一套阈值 |
zero | Boolean | true | 阈值是否包含 0 作为基线 |
smooth | Boolean | true | 是否用线性插值平滑等值线多边形(密度估计场景下忽略) |
scale | Number|Number[] | — | 输出坐标缩放(可传[sx, sy]),用于匹配坐标空间 |
translate | Number[] | — | 输出坐标平移[dx, dy] |
as | String | "contour" | 输出字段名(设为null则直接输出几何对象) |
源码实现(packages/vega-geo/src/Isocontour.js)值得展开:每个输入栅格通过contour.size([w, h])(grid.values, thresholds)生成 GeoJSON 等值线;随后transformPaths()依据栅格自带的scale/translate(或显式参数)对坐标做仿射变换——x' = (x - x1) * sx + tx,并在sx * sy < 0(坐标翻转)时反转环的缠绕方向以维持几何正确性;最后用rederive()把源数据属性复制到输出 tuple,保证下游可以按分组字段着色。resolve: 'shared'时,阈值基于所有栅格最大值的全局统计计算。
Heatmap:把栅格渲染为 Canvas 位图
heatmap变换(packages/vega-geo/src/Heatmap.js)把输入数值栅格渲染为 Canvas 位图(bitmap),输出可用imagemark 绘制。位图尺寸与栅格一致,实际显示尺寸由 image mark 的 width/height 决定。
参数(docs/docs/transforms/heatmap.md):
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
field | Field | 恒等 | 存放栅格数据的字段 |
color | Color|Expr | "#888" | 像素颜色;表达式可读取$x、$y、$value、$max字段 |
opacity | Number|Expr | $value / $max | 像素不透明度;表达式同样可读取四个$字段 |
resolve | String | "independent" | $max求解方式:每个栅格独立计算或全局共享一个最大值 |
as | String | "image" | 输出 Canvas 位图的字段名 |
源码实现(packages/vega-geo/src/Heatmap.js)很巧妙:为每个像素构造一个代理数据对象({$x, $y, $value, $max}),颜色/不透明度函数若不依赖单个像素(通过accessorFields检测是否引用$x/$y/$value/$max),则只计算一次并用constant()缓存,从而大幅优化逐像素开销;最终通过vega-canvas创建离屏画布、逐像素写入 RGBA(pix[k+3] = ~~(255 * opacity(obj)))。
官方示例一:密度热力图(docs/docs/transforms/heatmap.md):
{ "type": "kde2d", "x": "x_value", "y": "y_value", "size": [{"signal": "width"}, {"signal": "height"}], "as": "grid" }, { "type": "heatmap", "field": "grid", "color": "steelblue", "opacity": {"expr": "datum.$value / datum.$max"} }官方示例二:数学函数热力图(不依赖输入栅格,直接用表达式逐像素填色):
{ "signals": [ {"name": "scale", "value": 0.05} ], "scales": [ { "name": "color", "type": "linear", "domain": [-1, 1], "range": {"scheme": "spectral"} } ], "data": [ { "name": "heatmap", "values": [{"width": 150, "height": 100}], "transform": [ { "type": "heatmap", "color": { "expr": "scale('color', sin(scale * (datum.$x + datum.$y)) * sin(scale * (datum.$x - datum.$y)))" }, "opacity": 1 } ] } ] }Contour:已被取代的旧变换
contour变换(packages/vega-geo/src/Contour.js)在 README 中即被标注为deprecated(自 Vega 5.8 起,未来主版本可能移除)。它同时提供了 d3-contour 中contours与densityContour的能力:要么直接对values数组(width×height 数值网格)算等值线,要么对输入点数据做核密度估计后生成等值线。
官方推荐迁移方案:改用更灵活的kde2d+isocontour(或kde2d+heatmap)组合。参数表(docs/docs/transforms/contour.md)如下,仅供理解遗留规格:
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
size | Number[] | ✔ | 等值线计算区域[width, height];提供values时为输入数据尺寸,做密度估计时为输出视图像素尺寸 |
values | Number[] | — | 直接给定数值网格;不指定则对输入点数据做密度估计 |
x/y | Field | — | 密度估计的像素坐标字段 |
weight | Field | — | 数据点权重字段 |
cellSize | Number | — | 密度计算单元大小 |
bandwidth | Number | — | KDE 带宽 |
smooth | Boolean | true | 是否线性插值平滑 |
thresholds | Number[] | — | 显式阈值数组(指定后忽略count/nice) |
count | Number | — | 期望等值线条数 |
nice | Boolean | false | 阈值是否对齐友好数值 |
从源码可见(packages/vega-geo/src/Contour.js),它实际上是"借用"了KDE2D.js的params与Isocontour.js的transform坐标修正逻辑,内部结构完全可以被新管线替代。
端到端实战:contour-plot 示例拆解
仓库自带的 docs/examples/contour-plot.vg.json 把上述密度管线串成了一个完整可运行的 Vega 规格,数据来自 docs/data/cars.json。整个流程分四步:
- 数据过滤:剔除
Horsepower或Miles_per_Gallon为空的行; - 密度估计:
kde2d按Origin(产地)分组,把经scale('x', ...)/scale('y', ...)换算后的像素坐标作为 x/y,size取视图宽高信号,bandwidth与counts由交互控件驱动; - 等值线:
isocontour读取grid字段,resolve跟随信号在independent/shared间切换,生成 3 层等值线; - 渲染:三层 mark 各司其职——
symbol画散点,imagemark 叠加heatmap生成的密度底色(颜色取自Origin的序数色标),pathmark 用geopath绘制等值线轮廓。
对应的标记/变换组合验证了本文所述的全部要点:heatmap输出喂给image,isocontour输出喂给geopath,而geopath在这里未指定投影(field: "datum.contour"的等值线坐标已是像素空间,无需再投影)。
地图场景则可参考 docs/examples/world-map.vg.json:通过信号绑定投影type、scale、rotate、center、translate,配合graticule变换生成的经纬网与geoshape绘制的国家边界,实现了可交互切换投影类型的世界地图——这也与 packages/vega-geo/test/projection-test.js 中验证的投影适配逻辑互相印证。
源码地图与扩展指引
如果你要在 Vega 数据流中二次开发或调试,以下文件值得优先阅读:
- 变换入口与导出:packages/vega-geo/index.js;
- 每个变换的
Definition都声明了类型元数据(type、metadata、params),这是 Vega 解析器校验规格参数的依据,分布在 packages/vega-geo/src/ 各文件; - 工具函数:packages/vega-geo/src/util/contours.js、packages/vega-geo/src/util/density2D.js、packages/vega-geo/src/util/quantize.js;
- GeoJSON 类型常量:packages/vega-geo/src/constants.js;
- 投影注册与属性透传:packages/vega-projection/src/projections.js;
- 测试用例:packages/vega-geo/test/projection-test.js 与 packages/vega-geo/test/geojson-test.js。
扩展自定义投影也很简单:vega-projection提供了projection(name, constructor)注册接口(见 packages/vega-projection/src/projections.js),注册后即可在规格的projections中直接引用;vega-projection-extended包则一次性注册 d3-geo-projection 的全部扩展投影。更完整的投影类型与属性清单,见 docs/docs/projections.md。
小结
vega-geo用一套小而精的变换集合覆盖了 Vega 地理可视化的全部基础能力:projection维护投影实例并支持fit自动适配;geojson为fit聚合数据;geopath/geoshape/geopoint分别面向 SVG 路径、Canvas shape 与散点三种渲染需求;graticule提供地图参考网格;kde2d、isocontour、heatmap组成现代的密度等值线与热力图管线,而旧有的contour已进入废弃状态。理解这些变换的参数语义与底层实现(大多直接构建在 d3-geo 之上),你就能在 Vega 规格中组合出从世界地图到密度等高线的各类地理可视化。
【免费下载链接】vegaA visualization grammar.项目地址: https://gitcode.com/gh_mirrors/ve/vega
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考