uPlot Scale Range Policy 深度解析:scan / auto / range 三权分离的坐标轴范围模型

原创2026-09-23 17:30:021,298 阅读
文章标签:前端图表库数据可视化

uPlot Scale Range Policy 深度解析:scan / auto / range 三权分离的坐标轴范围模型

导读

本文基于 uPlot 仓库中的 docs/investigations/scale-range-policy.md 调研文档,系统讲解 uPlot 的 Policy B 尺度范围模型(scale-range policy):它将"何时重算"(scale.auto)、"如何扫描数据"(scale.scan)与"如何推导边界"(scale.range)三者彻底分离,并让 X / Y 轴共用同一套边界规则。读完本文,你将掌握 scale.scan 回调与静态 uPlot.scan() 辅助函数的使用、静态范围数组与局部边界的语义、setRange / setScale / cursor.drag.setRange 三者各自的适用场景,以及空数据、序数轴、facet 模式下的边界行为,并看到对应的源码与测试依据。


1. 模型总览:把"重算"与"扫描"解耦

传统图表库中,坐标轴范围的自动计算往往是一条隐式链路:数据变化 → 扫描全部数据 → 计算边界 → 重绘。uPlot 的 Policy B 模型把这条链路拆成三个独立控制点,见下表:

控制项 职责
scale.auto 控制数据或 X 尺度变化后是否隐式触发重算(调度器)
scale.scan 控制重算发生时如何扫描数据的最小/最大值(扫描器)
scale.range 根据扫描结果推导最终边界(定界器)

对应实现位于 src/uPlot.js 的 initScale()(src/uPlot.js#L600-L621):sc.auto、sc.scan、sc.range 均被归一化为内部函数。其中 auto 默认值为 true(fnOrSelf(sc.auto)),range 未指定时会按尺度类型选择内置定界器——时间轴用 snapTimeX,线性/对数/asinh 分别对应 snapNumX/snapLogX/snapAsinhX 与对应的 Y 轴版本(src/uPlot.js#L602-L605)。

scale.scan 接受布尔值或回调函数:

  • true → 内部映射为 scanCached(缓存感知扫描器,src/uPlot.js#L378-L380);
  • false → 映射为 scanNone(不扫描,直接返回 <a href="https://link.gitcode.com/i/4daca804298b6c3916ff32724aa6b4d6" target="_blank">null, null],[src/uPlot.js#L386-L388);
  • 省略 → 默认映射为 scanAuto,它会先询问 scale.auto:允许重算则走 scanCached,否则走 scanNone(src/uPlot.js#L390-L392);
  • 自定义回调 → 直接使用,契约见下。

这三者的默认解析逻辑集中在 src/uPlot.js#L613-L614:

let scan = ifNull(sc.scan, rangeIsArr && rn[0] != null && rn[1] != null ? false : null);
sc.scan = scan == null ? scanAuto : scan === true ? scanCached : scan === false ? scanNone : scan;

注意这一行的关键语义:对于完全具体的 range: [min, max] 数组,省略 scan 时默认是 false(静态范围不扫描数据);其余类型的范围保留原有的数据驱动扫描默认值(跟随 auto)。

scale.scan 回调的公开契约是:

(u, scaleKey, i0, i1) => [min, max]

它接收当前实例、尺度 key、以及当前对齐窗口的起止索引(Y 轴场景)或 null(全量场景),返回一个聚合的 <a href="https://link.gitcode.com/i/61f6453c6d1f444e7788baf311b64aab" target="_blank">min, max] 元组,并且每个被计算的独立尺度只调用一次。i0 / i1 描述的是当前可见对齐窗口;省略或传 null 时扫描整个数据数组([dist/uPlot.d.ts#L752-L762)。

1.1 两个公开 API 与 scale.range() 的关系

API 行为
u.setRange(scaleKey, min, max) 设置具体边界,完全绕过 scale.range()
u.setScale(scaleKey, {min, max}) 对象形式 API;具体边界绕过 scale.range(),null 边界请求计算

实现上,setScale() 就是 setRange() 的薄包装(src/uPlot.js#L2747-L2751),而 setRange() 把请求写入 pendScales<a href="https://link.gitcode.com/i/7d8a4bb8a3d58a5cba785785fcf37564" target="_blank">key] = {min, max} 后触发一次 commit()([src/uPlot.js#L2714-L2743)。

类型层面:TypeScript 声明中 setScale 的边界类型是 number | null(dist/uPlot.d.ts#L97),undefined 不作为受支持的边界类型暴露;但在 JS 运行时,省略或 undefined 的边界同样按"需计算"处理(详见第 3 节边界规则)。


2. 静态范围数组:range: [min, max] 的完整语义

当一个尺度配置为完全具体的 range: [min, max] 数组时,行为遵循以下规则(这也是 dist/uPlot.d.ts 中 Scale.range 注释所描述的"静态范围"场景):

  • 默认 scan: false(不扫描数据,见上节源码解析),但保持标准的 auto: true 默认值;
  • 该数组不会覆盖显式的 auto: false、auto: true 或 auto 回调;
  • 它也尊重显式的 scan: true、scan: false 及自定义 scan 回调。

2.1 默认配置下的表现

使用默认配置时:

  • 双击可将手动缩放过的 Y 轴恢复到配置的静态范围;
  • 后续的 X 轴单独缩放、默认的 setData()、默认的 redraw() 同样会把静态 Y 范围恢复回来;
  • 同一批次中同时给出具体的 X 与 Y 边界时保持显式——静态范围不会覆盖该批次中的显式 Y 边界。

2.2 显式 auto 与 scan 的叠加

  • 显式 auto: false 时,隐式重算不会恢复该范围;但显式传入 null 边界仍可请求重算;
  • auto 回调与其它范围一样,通过返回值控制是否调度重算;
  • 显式 scan: true 时,默认的 setData() 会刷新静态尺度的数据极值;而显式 auto: false 会禁用这种隐式刷新。

2.3 实战配置:X 变化时保留手动 Y 边界

下面这段配置(原文示例)可以在显式 X 变化、默认 redraw() 与公开的 null/null X 重置过程中保留手动 Y 边界:

scales: {
	y: {
		range: [1, 10],
		auto: (u, viaAutoScaleX) => viaAutoScaleX,
	},
},

其语义是:只有当 X 发生自动缩放(viaAutoScaleX 为 true,即"经由自动 X 缩放")时才允许 Y 重算,从而在 X 变化时保住手动 Y 范围;而双击与自动 setData() 时,则把静态 Y 范围恢复回来——且不需要做 Y 数据扫描。该回调是可选的,默认仍为 auto: true。

补充说明:

  • 静态 asinh 尺度保留默认阈值 1(src/uPlot.js#L607-L608 中仅当 rangeIsArr || sc.auto === false 时 sc.asinh = 1);
  • 局部(partial)Y 范围数组仍会扫描数据;本次变更并不扩大对局部 X 范围数组的支持。

3. 边界规则:Concrete / Null 的四种组合

Policy B 的边界请求规则用一个 2×2 表格即可完整描述:

最小值 最大值 结果
Concrete Concrete 两端都是显式,scale.range() 不执行
Concrete Null 或 undefined 最小值保持精确,最大值来自 scale.range()
Null 或 undefined Concrete 最小值来自 scale.range(),最大值保持精确
Null 或 undefined Null 或 undefined 两端都来自 scale.range()

关键要点:

  1. 请求语义:请求中的 concrete 边界是"显式",null 边界是"计算"。运行时 JS 还把省略或 undefined 的边界视为"计算",但 TypeScript 的 setScale() 契约只接受 number | null,不暴露 undefined。
  2. 局部范围:当存在局部范围时,scale.range() 收到的是扫描出的数据最小/最大值,实现随后把显式那一侧恢复回去。
  3. 交叉拒绝:如果计算出的那一侧越过显式侧(例如显式 min=10 而计算出的 max 小于 10),uPlot 会拒绝这次更新,保持上一次有效范围继续生效。

这三条规则在 src/uPlot.js 中有精确的源码对应:

  • isFullyExplicit(min, max) 与 isFullyImplicit(min, max) 负责分类待处理请求(src/uPlot.js#L695-L696);
  • applyCalculatedRange() 恢复显式侧,并检测"交叉的局部范围"后回退到旧边界(src/uPlot.js#L698-L718):
function applyCalculatedRange(wsc, psc, minMax, key) {
	if (isFullyImplicit(psc.min, psc.max)) {
		wsc.min = minMax[0];
		wsc.max = minMax[1];
		return;
	}

	let minExplicit = psc.min != null;
	let maxExplicit = psc.max != null;
	let min = minExplicit ? psc.min : minMax[0];
	let max = maxExplicit ? psc.max : minMax[1];

	if (isFullyExplicit(min, max) && min > max && minExplicit != maxExplicit) {
		wsc.min = scales[key].min;
		wsc.max = scales[key].max;
	}
	else {
		wsc.min = min;
		wsc.max = max;
	}
}

3.1 测试证据

test/scale-range-policy.mjs 逐条覆盖了上述规则:

  • setScale('x', {min: 0.25, max: 1.75}) 之后 range 回调调用次数为 0,边界精确为 <a href="https://link.gitcode.com/i/47733e8f46629c816715ff5afc8e48c6" target="_blank">0.25, 1.75]([test/scale-range-policy.mjs#L31-L49);
  • setRange('x', 0.25, 1.75) 同样不触发 scale.range(test/scale-range-policy.mjs#L51-L66);
  • 局部边界 {min: -10, max: null} 只计算 max 侧,结果为 <a href="https://link.gitcode.com/i/e76a7d0e2643f49197b9877e9f9fc98f" target="_blank">-10, 3];{min: undefined, max: 10} 只计算 min 侧,结果为 [-1, 10]([test/scale-range-policy.mjs#L68-L95);
  • 交叉局部范围 setScale('x', {min: 10, max: null}) 会触发回调但保持初始范围不变(test/scale-range-policy.mjs#L218-L247)。

4. 自动与显式 X 范围:哪些操作走 scale.range()?

Policy B 的一个重要结论是:自动 X 边界属于"计算边界",因此同样经过 scale.range()——X 与 Y 共用同一条规则。

4.1 会计算 X 范围的操作

  • 初始自动尺度设置;
  • X 自动缩放激活时的 setData();
  • 双击尺度重置;
  • setScale('x', {min: null, max: null});
  • 带一个 null 边界的局部 X 范围。

4.2 使用显式 X 边界的操作

  • 带两个具体边界的公开 setScale() 调用;
  • 公开的 setRange() 调用;
  • 光标拖拽缩放;
  • 保留当前 X 边界的内部重绘操作。

显式 X 边界绕过 scale.range(),与 Y 轴规则完全一致。

测试佐证:setData(<a href="https://link.gitcode.com/i/da0466e309a9495f1bdcc57c8f00972c" target="_blank">[10, 20, 30], ...]) 后 X 边界经 range 回调变为 [9, 31];setScale('x', {min: 12, max: 18}) 后回调不再触发、边界精确为 [12, 18];双击后再次回到自动计算范围 [9, 31]([test/scale-range-policy.mjs#L97-L127)。

4.3 单点 X 数据的边界扩张

扫描器把原始 X 极值交给定界器后,默认 X 定界器会对相等极值做扩张(应对单点或重复 X 值);自定义 X 定界器收到相等的极值原样传入,扩张与否由自己控制。内置定界器在 src/uPlot.js#L602-L604 中按 distr 选择。单点 X 的完整期望值矩阵(正/负数、零、log、asinh、time、ordinal 等 20 种组合)记录在 test/scale-x-range.mjs#L30-L56,例如单点 x = 10 线性轴扩张为 [0, 20],对数(base 2)扩张为 [4, 16],ordinal 则为 [0, 1]。

4.4 拖拽缩放回调:cursor.drag.setRange

cursor.drag.setRange 控制内置拖拽缩放(src/uPlot.js#L3669-L3681),其回调契约为:

(self, scaleKey, min, max) => [min, max] | null
  • 每个发生变化的独立尺度都会收到有序的候选边界(min/max 已按大小排序);
  • 返回调整后的 [min, max] 元组,或返回 null 取消该尺度的变化;
  • 程序化调用、自动、重绘与双击操作不会触发它。

源码证据:setDragRange() 先交换乱序边界,然后调用 drag.setRange(self, key, min, max),只有返回非 null 时才继续 setRange(key, range[0], range[1])(src/uPlot.js#L3669-L3681)。


5. 扫描行为:scale.scan 与静态 uPlot.scan()

5.1 扫描器的选择与参与规则

scale.scan 选择尺度计算时使用的扫描器,而系列与 facet 的 scan 标志决定哪些数据参与扫描:

  • 对独立 Y 尺度,scan: true 使用当前可见索引区间,更新参与者的极值缓存并返回聚合结果(内部实现 scanCached,src/uPlot.js#L378-L380);
  • 内置的对齐 X 计算(scanCachedX,src/uPlot.js#L382-L384)在调用 scale.range() 之前扫描完整 X 域;
  • 系列级 s.scan 与 facet 级 facet.scan 在 src/uPlot.js#L348-L362 中控制参与度;scan 同时是 auto 的废弃别名(src/uPlot.js#L1152-L1157)。

5.2 静态 uPlot.scan(self, scaleKey, i0?, i1?, cache?)

这是公开的扫描辅助函数(uPlot.scan = scanScale,src/uPlot.js#L4021),返回一个聚合的 [min, max] 元组:

  • 省略索引时选择每个完整数据数组;
  • 默认 cache: false 执行纯扫描——不读也不写极值缓存;
  • cache: true 时,已存在且非 null 的参与 Y 缓存与 mode-2 facet 缓存具有权威性,只对缓存未命中(cache miss)的数据做扫描并写入;此时提供的索引只约束"未命中部分"的数据选取,不会约束已有缓存;
  • mode-1 对齐 X 始终读取自身数据。

5.3 两个高频用法

扫描并缓存完整域:

scan: (u, scaleKey) => uPlot.scan(u, scaleKey, null, null, true)

扫描并缓存当前可见窗口:

scan: (u, scaleKey, i0, i1) => uPlot.scan(u, scaleKey, i0, i1, true)

5.4 自定义扫描回调的约束

自定义 scale.scan 回调必须填充每个参与系列或 facet 的最终极值缓存。当内置数据极值就是回调的最终 per-series 值时,使用 cache: true 调用 uPlot.scan() 即可满足该要求(见 dist/uPlot.d.ts#L752-L762 的完整契约注释:mode 2 下还需把第二个 facet 的极值镜像到 series.min/max,且不得改动渲染索引)。

scanScaleInternal() 是纯扫描与缓存感知扫描的公共实现(src/uPlot.js#L294-L372):它遍历参与系列/facet,对每个 facet 先检查缓存命中,未命中则调用 getMinMax() 计算并把结果写回 facet.min/max(mode-1 时同步镜像到 series[si].min/max),最终聚合成一个尺度级 [min, max]。

5.5 scan / auto 的组合语义

  • scale.scan 为 false 时,scale.range() 仍可能运行,此时计算出的数据边界为 null;
  • scale.auto 为 false 时,uPlot 不调度隐式重算,但公开调用带 null 边界的 setScale() 仍可请求重算。

典型示例——该尺度在 setData() 后不变:

scales: {
	y: {
		auto: false,
		scan: true,
	},
},

但下面的调用会请求一次全新的扫描与范围计算:

u.setScale('y', {min: null, max: null});

6. 重绘与极值缓存:redraw() 的细节

redraw() 与 redraw(true, true) 会重建系列路径并保留当前 X 边界,但自动尺度调度、扫描器回调与范围回调仍然运行。

  • 数据未变且缓存有效时,内置 Y 扫描器直接聚合缓存极值,不再读取数据值;自定义扫描器仍自行控制数据访问;空缓存视为未命中。
  • 重绘不会替换已有的待处理 X 请求;数据更新、缩放与自动重置与重绘共享同一提交时,仍保持正常的失效行为。
  • X 请求可能让某个 Y 尺度不被计算(其边界显式,或 auto 回调抑制了重算)。此时私有集合 redrawDirty 记录这些延迟失效,但不立即丢弃公开极值缓存;后续重绘在重算该尺度前先清除受影响的缓存——这样在自动重算被抑制期间,自定义全域缓存得以保留。
  • setData() 即使在 resetScales: false 时也会使极值失效;就地修改数据后调用方仍必须调用 setData()——只改数据再调 redraw() 是不受支持的用法。
  • redraw(false, true) 仍是纯"坐标轴与布局"刷新,不处理尺度;redraw(false, false) 保留缓存的路径(除非其它待处理工作使其失效)。

源码位置:redrawDirty 声明于 src/uPlot.js#L693,延迟失效记录于 X 请求处理逻辑(src/uPlot.js#L1614-L1623),重绘前按 redrawDirty 清除缓存(src/uPlot.js#L1626)。


7. 空数据的行为

  • 数据为空时,自定义 scan 回调以 undefined 索引运行一次;布尔扫描与默认扫描产生 null 极值;
  • 默认 range 返回 null 边界,因此空数据时自动 X 与 Y 轴都变为不活动;
  • 完全具体的边界在空数据下仍绕过 scale.range();局部边界在回调运行后保留其显式侧。

源码佐证:scanScaleInternal() 在 data == null || scale == null 时直接返回 nullNullTuple(src/uPlot.js#L299-L300);测试覆盖于 test/scale-range-policy.mjs#L249-L299(空数据下 concrete 边界 [1,2]/[3,4] 不经回调直接生效;局部边界在 [null, null] 扫描结果上保留显式侧)。

布局影响:对齐空数据此前保留旧 X 范围、仅自动 Y 范围变 null;现在两个自动范围都变 null。test/layout.mjs 记录了这一新行为——两个自动轴保留区在数据返回前都被移除。


8. Ordinal 与 Faceted(facet 模式)数据

  • Ordinal X 转换只作用于具体边界:null 边界不经过 closestIdx()。源码在 setRange() 中可见:仅当 key == xScaleKey && sc.distr == 2 && dataLen > 0 时对非 null 的 min/max 调用 closestIdx(min, data<a href="https://link.gitcode.com/i/8acc5a0069a1e117f5f7175de80c585a" target="_blank">0])([src/uPlot.js#L2726-L2734);
  • 局部边界规则同样适用于 mode 2:每个 facet 尺度扫描自身数据并保留显式侧(测试 test/scale-range-policy.mjs#L190-L215);
  • 依赖尺度(dependent scale)保持既有行为:其 scale.range() 回调收到的是基础尺度的最终边界。

9. 兼容性影响:demo 迁移与边界钳制建议

新 X 行为影响了一批原本用 scale.range() 修改显式缩放边界的 demo。这些 demo 现在让 X 范围策略与 cursor.drag.setRange 共享同一 helper,以保持内置拖拽行为:

demos/trendlines.js

用一个 X-range helper 同时服务自动范围与内置拖拽缩放;拖拽回调把边界恢复为精确数据值(通过 u.data<a href="https://link.gitcode.com/i/a30015d59b3c0568d3b3684ef7a52b88" target="_blank">0][u.valToIdx(min)] 吸附到最近数据点),不把显式边界路由到 scale.range()([demos/trendlines.js#L10-L34):

const xRange = (u, min, max) => {
	if (min == null)
		return [null, null];

	min = u.data[0][u.valToIdx(min)];
	max = u.data[0][u.valToIdx(max)];

	return min == max ? uPlot.rangeNum(min, max, 0.1, true) : [min, max];
};

// ...
cursor: {
	drag: {
		setRange: (u, scaleKey, min, max) => scaleKey == 'x' ? xRange(u, min, max) : [min, max],
	},
},
scales: {
	x: {
		time: false,
		range: xRange,
	},
},

demos/grouped-bars.js

X-range helper 始终返回完整类别范围;拖拽回调用该 helper 阻止内置 X 拖拽缩放(延续旧 scale.range() 的行为,demos/grouped-bars.js#L149-L183):

const xRange = u => {
	let min = 0;
	let max = Math.max(1, u.data[0].length - 1);
	// ... 按分组宽度/分布计算左右 padding ...
	return [min, max];
};

// ...
setRange: (u, scaleKey, min, max) => scaleKey == 'x' ? xRange(u) : [min, max],

demos/bars-values-autosize.html

X-range helper 在两侧各加 0.5 的 padding;两个图表配置共用该 helper 处理自动范围与内置拖拽缩放(demos/bars-values-autosize.html#L206-L223):

const xRange = (u, min, max) => [min - .5, max + .5];
const dragXRange = (u, scaleKey, min, max) => scaleKey == "x" ? xRange(u, min, max) : [min, max];

结论:何时用哪个回调

这种分离是有意设计:

  • 要**钳制(clamp)、吸附(snap)或拒绝(reject)**内置拖拽缩放的边界 → 用 cursor.drag.setRange;
  • 程序化的 setScale() 与 setRange() 调用同时绕过 scale.range() 与 cursor.drag.setRange,因此调用方必须在调用前自行调整显式边界。

10. 实现细节:核心函数一览

主实现在 src/uPlot.js 中,文档列出的关键构件与职责如下:

构件 职责
待处理请求 包含 {min, max};内部"仅重绘"请求额外带 redraw: true
redrawDirty 记录 X 请求后的延迟缓存失效;尺度级或全量极值重置会清除对应条目
isFullyExplicit() / isFullyImplicit() 对待处理请求分类(src/uPlot.js#L695-L696)
applyCalculatedRange() 恢复显式侧、拒绝交叉局部范围(src/uPlot.js#L698-L718)
resetAutoScaleXIdxs() 自动 X 计算前重置对齐 X 索引(src/uPlot.js#L1520-L1531)
setRange() 归一化边界,处理公开与内部请求(src/uPlot.js#L2714-L2743)
setScale() setRange() 的对象形式包装(src/uPlot.js#L2747-L2751)
scanScaleInternal() 纯扫描或缓存感知扫描,返回单个尺度聚合(src/uPlot.js#L294-L372)
scanCached() / scanCachedX() 缓存感知的独立扫描 / 全域对齐 X 扫描(src/uPlot.js#L378-L384)
setDragRange() 在调用 setRange() 前应用 cursor.drag.setRange(src/uPlot.js#L3669-L3681)
自定义扫描器 返回聚合值,并拥有其参与极值缓存

类型契约

公开 TypeScript 契约在 dist/uPlot.d.ts,包含 Scale.scan、静态 uPlot.scan(dist/uPlot.d.ts#L177)、u.setRange() 与 cursor.drag.setRange(dist/uPlot.d.ts#L589)。一个值得注意的命名差异:声明文件把 Scale.Auto 回调的第二个参数命名为 resetScales(dist/uPlot.d.ts#L726),而运行时行为与本文档统一使用 viaAutoScaleX。


11. 测试矩阵与验证结果

测试文件覆盖范围

  • test/scale-scan.mjs:扫描矩阵——auto: true/false 与默认扫描值、auto: false + scan: true、auto: true + scan: false、自动重算关闭时的显式重算、每尺度一次的回调调用、显式缓存公开扫描回调、全量与索引聚合扫描、纯公开扫描与显式缓存变更、与渲染索引窗口无关的缓存复用、facet 极值与对数数据、空数据;
  • test/scale-range-policy.mjs:范围策略——具体 X/Y 边界、null 与运行时 undefined 边界、每侧局部边界、交叉局部范围、初始与 setData() X 自动缩放、显式 X 缩放、具体 setRange() 边界、双击重置、空数据、ordinal X 数据、facet Y 数据;
  • test/scale-static-range.mjs:静态范围,两种数据模式下共 34 个测试——X 缩放 / setData() / redraw() / 双击后的默认静态 Y 恢复、保留双显式范围的具体 X/Y 批次、显式 auto: true/false 与回调调度、回调在显式 X 变化 / 重绘 / 公开 null/null X 重置时保留手动 Y、双击与自动 setData() 时无 Y 扫描地恢复静态 Y、显式 X/Y 扫描覆盖(含自定义回调)、局部 Y 范围数组的数据扫描;
  • test/scale-x-range.mjs:单点 X 范围、原始自定义定界器输入、局部边界、自定义扫描、空数据;
  • test/scale-scan-cost.mjs:扫描成本、排序端点、全域对齐 X 计算;
  • test/redraw-scan.mjs:重绘缓存复用、回调刷新、延迟失效、待处理请求、已通知的数据变更、ordinal 边界;
  • test/cursor-drag.mjs:拖拽边界细化、取消、同步、回调作用域;
  • 既有 asinh 测试保留静态默认阈值 1;test/layout.mjs 覆盖空数据布局行为。

最新验证结果(文档记录)

  • 完整 Node 套件(npm test):1,011 通过;
  • 聚焦的五文件 scale-range 命令:74 通过(含 static-range、range-policy、issue、HTML demo 与 asinh 测试);
  • 静态范围套件:34 通过(两种数据模式);
  • cursor-drag 与 range-policy 套件:50 通过。

说明:TypeScript 检查未重复执行,因为该 checkout 未安装 tsc。静态范围修复已体现在生成的 JavaScript 中,提交 e75c8b4 同时修改了源码、声明、测试与所有生成 bundle;当前工作树的 bundle 还包含后续源码变更但未暂存,此状态并不代表 npm 发行版已可用。


12. 快速参考:配置速查表

需求 配置
静态范围 + 自动恢复(默认) range: [min, max](自动获得 scan: false + auto: true)
X 变化时保留手动 Y range: [1, 10], auto: (u, viaAutoScaleX) => viaAutoScaleX
完全关闭隐式重算 auto: false(仍可用 setScale(key, {min:null, max:null}) 显式请求)
只扫描不自动重算 auto: false, scan: true
扫描并缓存全域 scan: (u, k) => uPlot.scan(u, k, null, null, true)
扫描并缓存可见窗口 scan: (u, k, i0, i1) => uPlot.scan(u, k, i0, i1, true)
钳制 / 吸附 / 拒绝拖拽缩放 cursor.drag.setRange: (u, k, min, max) => [...] | null
程序化设置具体边界 u.setRange(k, min, max) 或 u.setScale(k, {min, max})(均绕过 scale.range())

这套三权分离(auto 调度、scan 扫描、range 定界)的范围策略让静态范围、局部边界、拖拽缩放与数据扫描各自获得独立且可预测的控制入口,是理解 uPlot 坐标轴系统行为的核心参考。

登录后查看全文
uPlot