uPlot Scale Range Policy 深度解析:scan / auto / range 三权分离的坐标轴范围模型
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() |
关键要点:
- 请求语义:请求中的 concrete 边界是"显式",null 边界是"计算"。运行时 JS 还把省略或
undefined的边界视为"计算",但 TypeScript 的setScale()契约只接受number | null,不暴露undefined。 - 局部范围:当存在局部范围时,
scale.range()收到的是扫描出的数据最小/最大值,实现随后把显式那一侧恢复回去。 - 交叉拒绝:如果计算出的那一侧越过显式侧(例如显式 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 坐标轴系统行为的核心参考。