Vega geoshape 变换详解:用 shape 标记实现高性能动态地图渲染
【免费下载链接】vegaA visualization grammar.项目地址: https://gitcode.com/gh_mirrors/ve/vega
geoshape是 Vega 中专门为shape标记服务的几何变换:它将 GeoJSON 为骨架,结合仓库中 GeoShape 实现、投影注册表 与 scenegraph 渲染代码 等源码证据,深入讲解其参数语义、与geopath的选型差异,并给出可直接运行的地图示例。读完你将掌握:如何用geoshape+shape标记渲染静态或动态地图、如何配置投影与点半径,以及为什么它在 Canvas 渲染动态地图时更快。
geoshape 是什么:把 GeoJSON 变成"待渲染的形状"
geoshape变换的核心职责是:接收 GeoJSON 要素,生成一个 shape 实例——它不是一个字符串,而是一个携带了绘图逻辑(drawing function)的对象。这个实例会被写入数据元组的输出字段(默认shape),随后由 shape 标记 在渲染时调用。
关键设计在于"延迟投影":geoshape不在变换阶段把几何体换算成具体坐标或路径字符串,而是把投影与绘制动作封装进 shape 实例,等到渲染阶段才真正执行。从 shape 标记文档 可以看到,shape 标记的几何形状是在渲染时确定的,其类型专属属性只有一个shape,且"Shape 实例不能直接指定,必须由geoshape这类数据变换生成"。
该变换基于 d3-geo 库实现,官方限定其使用范围为 shape 标记:"intended for use solely with the shape mark type"。
与 geopath 的对比:形状实例 vs SVG 路径字符串
geoshape与 geopath 变换 功能相似,都是"GeoJSON → 可绘制几何"的桥接,但产出物完全不同:
| 维度 | geoshape | geopath |
|---|---|---|
| 输出物 | shape 实例(绘图函数,延迟到渲染期执行) | SVG path 字符串(如M...L...Z) |
| 配套标记 | shape | path |
| 投影时机 | 渲染期(Canvas 绘制时) | 变换期(立即生成字符串) |
| 适用场景 | Canvas 渲染的动态地图 | SVG 静态地图、需要 path 字符串的场景 |
| 默认输出字段 | shape | path |
官方文档指出:由于geoshape不生成中间 SVG 路径字符串,在使用 Canvas 渲染动态地图时可以提升性能——省去了路径字符串的生成与解析环节。如果地图的投影、缩放等参数会随交互频繁变化,geoshape是更合适的选择;如果数据只在变换阶段处理一次、输出静态 SVG,则geopath更直观。
变换参数详解
geoshape的完整参数定义可在 GeoShape.js 的 Definition 中找到,与官方文档的参数表一一对应:
| 属性 | 类型 | 说明 |
|---|---|---|
| projection | String | 要使用的投影名称。若未指定,GeoJSON 数据将不做投影处理。 |
| field | Field | 包含 GeoJSON 数据的字段。若未指定,使用完整输入数据对象。 |
| pointRadius | Number | Expr | 绘制 GeoJSONPoint与MultiPoint几何时使用的默认半径(像素)。可以使用表达式按输入 GeoJSON 的属性动态计算半径。(自 Vega 3.1 起支持) |
| as | String | 写入生成的 shape 实例的输出字段名,默认"shape"。 |
projection:投影名称
指定仓库中 projections.js 投影注册表 里注册的投影类型名称(如mercator、albers、albersUsa、orthographic、equirectangular、conicEqualArea、naturalEarth1、mollweide等,覆盖 d3-geo 全部标准投影及 d3-geo-projection 的mollweide)。对应源码中该参数类型为'projection',在解析阶段会解析为具体的投影实例。
若省略projection,则 getProjectionPath 会回退到defaultPath(即不带投影的geoPath()),此时 GeoJSON 数据按经纬度原样映射,不进行任何投影变换——这正是文档中"If unspecified, the GeoJSON data will not be projected"的底层含义。
field:GeoJSON 数据所在字段
当元组本身就是一个 GeoJSON Feature(或 Geometry)对象时无需指定;当 GeoJSON 嵌套在元组的某个字段中时,通过field指定该字段。源码中该参数默认值为'datum',即默认读取整个元组对象。
pointRadius:点几何的绘制半径
Point/MultiPoint几何在投影后只是一个点,需要指定像素半径才能"画得出来"。官方文档强调,该参数支持表达式,可以根据输入 GeoJSON 的属性动态计算半径(自版本 3.1 起)。从源码看,该参数声明为{'name': 'pointRadius', 'type': 'number', 'expr': true},expr: true表明允许传表达式值。
在 GeoShape.js 的 shapeGenerator 中,当pointRadius非空时,会为每个元组临时设置path.pointRadius(pointRadius)再执行投影,随后恢复原值,从而支持"半径随要素属性变化"的动态效果。
as:输出字段名
生成的 shape 实例写入元组的哪个字段,默认"shape"。在使用 shape 标记时,该标记的shape编码通道会读取这个字段进行渲染。
完整可运行示例
基础用法
官方文档给出的最小配置如下:
{ "type": "geoshape", "projection": "projection" }即为 GeoJSON 数据生成 shape 实例,并使用名为projection的投影(需在projections配置块中预先定义)。
真实示例:世界地图
仓库中的 world-map.vg.json 是geoshape的典型完整应用(上图即其渲染结果)。其核心结构是:先加载 TopoJSON 世界数据并抽取countries要素,再定义graticule(经纬网),最后用两个 shape 标记分别绘制经纬网与国界:
{ "data": [ { "name": "world", "url": "data/world-110m.json", "format": {"type": "topojson", "feature": "countries"} }, { "name": "graticule", "transform": [{"type": "graticule"}] } ], "projections": [ { "name": "projection", "type": {"signal": "type"}, "scale": {"signal": "scale"}, "rotate": [{"signal": "rotate0"}, {"signal": "rotate1"}, {"signal": "rotate2"}], "center": [{"signal": "center0"}, {"signal": "center1"}], "translate": [{"signal": "translate0"}, {"signal": "translate1"}] } ], "marks": [ { "type": "shape", "from": {"data": "graticule"}, "encode": { "update": { "strokeWidth": {"value": 1}, "stroke": {"signal": "invert ? '#444' : '#ddd'"}, "fill": {"value": null} } }, "transform": [{"type": "geoshape", "projection": "projection"}] }, { "type": "shape", "from": {"data": "world"}, "encode": { "update": { "strokeWidth": {"signal": "+borderWidth"}, "stroke": {"signal": "invert ? '#777' : '#bbb'"}, "fill": {"signal": "invert ? '#fff' : '#000'"}, "zindex": {"value": 0} }, "hover": { "strokeWidth": {"signal": "+borderWidth + 1"}, "stroke": {"value": "firebrick"}, "zindex": {"value": 1} } }, "transform": [{"type": "geoshape", "projection": "projection"}] } ] }这个例子展示了geoshape的几个关键配合方式:
- 投影由信号驱动:
projection的类型、比例尺、旋转角、中心点全部绑定到信号,用户可通过控件实时调整(mercator、albers、orthographic等十余种投影可切换); - 悬停高亮:通过
hover编码集(firebrick 描边 + 提升zindex)实现鼠标悬停时国家边界高亮; - 经纬网与国界共享同一投影:两个 shape 标记各自携带一个
geoshape变换,均引用projection投影。
交互式投影示例
仓库 geoshape.vg.json 提供了一份更聚焦的官方示例:将投影类型、scale(50–1000)、rotate(-180–180 / -90–90)和center全部暴露为可绑定控件,数据仅含world(TopoJSONcountries)与graticule两个数据集,并带有一个过滤条件——当投影为albersUsa时仅绘制美国(datum.id === 840),以适配该投影仅覆盖美国领土的特性。
源码实现剖析:shape 实例是如何生成与消费的
生成端:GeoShape 变换算子
GeoShape.js 继承自 vega-dataflow 的Transform基类,其运行逻辑(transform(_, pulse)方法)如下:
- 参数未变化时的增量更新:仅对
pulse.ADD(新增)的元组写入 shape 实例:out.visit(flag, t => t[as] = shape); - 参数变化时全量重建:当投影、字段或 pointRadius 任一参数被修改(
_.modified())时,重新构造 shape 生成器并对全部元组执行materialize().reflow(),触发重排; - 字段标记:返回
out.modifies(as),向数据流声明"输出字段as被修改"。
其metadata声明为{'modifies': true, 'nomod': true}(表示会修改数据元组)。核心的shapeGenerator返回的 shape 函数在无pointRadius时形如_ => path(field(_)),即"给定元组 → 取出 GeoJSON → 用投影路径绘制";同时它暴露一个context方法把渲染上下文(Canvas context 或边界计算 context)注入底层 d3-geo path,从而让 shape 实例既能绘制也能参与包围盒计算。
投影端:getProjectionPath 与投影注册表
projections.js 中的getProjectionPath(proj)返回(proj && proj.path) || defaultPath。也就是说:
- 若指定了投影,返回该投影预生成的
geoPath().projection(p); - 若未指定,返回全局默认的
geoPath()(无投影)。
这也印证了"未指定投影即不投影"的文档表述。同文件还定义了完整的投影注册表与projectionProperties(clipAngle、clipExtent、scale、translate、center、rotate、parallels、precision、reflectX、reflectY以及 d3-geo-projection 扩展属性),任何投影类型名都需要能在这里解析到。
消费端:scenegraph 的 shape 绘制
shape 实例最终由 scenegraph 渲染模块消费。path/shapes.js 中的shape(context, item)函数是入口:
export function shape(context, item) { return (item.mark.shape || item.shape) .context(context)(item); }它取出元组上的 shape 实例(优先用mark.shape),注入渲染上下文并调用。Canvas 绘制路径(markItemPath.js、markMultiItemPath.js)中,draw阶段调用context.beginPath()后执行shape(context, items),直接发出绘制命令;bound阶段则用同一 shape 实例在边界计算 context 中求包围盒。由于 shape 实例携带的正是投影后的几何绘制逻辑,渲染期直接出图,无需任何中间字符串解析——这就是 Canvas 动态地图场景下geoshape比geopath更快的原因。
使用注意事项
- 只能搭配 shape 标记使用:
geoshape的产出物是 shape 实例而非路径字符串,无法用于 path 标记;需要 SVG path 字符串时请改用 geopath。 - 投影必须先在
projections中定义:projection参数引用的是投影配置块中的名称(参考 projections 文档),而非投影类型本身。 - GeoJSON 数据的来源:可直接在变换上游用
geojson变换把经纬度(lon/lat字段)组装成 GeoJSON FeatureCollection(见 GeoJSON 变换实现 及其 测试用例),也可通过 TopoJSON 格式加载并抽取要素(如world-110m.json)。 - 点要素需显式设置半径:若数据包含
Point/MultiPoint几何,建议通过pointRadius(数值或表达式)指定绘制半径,否则点要素可能不可见或过小。
小结
geoshape是 Vega 地理可视化的高性能路径:它将"投影 + 几何绘制"封装为渲染期执行的 shape 实例,与shape标记配合,在 Canvas 动态地图(交互式投影切换、缩放旋转、悬停高亮)场景下避免了中间 SVG 路径字符串的生成与解析开销。理解其四个参数(projection、field、pointRadius、as)以及"变换期生成实例、渲染期执行绘图"的两阶段模型,即可在 Vega 中构建出交互流畅的地图应用。
【免费下载链接】vegaA visualization grammar.项目地址: https://gitcode.com/gh_mirrors/ve/vega
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考