- 前端
- 图表库
- 数据可视化
【免费下载链接】uPlot
📈 A small, fast chart for time series, lines, areas, ohlc & bars
本报告解读 uPlot 仓库中 docs/investigations/coverage-gaps.md 这一内部审计文档,它基于 2026-09-17 的工作树,通过npm test(Node 26.8.2,1,011 项通过,耗时 14 秒)生成的 Istanbul 覆盖率数据,逐文件、逐分支地列出了当前测试套件尚未覆盖的代码路径,并为每个缺口给出了具体的补测优先级与测试策略。读者读完本文后,可以掌握 uPlot 测试基础设施的运行方式(npm test的完整调用链)、各源码模块的真实覆盖率基线,以及"内置光标选中、同步拖拽、faceted 数据与动态 series、路径构建器、日期格式化"等核心模块中高优先级缺口的具体补测方案。
审计基础与方法论
审计快照与证据来源
该报告描述的是 2026-09-17 的当前工作树,其状态包含e27e4092cfc67c7c57d7b7e1c39a08fae1cfb143之上未提交的源码与测试改动。上一份报告描述的是一个 614 项测试的历史快照,已经不能代表当前测试套件,因此本次审计完全以重新执行测试得到的证据为准。
审计的唯一证据命令是:
npm test该命令使用 Node 26.8.2 执行,结果为1,011 项测试全部通过,耗时 14 秒,并生成了一份全新的 Istanbul 覆盖率报告。报告中特别强调了三点可信度边界:
- 测试运行器在每次执行前会删除
.nyc_output目录(见 scripts/test.mjs 中rmSync(coverageDir, { recursive: true, force: true })),因此不会复用历史覆盖率数据; - Bun 验证未运行,报告对 Bun 兼容性或 Bun 下的覆盖率不做任何声明;
- 覆盖率只代表代码被执行过,不代表断言质量——某一行被执行但断言不严格,与完全未执行是不同的风险等级。
npm test 的底层调用链
从 package.json 可以看到test脚本指向node ./scripts/test.mjs。scripts/test.mjs 的逻辑是:
- 根据运行时是 Bun 还是 Node 选择不同的覆盖率输出目录(
.nyc_output/bun或.nyc_output); - 通过
NODE_OPTIONS=--import ./scripts/register-hooks.mjs注入 Istanbul 插桩钩子(Bun 下则使用--preload); - 调用
mocha(来自mocha/bin/mocha.js)执行全部测试; - 最后运行
scripts/report-coverage.mjs生成覆盖率报告。
package.json中nyc配置的include为dist/**/*.esm.js与src/**/*.js,exclude为src/paths/catmullRomCentrip.js(该路径构建器不在本次审计范围内)。因此本文下面所有源码引用均以src/目录为准。
当前覆盖率总览
全局与核心文件指标
报告给出了三个层级的覆盖率:
| Area | Statements | Branches | Functions | Lines |
|---|---|---|---|---|
| All files | 94.47% | 88.09% | 92.96% | 94.39% |
src | 94.24% | 88.48% | 91.98% | 94.17% |
src/paths | 95.62% | 85.82% | 98.36% | 95.54% |
主要源文件的覆盖率:
| File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|
src/uPlot.js | 93.46% | 88.54% | 94.94% | 93.41% |
src/utils.js | 95.57% | 91.25% | 92.59% | 95.60% |
src/fmtDate.js | 82.17% | 59.55% | 71.42% | 81.81% |
src/opts.js | 98.97% | 96.13% | 95.74% | 98.92% |
src/dom.js | 100% | 93.33% | 100% | 100% |
注意 src/fmtDate.js 是分支覆盖率最低的核心模块(59.55%),而 src/dom.js 达到了语句与行覆盖率双 100%。src/opts.js仅剩372与500-501两处未覆盖(分别是无调用方的mkDate()与默认cursor.drag.click的传播抑制分支)。
全局缺口总表
报告用一张表格概括了各领域的当前状态、主要剩余缺口与优先级:
| Area | Current assessment | Main residual gaps | Priority |
|---|---|---|---|
| Built-in cursor selection | Large direct gap | Non-null search, hover bias, proximity rejection | High |
| Dynamic series | addSeries()已覆盖,delSeries()未覆盖 | 带 legend 与 cursor 状态的删除、按索引插入 | High |
| Drag and synchronization | 基础拖拽与一个同步 X 用例已覆盖 | 同步 Y、key 不匹配、旋转、drag.uni | High |
| Date formatting and zones | 常见路径被间接覆盖 | 特定 token 与DateZoned分支 | Medium |
| Path builders | 语句覆盖高但分支有缺口 | 抽稀 null、垂直路径、扩展、短样条 | Medium |
| Scale, layout, and bands | 远强于旧报告所述 | 依赖缩放、band clip、轴边框、range guards | Medium to low |
| Data joining | 核心行为已有直接测试 | 顺序检查中的 null 填充 X 列 | Low |
| Browser lifecycle | 部分状态变更有 Happy DOM 测试 | 原生 DPR、全局 resize/scroll、媒体查询 | Browser integration |
| Debug and uncommon APIs | 小且孤立的缺口 | IDs、日志、band 变更、罕见回调形式 | Low |
自旧快照以来的实质变化
报告明确指出旧报告严重低估了多个领域,这些领域如今已有直接覆盖:
- test/cursor-drag.mjs 的 41 项测试覆盖了 DPR 1 与 DPR 2 下的 X、Y、XY 鼠标拖拽;
- 拖拽测试覆盖正向/反向移动、反转 scale、阈值、plot 外释放、双击重置、首选的
drag.setRangeAPI 以及已废弃的布尔drag.setScale别名; - 一个同步 X 拖拽用例现在能触达目标图表并校验每个图表的回调;
- test/cursor-points.mjs 覆盖共享与逐 series 的点、DPR 变化、隐藏点、旋转轴与 mode 2;
- mode 2 初始化与 scale 扫描在 scale、cursor、asinh 与 issue 测试中有广泛直接覆盖;
addSeries()在 aligned 与 faceted 两种测试中都有执行,但显式指定索引插入未执行;- test/join.mjs 覆盖有序/无序输入、null 模式、TypedArray、大规模合并与输入不可变性;
- layout 与 scale-range 测试套件覆盖了大量 resize、range、scan、隐藏轴与延迟提交(deferred-commit)场景。
这些新增直接推翻了旧报告中"基础拖拽未测、mode 2 初始化未测、数据 join 未测、隐藏轴行为未测"等宽泛结论。
内置光标选中:最高优先级缺口
默认非 null hover 选择逻辑
默认的cursor.dataIdx实现位于 src/uPlot.js。该实现的核心流程(结合 src/uPlot.js 的上下文)为:
- 构造
skip集合(包含void 0以排除对齐伪影),并读取cursor.hover.prox与cursor.hover.bias(默认0); - 当
bias == 0 || bias == -1时从光标索引向左扫描,寻找第一个非 null 值(nonNullLft);当bias == 0 || bias == 1时向右扫描(nonNullRgt); - 若两侧都有值:当启用
prox时按像素距离lftDelta <= rgtDelta选择更近一侧,并分别与_prox阈值比较后才接受候选;未启用prox时直接选择索引距离更近的一侧; - 若仅一侧有值,则直接选中该侧;
- 当最近的 X 点本身非 null 但像素距离超过
prox上限时,返回null拒绝该候选(对应 src/uPlot.js 的else if (withProx)分支)。
当前测试套件虽然进入了缺失值路径,但没有执行大部分选择逻辑。报告列出的未覆盖语句为:
src/uPlot.js:1087-1088(左侧候选通过prox阈值)src/uPlot.js:1095-1096(仅一侧有值时的选择)src/uPlot.js:1101-1113(withProx下按像素距离选择最近侧)src/uPlot.js:1117(cursor._lock初始化)src/uPlot.js:1125(focus 对象合并)src/uPlot.js:1127-1128(cursorFocus/cursorOnePt判定)
残余用例包括:向左/向右搜索非 null 值、两侧都有值时选最近侧、仅一侧有值时选唯一侧、cursor.hover.bias取-1/0/1、通过cursor.hover.prox接受或拒绝候选、以及当最近非 null X 点的像素距离超过 prox 上限时拒绝。
报告特别提醒:nearest-non-null相关 demo 测试(如 demos/nearest-non-null.html)使用的是自定义 cursor 行为,并不能证明默认实现。建议的补测方式是构造一个小的 Happy DOM 集成表格,通过setCursor()或鼠标事件驱动光标,再断言cursor.idxs与 legend 值。
非零 focus 偏差
常规focus.bias: 0路径已有覆盖,但 src/uPlot.js 的非零偏差逻辑未覆盖。该逻辑(实际位于updateCursor的 series 循环内,见 src/uPlot.js)在focus.prox >= 0时启用:比较鼠标 Y 值与候选 series 值的符号,当二者同号时才可能成为最近 series;若bias == 1偏向远离零的一侧,bias == -1偏向靠近零的一侧——正如源码注释所说"with a focus bias, we will never cross zero when prox testing"。
未覆盖语句为src/uPlot.js:2969、2971-2972、2976、2981-2982。建议补充聚焦用例:
- 正/负的 series 值;
- 光标值与 series 值在零的同侧;
- 光标值与 series 值在零的异侧;
- 偏向零(bias 向 0)与远离零(bias 向 1)两种情况。
拖拽选择与同步:从"大缺口"到"具体分支"
现已覆盖的行为
test/cursor-drag.mjs通过 41 项测试提供了大量集成覆盖,包括:
- X-only、Y-only 与 XY 拖拽;
- 正向与反向拖拽;
- DPR 1 与 DPR 2;
- 反转的 scale 方向;
drag.dist与零位移处理;- 当首选
drag.setRange为 false 时保留选区; - 通过首选
drag.setRange回调进行选区细化与取消; - 已废弃的纯布尔
drag.setScale别名的 true/false 兼容性; - 在 plot 外释放鼠标;
- resize 与轴折叠后的几何刷新;
- 双击重置范围;
- 两个水平图表间的同步 X-only 拖拽。
因此"基础拖拽是普遍缺口"的旧结论已不成立。
同步选择投影的剩余缺口
同步选择的投影逻辑位于 src/uPlot.js(对应拖拽同步分支实际在src/uPlot.js的setRange/同步回调区域,报告引用 3080-3144 作为同步选择投影)。现有同步测试只到达了"水平方向 + X key 匹配"的路径。当前缺口包括:
- src/uPlot.js 的同步 Y 选择;
- src/uPlot.js 与
3124-3130的旋转源选择(sx.ori为垂直时的水平/垂直值交换); - src/uPlot.js 的非匹配 X key 与全宽重置;
- src/uPlot.js 源无激活拖拽维度与选区清除;
- 两个维度的 scale-key 匹配/不匹配回调。
此外,同步光标投影(src/uPlot.js:3302-3349)也缺少旋转源与无 scale-key 的用例。
旋转与单向拖拽缺口
水平拖拽路径覆盖很强,但以下分支未被执行:
- src/uPlot.js 的旋转拖拽维度交换;
- src/uPlot.js 的
drag.uni低于阈值时的方向选择; - src/uPlot.js 的旋转 X 选择;
- src/uPlot.js 的旋转 Y 选择;
- src/uPlot.js 的旋转拖拽轴映射与 src/uPlot.js
mouseLeave()期间垂直边缘吸附。
报告建议直接扩展现有拖拽测试框架——它已能提供稳定的 plot 几何与鼠标事件。
更小的 cursor 生命周期缺口
旧报告称大多数 cursor 生命周期函数未覆盖,这一论断已不再正确。mouseMove()、mouseUp()、mouseLeave()、dblClick()现在都运行于集成测试中。剩余分支包括:
- src/uPlot.js、
3492-3493、3546-3547的锁定光标(cursor._lock)提前返回; - src/uPlot.js 的同步 focus 映射;
- src/uPlot.js 的内部 DPR 监听器。
这些分支优先级低于默认 hover 选择与同步拖拽投影。
Faceted 数据与动态 series
现状:mode 2 不再是宽泛缺口
Mode 2(faceted)初始化不是普遍缺口。现有测试已能构造 faceted 图表并覆盖 scale 扫描、cursor 点、范围与拖拽重置。addSeries()(src/uPlot.js)也有 aligned 与 faceted 两种直接覆盖,但按传入索引插入的分支(src/uPlot.js 的si = si == null ? series.length : si及后续splice(si, 0, opts))仍未覆盖。
delSeries():完全未覆盖
delSeries()位于 src/uPlot.js,目前没有任何覆盖。该函数按序执行:
- 从
series数组移除条目; - 使 band group 缓存失效(
invalidateBandGroups()); - 移除 legend 的 values 条目(
FEAT_LEGEND && legend.values.splice(i, 1))并刷新 legend; - 移除
activeIdxs中的光标索引; - 若光标启用:移除 cursor-point DOM 元素(
cursorPts.splice(i, 1)[0].remove())并同步cursorPtsLft/cursorPtsTop; - 触发
fire("delSeries", i)hook 通知。
注意:该函数不会移除 scale,也没有独立的 faceted 清理逻辑。测试必须断言已实现的公开行为,而不是假设存在这种清理。报告建议:添加一个启用 legend 与 cursor 的 aligned 测试、一个 mode 2 冒烟测试,并覆盖从 series 数组中间位置删除的场景;同时覆盖带索引的addSeries()插入,断言 series 顺序、legend 顺序、cursor 数组、hooks 以及setData()之后的行为。Mode 2 可见性重定范围(src/uPlot.js)是另一个独立的小缺口。
数据 join 与排序:低优先级残留
join() 核心行为已被直接覆盖
test/join.mjs 直接覆盖了join()的主要行为:
- 相同 X 列与保留的列引用;
- 相同与不同的无序 X 列;
- Typed X/Y 列;
- 部分与不相交的重叠;
- 每张表多个 Y series;
- 显式
null与undefined值; NULL_REMOVE、NULL_RETAIN、NULL_EXPAND三种模式;- 空表与空 series;
- 输入不可变性;
- 100 张表的共享有序合并路径;
- 与旧算法的随机化对比。
结论:核心 join 行为已覆盖,不应再保持高优先级。
isAsc() 的小残留
isAsc()位于 src/utils.js,套件未执行以下分支:
- src/utils.js 的前导 nullish X 值跳过循环;
- src/utils.js 的尾部 nullish X 值跳过循环;
- src/utils.js 的"全 nullish 或单一被 null 包围的值"直接返回
true。
报告的建议很务实:只有当null 填充的 X 列是被支持的输入时才补一个紧凑测试;否则应在文档中说明或直接拒绝该输入,而不是为了提升百分比去补纯数字测试。
日期格式化与时区:分支覆盖率最低的模块
未覆盖的 token 回调
src/fmtDate.js 的覆盖率为语句 82.17%、分支 59.55%、函数 71.42%、行 81.81%,是核心模块中分支覆盖率最低的。常见格式化与时区路径通过 demos 与图表测试运行,真正未覆盖的 token 回调比旧报告更具体:
{MMMM}(src/fmtDate.js)完整月份名;{WWWW}与{WWW}(src/fmtDate.js)完整/缩写星期名;{HH}与{H}(src/fmtDate.js)补零/不补零的小时;{AA}(src/fmtDate.js)大写 AM/PM;{a}(src/fmtDate.js);{m}(src/fmtDate.js);{s}(src/fmtDate.js)。
{fff}毫秒 token 已覆盖;自定义名称与模板装配(src/fmtDate.js 的fmtDate(tpl, names))也已有覆盖,因此不是宽泛缺口。
DateZoned 的时区残留
DateZoned(src/fmtDate.js)剩余时区缺口包括:
- src/fmtDate.js 的本地时区快速路径(
tz == null || tz == localTz时直接返回Date); - src/fmtDate.js 的拷贝构造(参数为
DateZoned实例时复制tz/#str/#utc); - src/fmtDate.js 的
DateZoned.#get()中 UTC-backed getter 分支(this.#utc ? utcMeth.call(this) : ...); - src/fmtDate.js 的零偏移
GMT归一化(f.endsWith('GMT')时补+00:00); - src/fmtDate.js 的 zoned
getDay()(根据瑞典语星期缩写首字母推断星期,如sön→0、mån→1); - src/fmtDate.js 的 zoned
getTimezoneOffset()(从格式化字符串尾部解析偏移)。
死代码:不要为了覆盖率去测
leapYear()(src/fmtDate.js)是私有函数且没有调用方;mkDate()(src/opts.js)同样无调用方,其未覆盖语句在 src/opts.js。报告明确要求:不要通过人为手段让测试触达这些死助手函数;如果它们仍然必要,应在单独的源码变更中移除或重新接入。日期 token 与DateZoned方法的补测应使用表驱动单元测试,且不需要图表。
路径构建器:高语句覆盖下的分支缺口
路径语句覆盖很高,但分支覆盖只有 85.82%。补测原则:保持路径测试小而聚焦,并对比记录的Path2D命令。
线性路径与抽稀(decimation)
src/paths/linear.js 语句覆盖 93.68%、分支覆盖 80.76%。未覆盖语句为93-94、107、109-110、119,全部属于抽稀路径(decimate分支,即idx1 - idx0 >= xDim * 4时启用,见 src/paths/linear.js),而非普通非抽稀路径。目标用例:
- 复用的像素桶(pixel bucket)内出现
null; - 新像素桶起始处出现
null; - 最后一个桶仍需绘制其 min/max 累加器;
- 反向或垂直抽稀(方向分支未覆盖)。
旧报告"所有spanGaps、fill 与 clipping 行为都未测"的说法过于宽泛,已被否定。
阶梯路径
src/paths/stepped.js 语句覆盖 92.64%、分支覆盖 72.54%。未覆盖语句为18、45-46、78-79,主要目标:
- 空输入或全 null 输入;
extend: true且align: -1;extend: true且align: 1;- 垂直或反向方向;
spanGaps: true。
样条
src/paths/monotoneCubic.js 未覆盖语句为13、20、48,分别代表:少于两个点、恰好两个点、计算出的斜率非有限值(如重复的 X 坐标)。
src/paths/spline.js 未覆盖语句为10与27-29,需要补全 null 与垂直方向的包装器用例。
柱状图与共享路径工具
- src/paths/bars.js 是自定义柱状显示几何的 value-unit 宽度分支;
- src/paths/utils.js 反向 band 裁剪;
- src/paths/utils.js 合并重复的 gap 起始点;
- src/paths/utils.js 反向 gap 扫描;
- src/paths/utils.js 垂直 Bézier 辅助函数。
这些都是聚焦的分支测试,不需要构造庞大的路径选项矩阵。
Scale、布局与工具函数的边缘
当前 layout 与 range 测试套件已让旧报告的宽泛缺口清单过时——它们覆盖了大量 resize、隐藏轴、scan、range-policy 与延迟提交路径。
src/uPlot.js 的残留聚类
- src/uPlot.js 轴回退到 series scale(axis 指定了不存在的 scale 时回退,见
initAxis中sc == null的处理); - src/uPlot.js
setData()后非 auto 的 X 范围; - src/uPlot.js 依赖 scale 的传播;
- src/uPlot.js 可见窗口外的 null 搜索;
- src/uPlot.js band 裁剪组合;
- src/uPlot.js 非正的轴维度;
- src/uPlot.js 轴边框绘制;
- src/uPlot.js 延迟的初始选择;
- src/uPlot.js 反转、极小与单索引 ordinal range guards;
- src/uPlot.js faceted 可见性重定范围。
src/utils.js 的残留语句
- src/utils.js
rangeAsinh()的零端点; - src/utils.js
hasData()的全 null 结果; - src/utils.js 非零的折叠范围回退;
- src/utils.js
numDigits(); - src/utils.js
cmpObj(); - src/utils.js TypedArray 复制。
报告建议只针对已文档化的契约或历史上的故障补测试,不要为了提升覆盖率构建宽泛的组合矩阵。
浏览器生命周期行为:需要真实浏览器集成测试
src/dom.js 已实现语句与行覆盖率双 100%,剩余分支位置为25-28与123。未测用例包括:
- DPR 通知但 DPR 未变化;
- DPR 变化后移除之前的 media-query 监听器;
- 启用 capture 时的
off()。
相关 src/uPlot.js 缺口:
- src/uPlot.js 全局矩形失效;
- src/uPlot.js 全局 DPR 传播;
- src/uPlot.js 图表 DPR 监听器。
现有测试覆盖了显式 resize、直接setPxRatio()、cursor 矩形刷新与从cursorPlots移除图表;旧报告错误地声称 cursor 追踪移除未覆盖。关键结论是:Happy DOM 无法复现原生 media-query 与布局行为,全局 resize、scroll 与 DPR 变化应使用真实浏览器的集成测试(如 test/e2e 目录所承载的方案)。
Debug 与不常见 API 路径
低价值缺口包括:
- 内部
log()(src/uPlot.js); - 显式根
opts.id(src/uPlot.js); - 拖拽后的 click 回调(src/uPlot.js);
- 默认
cursor.drag.click的传播抑制(src/opts.js); setBand()(src/uPlot.js,assign(bands[bi], opts));- 带索引的
addBand()/delBand()分支(src/uPlot.js 与2608); - 无挂载目标的构造器初始化(src/uPlot.js)。
报告建议:除非有已报告的问题触达这些路径,否则保持 debug 与防御性路径未覆盖即可;当 band 行为发生变化时,公开的 band 变更(mutation)API 是合理的例外。
推荐的补测顺序
报告按投入产出比给出了明确优先级:
- 为内置的非 null
cursor.dataIdx行为添加表驱动集成测试; - 添加 aligned 与 faceted 的
delSeries()测试,以及带索引的addSeries()覆盖; - 扩展拖拽测试框架,覆盖同步 Y、key 不匹配、旋转与
drag.uni; - 为未覆盖的公开日期 token 与
DateZoned方法添加单元测试; - 添加聚焦的路径测试:抽稀 null、阶梯扩展、垂直样条与反向工具函数;
- 仅为已文档化的行为或已知回归添加 scale、band 或 layout 测试;
- 将原生 DPR、resize 与 scroll 行为保留在浏览器集成测试套件中。
最后一条总原则贯穿始终:不要仅为提升覆盖率百分比而去瞄准死助手函数、debug 路径或防御性分支——覆盖率报告的价值在于暴露真实的行为风险,而不是制造一个好看的百分比数字。
- 前端
- 图表库
- 数据可视化
【免费下载链接】uPlot
📈 A small, fast chart for time series, lines, areas, ohlc & bars
相关推荐
Pencil Project单元测试覆盖率: Istanbul配置与报告分析
Pencil Project单元测试覆盖率: Istanbul配置与报告分析 Pencil Project作为一款开源的GUI原型设计工具,其代码质量直接影响用
桌面应用Blockly 单元测试覆盖率工具:istanbul 配置与报告分析
Blockly 单元测试覆盖率工具:istanbul 配置与报告分析 在现代软件开发中,单元测试覆盖率是衡量代码质量的重要指标之一。它能够帮助开发团队识别未被测
前端低代码UI组件PS3游戏加载革命:如何用webMAN-MOD打造你的终极游戏中心
PS3游戏加载革命:如何用webMAN MOD打造你的终极游戏中心 你是否曾经为PS3游戏管理而烦恼?每次都要插拔硬盘、拷贝文件,或者面对复杂的安装流程感到头疼
后端嵌入式
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考