- 数据可视化
【免费下载链接】vega
A visualization grammar.
heatmap变换(Vega 5.8 引入)用于将输入的栅格网格(矩阵)数据渲染为输出热力图图像(Canvas bitmap),可用来可视化二维栅格数据、密度估计结果或数学函数。本文以 heatmap 官方文档 为主体,结合 vega-geo 包中的 Heatmap 实现源码 与仓库中的 heatmap-sinusoids.vg.json、heatmap-image.vg.json 等测试规格,完整讲解参数语义、底层像素渲染原理,并给出可直接运行的实战配置。
一、功能定位:从栅格到像素
heatmap 变换接收一个数值栅格网格(raster grid / matrix)作为输入,输出是一个 Canvas bitmap 绘制到画布上。其中:
- Canvas bitmap 的内部尺寸与输入栅格网格完全一致(每个网格单元对应一个像素);
- 实际渲染出的图像尺寸由 image mark 的
width、height属性决定; - 完整示例可参考仓库中的 contour-plot 示例可视化。
在数据流层面,heatmap 变换位于 vega-geo 包中,与其配套的通常是生成栅格网格的 kde2d(二维核密度估计)或isocontour(等值线提取)变换。该变换的元数据声明为{'modifies': true},意味着它会直接修改数据元组并触发下游重排(pulse.reflow(true).modifies(as)),这一点在 Heatmap.js 的Heatmap.Definition中可见。
二、Transform 参数详解
heatmap 变换共支持 5 个参数,参数定义与默认值如下表所示:
| Property | Type | 默认值 | Description |
|---|---|---|---|
| field | Field | —(数据对象本身) | 承载栅格网格数据的字段。若未指定,则数据对象本身被解释为栅格网格 |
| color | Color | Expr | "#888"(灰色) | 设置每个像素颜色的常量颜色值或表达式。若为表达式,将以上下文datum调用,其中包含网格的$x、$y、$value、$max字段 |
| opacity | Number | Expr | $value / $max | 设置每个像素不透明度的常量值或表达式,表达式上下文同样包含$x、$y、$value、$max |
| resolve | String | 'independent' | 多个输入网格间最大值(datum.$max)的求解方式。'independent'(默认)对每个网格单独计算最大值;'shared'对所有输入网格使用单一全局最大值 |
| as | String | "image" | 输出字段名,用于写入生成的 bitmap canvas 图像 |
源码中对应的参数声明为(Heatmap.js):
Heatmap.Definition = { 'type': 'heatmap', 'metadata': {'modifies': true}, 'params': [ { 'name': 'field', 'type': 'field' }, { 'name': 'color', 'type': 'string', 'expr': true}, { 'name': 'opacity', 'type': 'number', 'expr': true}, { 'name': 'resolve', 'type': 'enum', 'values': ['shared', 'independent'], 'default': 'independent' }, { 'name': 'as', 'type': 'string', 'default': 'image' } ] };值得注意的底层细节:
- 像素级表达式上下文:
$x、$y为像素在网格内的行列坐标,$value为当前像素对应的网格数值,$max为该网格(或共享计算时全体网格)的数值最大值; - 依赖分析优化:源码中的
dependency()函数会检测颜色/不透明度函数是否引用了$x、$y、$value、$max字段(Heatmap.js)。若表达式不依赖像素数据,则只计算一次颜色/不透明度并复用为常量,从而显著提升渲染性能;反之(如opacity缺省时的$value / $max)则逐像素求值; - 默认不透明度渐变:未指定
opacity时,默认使用obj => (obj.$value / obj.$max) || 0,即按网格数值归一化得到 [0, max] 区间的不透明度渐变(Heatmap.js); - 默认颜色:未指定
color时使用中灰色#888(Heatmap.js)。
三、渲染管线:源码级逐像素实现
理解 heatmap 的关键在于其内部渲染循环(Heatmap.js):
function toCanvas(grid, obj, color, opacity) { const n = grid.width, m = grid.height, x1 = grid.x1 || 0, y1 = grid.y1 || 0, x2 = grid.x2 || n, y2 = grid.y2 || m, val = grid.values, value = val ? i => val[i] : zero, can = canvas(x2 - x1, y2 - y1), ctx = can.getContext('2d'), img = ctx.getImageData(0, 0, x2 - x1, y2 - y1), pix = img.data; for (let j=y1, k=0; j<y2; ++j) { obj.$y = j - y1; for (let i=x1, r=j*n; i<x2; ++i, k+=4) { obj.$x = i - x1; obj.$value = value(i + r); const v = color(obj); pix[k+0] = v.r; pix[k+1] = v.g; pix[k+2] = v.b; pix[k+3] = ~~(255 * opacity(obj)); } } ctx.putImageData(img, 0, 0); return can; }从源码可以确认以下事实:
- 栅格网格对象的约定结构为
{ width, height, values, x1?, y1?, x2?, y2? },其中values是长度为width * height的一维数值数组,按行优先(row-major)存储;x1/y1/x2/y2用于裁剪区域,缺省时取[0, 0, width, height]; - 使用
vega-canvas包的canvas()创建位图,通过ctx.getImageData逐像素写入 RGBA 通道:RGB 来自颜色函数,Alpha 为255 * opacity(~~做截断取整); field未指定时,代码通过_.field || identity直接使用数据元组本身作为栅格(Heatmap.js),这与文档中"数据对象本身被解释为栅格网格"的描述一致。
四、实战示例一:二维密度热力图(kde2d + heatmap)
最常见的组合是先用 kde2d 对数据流做二维核密度估计,再把输出的密度网格交给 heatmap 渲染。文档给出的核心配置如下:
{ "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"} }其中:
kde2d从x_value、y_value字段读取预计算的像素坐标,并用width、height信号指定密度计算区域;- heatmap 使用纯蓝色
steelblue作为颜色,opacity表达式即默认的归一化渐变($value / $max),数值越大的像素越不透明; - 该
opacity表达式与源码中未指定opacity时的默认行为完全一致。
仓库中的 contour-plot.vg.json 展示了更完整的实战组合:先用kde2d按Origin分组生成密度网格,再用 heatmap 通过color: {"expr": "scale('color', datum.datum.Origin)"}按分组着色,最后叠加isocontour提取的等值线,同时用信号resolve动态切换'independent'/'shared'最大值求解方式:
{ "type": "heatmap", "field": "datum.grid", "resolve": {"signal": "resolve"}, "color": {"expr": "scale('color', datum.datum.Origin)"} }另一个可直接运行的实例是 scatter-plot-heatmap.vg.json,它对cars.json的马力与油耗字段做二维密度估计,用viridis配色,并通过表达式scale('density', datum.$value / datum.$max)把归一化密度映射到颜色,cellSize以信号绑定方式在 1~16 之间切换以控制网格分辨率:
{ "type": "heatmap", "field": "datum.grid", "color": {"expr": "scale('density', datum.$value / datum.$max)"}, "opacity": 1 }五、实战示例二:数学函数热力图(纯表达式驱动)
当输入数据本身不携带栅格数值时,heatmap 依然可以工作:表达式可用$x、$y像素坐标动态计算每个像素的值。文档给出的二维正弦函数示例:
{ "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 } ] } ] }要点:
- 数据对象只声明
width: 150, height: 100,不提供values,此时栅格数值视为全零,完全由$x、$y表达式驱动颜色; color表达式引用scale('color', ...)将正弦函数值映射到spectral配色方案;opacity: 1设为常量,关闭默认的数值归一化透明度,使颜色完全由表达式决定;- 渲染出的 Canvas 位图内部尺寸为 150×100 像素,最终画布尺寸由 image mark 控制。
仓库中的完整交互版 heatmap-sinusoids.vg.json 进一步添加了scale、offset、smooth三个可绑定信号(range slider 与 checkbox),并在 image mark 上设置aspect: false与smooth开关,可以在浏览器中实时调节函数频率并观察平滑效果。
六、与 image mark 的配合:尺寸、平滑与缩放
heatmap 变换的输出必须由 image mark 消费。根据 image mark 文档,从 Vega 5.8 起 image mark 的image属性可直接接受动态生成的 Canvas 实例(url指定时忽略该属性)。关键编码属性如下:
| Property | Type | 默认值 | Description |
|---|---|---|---|
| width / height | Number | — | 最终渲染尺寸,通常绑定width/height信号 |
| aspect | Boolean | true | 是否保持图像纵横比。热力图中通常设为false,使位图拉伸铺满画布 |
| smooth | Boolean | true | 缩放时是否平滑插值。设为false时直接按像素放大(利于观察原始网格单元) |
在 heatmap-image.vg.json 中可以看到完整的端到端示例:数据经formula/timeunit提取小时与日期,kde2d以size: [365, 24]、counts: true、bandwidth: [0, 0]、cellSize: 1生成逐日逐时的温度密度网格,heatmap 通过color: {"expr": "scale('color', datum.$value)"}映射到 Viridis 配色,image mark 负责将位图拉伸到整个画布并叠加坐标轴与渐变图例,形成经典的"西雅图年平均温度"日历热力图。
七、resolve 语义:多网格最大值的归一化控制
当数据流中包含多个栅格网格(例如kde2d使用groupby生成按分组的多组网格)时,$max的含义取决于resolve参数:
independent(默认):每个网格单独计算自己的最大值,适用于各网格量纲/数值范围差异较大的场景,保证每个网格内部对比度充分;shared:先对所有输入网格求全局最大值,再统一用于各网格的归一化,适用于需要在不同网格间进行公平对比的场景。
源码中的实现为(Heatmap.js):
var shared = _.resolve === 'shared', obj = { $x: 0, $y: 0, $value: 0, $max: shared ? max(source.map(t => max(field(t).values))) : 0 }; source.forEach(t => { const v = field(t); const o = extend({}, t, obj); if (!shared) o.$max = max(v.values || []); t[as] = toCanvas(v, o, color.dep ? color : constant(color(o)), opacity.dep ? opacity : constant(opacity(o)) ); });可以看到shared模式下$max在遍历前一次性计算完成,各网格共享同一最大值;independent模式下每个网格在循环内单独求$max。
八、性能与最佳实践
综合源码实现与仓库示例,可总结以下实践建议:
- 善用依赖分析优化:颜色/不透明度表达式若未引用
$x、$y、$value、$max,则整张图只求值一次;反之则逐像素调用,表达式越简单渲染越快; - 控制网格分辨率:
kde2d的cellSize决定网格粒度,1时网格尺寸与size一致,4时宽高各减半(2 倍降采样)。分辨率越高位图越大,内存与计算开销随之上升; aspect: false保证铺满:热力图位图与画布宽高比不一致时,应显式关闭 aspect,避免出现留白;smooth按需开关:需要观察网格单元边界时关闭平滑,追求视觉连续时保持开启;- 与等值线叠加:同一密度网格可同时供 heatmap 与
isocontour消费(见 contour-plot.vg.json),形成"热力图底图 + 等值线轮廓"的层次化呈现。
九、总结
heatmap 变换是 Vega 中把"数据矩阵"转化为"可视化图像"的桥梁:配合kde2d可快速呈现二维密度分布,配合纯表达式可生成函数图像,配合isocontour可叠加等值线。理解其$x/$y/$value/$max表达式上下文、resolve归一化语义与 image mark 的尺寸/平滑控制,即可在 heatmap 官方文档 基础上自由构建从数据探索到函数可视化的各类热力图应用。
- 数据可视化
【免费下载链接】vega
A visualization grammar.
相关推荐
终极指南:如何快速将栅格图像转为矢量图像 🚀
终极指南:如何快速将栅格图像转为矢量图像 🚀 在当今数字化时代, 图像矢量化 已成为设计师和内容创作者的必备技能。想象一下,当你需要将一张低分辨率的PNG或J
图像处理deck.gl GridLayer 完全指南:网格聚合热力图的属性、渲染与底层原理
deck.gl GridLayer 完全指南:网格聚合热力图的属性、渲染与底层原理 GridLayer 是 deck.gl 聚合图层(AggregationLa
前端数据可视化3D渲染图形学Vuetify VHeatmap 组件实战指南:用 v-heatmap 构建日历热力图与数据网格
Vuetify VHeatmap 组件实战指南:用 v heatmap 构建日历热力图与数据网格 v heatmap 是 Vuetify 实验室(Labs)提供
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考