- 数据可视化
【免费下载链接】vega
A visualization grammar.
本文基于当前仓库 packages/vega-util 的 README.md 与其 TypeScript 源码展开,全面讲解该工具包中函数生成器、类型检查、类型转换、对象/数组/日期/字符串工具、日志与错误处理等全部 API,并结合底层实现说明其设计意图与应用场景。
一、vega-util 是什么
vega-util 是 Vega 可视化语法体系中所有 JavaScript 模块共享的通用工具包,其 package.json 中将其定位为 "JavaScript utilities for Vega"。它提供了一组贯穿 Vega 各模块的辅助方法,包括:
- 函数生成器(Function Generators):如访问器(accessor)、比较器(comparator)、常量函数、防抖函数等;
- 类型检查器(Type Checkers):判断一个值是数组、布尔、日期、函数、可迭代对象、数字、对象、正则还是字符串;
- 类型转换(Type Coercion):将值强制转换为布尔、日期、数字或字符串;
- 对象、数组、日期、字符串的扩展操作:LRU 缓存、快速映射、配置合并、范围夹取、缩放/平移变换、季度计算、字符串填充与截断等;
- 日志(Logging)与错误(Errors):分级日志器与统一错误抛出。
从源码结构看,src/index.ts 是统一出口,全部 API 均以命名导出或默认导出的形式对外暴露。该包使用 TypeScript 编写(构建产物为 ESM,"type": "module"),测试通过tape运行(见 package.json 的test脚本),类型声明文件为build/index.d.ts。
作为 Vega 生态的最底层依赖,vega-util 被 vega-dataflow、vega-parser、vega-scale、vega-transforms 等几乎所有核心包引用。例如数据流引擎需要用accessor记录字段依赖,解析器需要用mergeConfig合并配置,变换模块需要用field生成字段访问器。理解 vega-util,是理解整个 Vega 运行时数据流的关键一步。
二、安装与基本用法
vega-util 作为 npm 包发布,在 Node.js 环境中可直接安装使用:
npm install vega-util引入方式遵循 ESM 规范:
import * as vega from 'vega-util'; // 或者按需引入 import { field, compare, logger } from 'vega-util';在浏览器端,vega-util 也会随 Vega 主包(packages/vega)一起打包,所有方法挂载在全局vega命名空间下(即下文 API 中vega.accessor、vega.field等调用形式)。本文所有 API 均以vega.<method>的文档形式呈现。
三、函数生成器(Functions)
函数生成器是 vega-util 最具 Vega 特色的部分,它们生成的函数往往带有字段元数据,供数据流引擎做依赖追踪。
3.1 accessor / accessorFields / accessorName
vega.accessor(function[, fields, name])用于给一个普通函数打上“访问器”标记:将依赖的字段名数组写入函数的fields属性,将名称写入fname属性,并返回原函数。
var f = vega.accessor(function(d) { return d.foo; }, ['foo'], 'foo'); vega.accessorFields(f); // ['foo'] vega.accessorName(f); // 'foo'其 TypeScript 实现见 src/accessor.ts,核心逻辑只有一行Object.assign(fn, { fields: fields || [], fname: name }),并定义了Accessor<T>接口,声明了fields?: readonly string[]与fname?: string两个可选属性。accessorFields(accessor)返回依赖字段数组(未设置时为null),accessorName(accessor)返回名称(未设置时为null)。
文档特别提醒:fields与fname是内部实现细节,客户端不应直接访问,而应通过accessorFields与accessorName这两个方法读取——这正是为了将来替换内部实现留出余地。有了字段依赖信息,Vega 数据流便能追踪数据元组的字段依赖关系,并在聚合等场景中生成恰当的输出名称。
3.2 ascending 与 compare
vega.ascending(a, b)是一个比较器函数,用于对原始值和 Date 值进行升序排序。
vega.compare(fields[, orders])则根据一组字段与可选的排序方向生成比较器:
fields必须是字符串、访问器函数,或由二者组成的数组。字符串表示要排序的对象属性名,按优先级顺序排列,支持嵌套属性(如'foo.bar.baz');orders必须是字符串或字符串数组,合法值为'ascending'或'descending'。若省略、比fields短、或包含其他值,对应字段默认按升序处理。
源码实现在 src/compare.ts:它会遍历字段,为每个字段生成访问器与方向系数(descending为-1,否则为1),再依据字段数量选择单字段比较器compare1或多字段比较器compareN。多字段比较按优先级依次比较,直到出现非零结果;同时会把所有字段名汇总进最终比较器的fields元数据(fmap),从而让数据流感知该比较操作依赖哪些字段。注意ascending对null/undefined值做了特殊处理(始终排在前),并对 Date 做了时间戳归一化比较。
var cmp = vega.compare(['date', 'price'], ['ascending', 'descending']); rows.sort(cmp); // 先按 date 升序,再按 price 降序3.3 constant
vega.constant(value)返回一个恒返回value的函数;如果value本身是函数,则直接原样返回。实现在 src/constant.ts。
var c = vega.constant(42); c(); // 423.4 debounce
vega.debounce(delay, func)生成“防抖”函数:自上次调用起经过delay毫秒后才真正执行func。调用时最多向func传递一个参数,且不保留this上下文。典型应用场景是交互事件的节流处理,如 Vega 视图中拖拽、缩放等高频事件的回调延迟。
3.5 field
vega.field(field[, name])根据字段字符串生成访问器函数,字段字符串支持嵌套属性(如'foo.bar.baz')。可选name参数指定生成函数的访问器名称;未指定时使用字段字符串本身。
var fooField = vega.field('foo'); fooField({foo: 5}); // 5 vega.accessorName(fooField); // 'foo' vega.accessorFields(fooField); // ['foo'] var pathField = vega.field('foo.bar', 'path'); pathField({foo: {bar: 'vega'}}); // 'vega' pathField({foo: 5}); // undefined vega.accessorName(pathField); // 'path' vega.accessorFields(pathField); // ['foo.bar']源码 src/field.ts 显示其内部先调用splitAccessPath将路径字符串拆为数组,再由 src/getter.ts 生成取值函数;若路径只有一段,访问器名称即该字段名,否则使用完整路径字符串。
3.6 预置访问器:id / identity / zero / one / truthy / falsy
src/accessors.ts 中定义了一组开箱即用的访问器:
vega.id(object):返回输入对象的id属性值(实现为field('id'));vega.identity(value):返回输入值本身;vega.zero():恒返回0;vega.one():恒返回1;vega.truthy():恒返回true;vega.falsy():恒返回false。
这些预置访问器均通过accessor标注了fname(如'identity'、'zero'、'one'、'true'、'false')与空字段列表,常用于变换的默认参数或占位回调。
3.7 key
vega.key(fields[, flat])生成一个访问器函数,为对象的一组字段拼接出可用作对象属性名的“键字符串”。fields必须是字符串或字符串数组;生成结果的访问器名称为'key'。可选参数flat(默认false)为true时,将字段名视为扁平属性名,跳过点号/方括号所表示的嵌套查找。
var keyf = vega.key(['foo', 'bar']); keyf({foo:'hi', bar:5}); // 'hi|5' vega.accessorName(keyf); // 'key' vega.accessorFields(keyf); // ['foo', 'bar']该功能在数据分组(如 groupby)、去重和哈希索引中广泛使用。
四、类型检查器(Type Checkers)
类型检查器用于判断 JavaScript 值的类型,均返回布尔值:
| API | 说明 |
|---|---|
vega.isArray(value) | 是否为 Array 实例 |
vega.isBoolean(value) | 是否为 Boolean 实例 |
vega.isDate(value) | 是否为 Date 实例 |
vega.isFunction(value) | 是否为 Function 实例 |
vega.isIterable(value) | 是否支持可迭代协议(iterable protocol) |
vega.isNumber(value) | 是否为 Number 实例 |
vega.isObject(value) | 是否为 Object 实例 |
vega.isRegExp(value) | 是否为 RegExp 实例 |
vega.isString(value) | 是否为 String 实例 |
它们各自对应 src/isArray.ts、src/isBoolean.ts 等独立源文件。以 src/isObject.ts 为例,其实现比typeof value === 'object'更严谨(null不为对象),且isIterable会检查Symbol.iterator的存在性。这些检查器在解析器、编码器和变换中承担参数校验与分支分发的职责。
五、类型强制转换(Type Coercion)
类型转换系列将值强制转换为目标类型,统一约定:null值与空字符串被映射为null。
| API | 说明 |
|---|---|
vega.toBoolean(value) | 字符串"true"、"1"映射为true;"false"、"0"映射为false;其余值按常规布尔转换 |
vega.toDate(value[, parser]) | 转为 Date 时间戳。Date 对象原样通过;数字(时间戳)默认原样通过;其他值使用Date.parse解析;可传入自定义parser函数接管解析 |
vega.toNumber(value) | 转为数字,null与空串映射为null |
vega.toString(value) | 转为字符串,null与空串映射为null |
对应源码位于 src/toBoolean.ts、src/toDate.ts、src/toNumber.ts、src/toString.ts。文档特别提醒:Date.parse在不同浏览器上的实现存在差异,跨浏览器场景建议显式传入自定义parser函数(例如 Vega 内部使用的时区感知解析器)。
六、对象操作(Objects)
6.1 extend
vega.extend(target[, source1, source2, …])将输入源对象的所有可枚举属性按顺序复制到目标对象上(后面的源覆盖前面),实现在 src/extend.ts。
6.2 inherits
vega.inherits(child, parent[, members])是设置面向对象继承的便捷方法:将child函数的prototype指向以parent.prototype为原型的对象,实现原型链继承;可选的members对象会被合并进新的子原型。返回新的子原型对象。实现在 src/inherits.ts。Vega 数据流中的算子类(如 vega-dataflow 中的变换算子)正是借助此类工具构建类层级。
6.3 lruCache
vega.lruCache([maxsize])提供字符串键控的键值缓存,按“最近最少使用”策略淘汰条目,支持has、get、set、clear方法。可选参数maxsize决定缓存上限,默认 10,000。
其实现(src/lruCache.ts,改编自 hashlru,MIT 许可)使用双缓存结构:当前缓存curr与前序缓存prev。当curr填满时,它变成prev,并新建空的curr;后续get会将prev中的元素提升回curr;再次填满翻转时,仍残留在prev中的旧条目即被丢弃。
var cache = vega.lruCache(1); // 用 1 元素缓存演示 cache.set('a', 1); // 当前缓存为 a->1 cache.set('b', 2); // 当前缓存为 b->2,前序缓存为 a->1 cache.get('a'); // -> 1(a 提升至当前缓存,b 落入前序缓存) cache.set('c', 3); // 当前缓存为 c->3,前序缓存为 a->1 cache.has('c'); // -> true (c 在当前缓存) cache.has('b'); // -> false (b 已被淘汰) cache.has('a'); // -> true (a 在前序缓存) cache.get('c'); // -> 3 cache.clear();这种无需精确 LRU 链表、只需两次翻转的设计,在保证近似 LRU 语义的同时获得了极高的实现简洁度与性能,适合高频缓存场景(如 Vega 中的字体/文本度量缓存)。
6.4 fastmap
vega.fastmap([object])提供字符串键控的键值映射结构,支持 ES6 Map API 的子集:has、get、set、delete、clear方法与size属性。若传入object,其全部键值对会被加入新实例。
var map = vega.fastmap({foo:1, bar:2}); map.has('foo'); // -> true map.get('foo'); // -> 1 map.delete('bar'); map.has('bar'); // -> false map.set('baz', 0); map.get('baz'); // -> 0 map.size; // -> 2 map.empty; // -> 1(空条目数) map.clean(); // 触发垃圾回收,清空空条目fastmap 的设计动机是性能:它使用普通 JavaScript 对象做哈希,并避免调用内置的delete运算符(删除时仅以哨兵值占位,见 src/fastmap.ts 中的NULL常量),从而规避 V8 等引擎中delete造成的对象形态(hidden class)退化。代价是删除会造成“空条目”累积,需要周期性调用clean()回收;test方法是 getter/setter,用于提供一个布尔函数,指示在垃圾回收时除空条目外还应移除哪些对象。
6.5 hasOwnProperty
vega.hasOwnProperty(object, property)返回输入对象是否自身定义了指定属性(忽略原型链)。与原生Object.hasOwnProperty等价,但加固了安全性:即使Object.prototype上的内建方法被覆盖,也能正确工作。实现在 src/hasOwnProperty.ts,并被 fastmap、lruCache 等内部使用。
6.6 mergeConfig 与 writeConfig
vega.mergeConfig(...config)将多个 Vega 配置对象合并为单个对象,参数列表中索引越靠后的配置优先级越高,可覆盖前面对象的同名设置。源码 src/mergeConfig.ts 中体现了三个关键规则:
signals数组按name去重合并,且源(后面的)信号优先于同名已有信号(mergeNamed实现);legend块仅对layout键递归合并;style块对所有属性递归合并;- 其他对象一律浅层覆盖,不做递归。
vega.writeConfig(config, key, value[, recurse])是mergeConfig内部使用的辅助方法:向配置对象写入属性。recurse参数控制是否递归合并:false或undefined不递归;true递归一层;若为对象值,则仅对其中映射为真值的键递归一层。此外,二者均通过isLegalKey拒绝__proto__、constructor、prototype等可能污染原型链的键名(见 src/mergeConfig.ts 中的isLegalKey定义),体现安全编码实践。
七、数组操作(Arrays)
7.1 array 与 toSet
vega.array(value):确保输入是数组。是数组则原样返回,否则包装为单元素数组[value];vega.toSet(array):将数组值转为对象集合,数组元素作为属性键、值均为1(元素会被强制转成字符串)。
vega.toSet([1, 2, 3]); // {'1':1, '2':1, '3':1}7.2 clampRange / inrange / flush
vega.clampRange(range, min, max):保跨度的范围夹取。若range的跨度小于(max - min)且有端点越界,则将整个范围平移,使跨度不变且一个端点恰好落在边界上;若跨度大于(max - min),直接返回[min, max]。典型应用是交互缩放时保持视口范围宽度不变;vega.inrange(value, range[, left, right]):判断值是否落在range数组的区间内;left/right布尔标志控制端点包含(true,默认)或排除(false);vega.flush(range, value, threshold, left, right, center):当值与范围端点“贴边”时在三者间选择返回值:若value距range最小值在threshold之内返回left,距最大值在threshold之内返回right,否则返回center。两者均实现在 src/inrange.ts。
7.3 extent 与 extentIndex
vega.extent(array[, accessor]):返回数组的最小值与最大值[min, max],忽略null、undefined与NaN;可选accessor先对每个元素做变换再比较;vega.extentIndex(array[, accessor]):返回最值对应的下标[minIndex, maxIndex](按自然序)。
vega.extentIndex([1,5,3,0,4,2]); // [3, 1] vega.extentIndex([ {a: 3, b:2}, {a: 2, b:1}, {a: 1, b:3} ], vega.field('b')); // [1, 2]7.4 lerp / span / peek
vega.lerp(array, fraction):返回数组首尾元素间按插值比例(通常 0~1)线性插值的值,如lerp([0, 50], 0.5)返回25;vega.span(array):返回数组的数值跨度,即末值减首值;vega.peek(array):返回数组最后一个元素但不移除它,是array[array.length - 1]的便捷简写。
7.5 merge
vega.merge(compare, array1, array2[, output]):将两个已排序数组合并为一个有序数组。compare是比较器,应与两个源数组的既有排序一致;结果写入可选的output数组(未指定时新建并返回)。该函数常用于数据流中增量式排序合并。
7.6 pan / zoom 系列(变换)
pan*与zoom*系列用于对数值域(domain,升序数组)做平移与缩放,是 Vega 交互缩放/平移的核心数学工具,全部实现在 src/transform.ts。delta以当前域跨度的比例表示,可为正负;返回值均为两元素数组(平移/缩放后的起止值)。
| API | 变换类型 | 额外参数 |
|---|---|---|
vega.panLinear(domain, delta) | 线性平移 | — |
vega.panLog(domain, delta) | 对数平移 | — |
vega.panPow(domain, delta, exponent) | 幂平移 | exponent |
vega.panSymlog(domain, delta, constant) | 对称对数平移 | constant |
vega.zoomLinear(domain, anchor, scale) | 线性缩放 | anchor锚点(null时取域中点) |
vega.zoomLog(domain, anchor, scale) | 对数缩放 | anchor |
vega.zoomPow(domain, anchor, scale, exponent) | 幂缩放 | anchor、exponent |
vega.zoomSymlog(domain, anchor, scale, constant) | 对称对数缩放 | anchor、constant |
这些函数保证了在非线性尺度(对数、幂、symlog)下缩放/平移仍能保持正确的视觉几何,是 Vega 交互式图表实现 zoom/pan 信号逻辑的底层依据。
7.7 visitArray
vega.visitArray(array[, filter], visitor):遍历数组,对每个通过可选filter的值调用visitor。filter的返回值若非假,会作为输入传给visitor——因此它既是过滤器,也是值变换器。未提供filter时所有值都被访问。visitor与Array.forEach一致,接收三个参数:值、当前下标、源数组引用。
// 控制台输出:1 0; 3 2 vega.visitArray([0, -1, 2], function(x) { return x + 1; }, function(v, i, array) { console.log(v, i); });八、日期操作(Dates)
vega.quarter(date):返回输入 Date 对象或时间戳在本地时区下的季度(1~4 的整数);vega.utcquarter(date):返回UTC 时区下的季度。
两者均实现在 src/quarter.ts,在时间分桶(如按季度聚合)与时间轴刻度中常用。
九、字符串操作(Strings)
| API | 说明 |
|---|---|
vega.pad(string, length[, character, align]) | 用重复字符将字符串填充到指定长度。默认字符为空格' ';默认在尾部填充;align可取'left'、'center'、'right' |
vega.repeat(string, count) | 将字符串重复count次 |
vega.splitAccessPath(path) | 将对象属性访问路径字符串拆为路径元素数组 |
vega.stringValue(value) | 返回同时符合 JSON 与 JavaScript 语法的值表示;对象与字符串经JSON.stringify,数字/布尔等原样返回。可用于生成运行时编译代码片段(如 Function 构造)中的字面量 |
vega.truncate(string, length[, align, ellipsis]) | 将字符串截断到目标长度;align为'left'/'center'/'right',默认截断'right'端;ellipsis指定省略标记,默认省略号…(\u2026) |
vega.pad('15', 5, '0', 'left'); // '00015' vega.repeat('0', 5); // '00000' vega.splitAccessPath('foo'); // ['foo'] vega.splitAccessPath('foo.bar'); // ['foo', 'bar'] vega.splitAccessPath('foo["bar"]'); // ['foo', 'bar'] vega.splitAccessPath('foo[0].bar'); // ['foo', '0', 'bar']值得深入的是splitAccessPath(src/splitAccessPath.ts):它手工解析点号、方括号与引号,支持"与'引用的键、反斜杠转义,并对未闭合的方括号、引号、缺失开括号等情况抛出错误(调用vega.error)。该函数是vega.field与vega.key嵌套字段支持的基石,也让getter(src/getter.ts)可以预编译出高性能的取值函数,而非运行时反复解析路径。
十、日志(Logging)
vega.logger([level, method, handler])生成一个分级日志器,用于有选择地向 JavaScript 控制台写日志。level指定初始日志级别,默认为None;生成的实例提供:
level(value):设置当前级别,只有级别小于等于value的消息才会输出;error(message1[, message2, …]):当前级别为Error或更高时,经console.error输出;warn(...):级别为Warn或更高时,经console.warn输出;info(...):级别为Info或更高时,经console.log输出;debug(...):级别为Debug或更高时,经console.log输出。
可选的method参数("log"、"warn"、"error")可将所有消息统一路由到同一 console 方法;可选的handler参数接受自定义处理函数,签名(method, level, values),其中method为"log"/"warn"/"error"之一,level为日志级别字符串,values为待打印的值数组。
级别常量定义于 src/logger.ts,数值从小到大依次为:
| 常量 | 数值 | 行为 |
|---|---|---|
vega.None | 0 | 全部消息被抑制 |
vega.Error | 1 | 仅输出错误 |
vega.Warn | 2 | 输出错误与警告 |
vega.Info | 3 | 输出错误、警告与信息 |
vega.Debug | 4 | 输出全部消息 |
const log = vega.logger(vega.Warn); log.warn('something suspicious'); // 输出 log.info('not important'); // 被抑制源码实现中,默认的log函数会将级别字符串(如'ERROR'、'WARN'、'INFO'、'DEBUG')拼在参数最前面输出;各方法均返回this以支持链式调用。
十一、错误处理(Errors)
vega.error(message)抛出携带指定消息的Error。它是错误处理的间接层,便于把错误条件嵌入表达式链中:
vega.error('Uh oh'); // 等价于 throw Error('Uh oh') // 在表达式中嵌入错误 return isOk ? returnValue : vega.error('Not OK');实现在 src/error.ts,同时被splitAccessPath等内部模块用于报告解析错误,保证错误消息与抛出路径统一。
十二、从源码看设计理念
综合 src/index.ts 的导出清单与各模块实现,可以提炼出 vega-util 的几个核心设计原则:
- 访问器元数据贯穿始终:
accessor、field、compare、key生成的所有“数据感知”函数都携带fields元数据,使上层数据流引擎能够静态追踪依赖,这是 Vega 声明式数据流高效求值的底层保障; - 性能与安全并重:
fastmap规避delete以保持对象形态、lruCache用双缓存近似 LRU、hasOwnProperty与mergeConfig防原型污染,都体现了对运行时性能与安全性的极致追求; - 统一错误与日志出口:解析类错误统一经
vega.error抛出,运行时诊断统一走分级logger,让 Vega 各模块的诊断行为保持一致、可被用户接管; - 纯函数、无副作用优先:绝大多数工具(
extent、lerp、pan*、zoom*等)是纯函数,输入输出清晰,便于测试与组合——对应源码目录下 test 中大量的*-test.js用例也正是围绕这些纯函数行为编写的。
如果你正在阅读 Vega 的其他源码包(如 vega-dataflow、vega-parser、vega-transforms),会发现大量对vega-util的引用;掌握了本文的 API 全貌,你就能在阅读与二次开发中快速定位这些基础能力,理解 Vega 数据流从声明式规格到运行时求值的每一步底层动作。
- 数据可视化
【免费下载链接】vega
A visualization grammar.
相关推荐
Vega Utilities API 完全指南:vega-util 工具函数库深度解析
Vega Utilities API 完全指南:vega util 工具函数库深度解析 Vega 在其整个数据流引擎、解析器与场景图渲染的实现中,大量依赖一组
数据可视化Vega 可视化语法实战:基于 vega 核心包的 JSON 声明式交互可视化开发指南
Vega 可视化语法实战:基于 vega 核心包的 JSON 声明式交互可视化开发指南 Vega 是一套可视化语法(visualization grammar)
数据可视化Skyvern自动化工具完整指南:从零开始构建智能网页操作流程
Skyvern自动化工具完整指南:从零开始构建智能网页操作流程 还在为重复的网页操作而烦恼吗?每天需要登录网站、填写表单、下载文件,这些看似简单却耗时耗力的任务
人工智能AI Agent浏览器控制GUI 自动化工作流自动化RPA后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考