D3 d3-zoom 缩放行为完全指南:从手势交互到 ZoomTransform 变换矩阵
d3-zoom 是 D3 中负责"平移与缩放"(pan & zoom)的官方行为模块:通过拖拽平移、滚轮缩放、触摸捏合等直接操作,让用户聚焦到感兴趣的区域。本指南基于本仓库文档 docs/d3-zoom.md 完整梳理 d3-zoom 的 API——从创建与挂载缩放行为、*zoom* 的全部配置方法、zoomTransform 变换矩阵的读写,到编程式控制(zoom.transform 及三个便捷方法)与底层平滑缩放插值(van Wijk & Nuij 算法),并结合本仓库 src/index.js 与 test/d3-test.js 说明 d3-zoom 在 d3 v7 中的组织方式。读完后你能独立为 SVG/Canvas/HTML 可视化搭建完整的可交互缩放方案,并能用代码驱动带过渡动画的缩放。
一、d3-zoom 的定位与能力边界
平移和缩放是 Web 地图交互的核心,同样适用于密集时间序列、散点图等可视化场景。根据 docs/d3-zoom.md,zoom 行为是一个"灵活的抽象",它处理了令人意外的多种输入模态和浏览器怪癖:
- DOM 无关:可以在 HTML、SVG 或 Canvas 上直接使用;
- 与其他模块组合:可与 d3-scale 和 d3-axis 组合实现坐标轴缩放联动,与 d3-drag 组合实现拖拽,与 d3-brush 组合实现 focus + context(焦点+上下文)视图;
- 可编程控制:通过
*zoom*.transform可以以代码驱动缩放,实现缩放按钮 UI 或数据漫游动画; - 平滑过渡:基于 Jarke J. van Wijk 和 Wim A.A. Nuij 的论文 "Smooth and efficient zooming and panning" 实现的 interpolateZoom 插值。
本仓库中的位置。当前仓库是 D3 的"全家桶"集成包,版本 7.9.0(见 package.json)。package.json 中声明了依赖 "d3-zoom": "^3.0.0",而 src/index.js 通过一行 export * from "d3-zoom"; 将 zoom 的全部导出(zoom、zoomIdentity、ZoomTransform 等)并入 d3 命名空间。因此你不需要单独安装 d3-zoom——安装 d3 后即可使用 d3.zoom() 等全部 API。这一"全量再导出"关系由测试 test/d3-test.js 强制保障:该测试遍历 package.json 的每个依赖模块,断言 d3 导出其中每一个属性(version 除外),保证 d3.zoom 等符号永远存在。
// 本仓库 test/d3-test.js 的核心断言逻辑(节选)
for (const moduleName in packageData.dependencies) {
it(`d3 exports everything from ${moduleName}`, async () => {
const module = await import(moduleName);
for (const propertyName in module) {
if (propertyName !== "version") {
assert(propertyName in d3, `${moduleName} exports ${propertyName}`);
}
}
});
}
从源码结构看,zoom 的具体实现位于 d3-zoom 独立包(src/zoom.js、src/transform.js),本仓库作为上层聚合包只做再导出;这意味着本仓库文档 docs/d3-zoom.md 中每个方法标注的 "Source: d3-zoom/src/zoom.js" 指向的就是那个上游实现。
二、创建并挂载缩放行为:zoom() 与 zoom(selection)
2.1 zoom() 创建行为
d3.zoom() 创建一个新的缩放行为。返回的 zoom 既是对象又是函数:
- 作为对象,它提供一整套链式配置方法(
scaleExtent、filter、on等,后文逐一讲解); - 作为函数,即
*zoom*(*selection*),它把行为应用到选择集上。
典型用法是通过 selection.call 把行为挂到选中的元素上:
selection.call(d3.zoom().on("zoom", zoomed));
2.2 zoom(selection) 的挂载细节
调用 *zoom*(*selection*) 时会发生三件事:
- 绑定事件监听器:内部通过 selection.on 绑定平移和缩放所需的全部监听器;
- 初始化变换状态:若尚未定义,把每个选中元素的 zoom transform 初始化为恒等变换(identity transform);
- 禁用 iOS 点击高亮:设置
-webkit-tap-highlight-color样式为 transparent。如果你想用别的高亮颜色,在挂载行为之后移除或重新设置该样式即可。
事件监听器带 .zoom 名称前缀,这给了你精确卸载的能力:
// 完全移除 zoom 行为的所有监听器
selection.on(".zoom", null);
// 只禁用滚轮缩放(例如避免与页面原生滚动冲突)
selection
.call(zoom)
.on("wheel.zoom", null);
文档同时给出了更优雅的方案:用 zoom.filter 精细控制哪些事件可以发起缩放手势(见第四节)。
状态存储位置是理解 zoom 行为的关键。zoom 行为把缩放状态存储在"行为所应用的元素"上,而不是存储在 zoom 行为对象本身。这个设计允许同一个 zoom 行为同时应用到多个元素上,且每个元素拥有独立的缩放状态。缩放状态会随用户交互或 zoom.transform 的编程式调用而变化。
读取缩放状态有两条途径:
- 在 zoom 事件监听器内,使用 event.transform(见第七节"Zoom 事件");
- 对任意给定节点,调用 zoomTransform——后者特别适合编程式修改缩放状态,例如实现放大/缩小按钮。
三、ZoomTransform:缩放变换的数学模型
3.1 变换矩阵与属性
zoomTransform(node) 返回指定 node 当前的变换。注意 node 通常是 DOM 元素而非 selection(一个 selection 可能包含多个处于不同状态的节点,而本函数只返回单个变换)。如果手里是 selection,先调用 selection.node:
var transform = d3.zoomTransform(selection.node());
// 在事件监听器内部,node 通常就是接收输入事件的元素(即 this)
var transform = d3.zoomTransform(this);
实现细节:元素的变换内部存储为 element.__zoom,但文档明确建议通过 zoomTransform 读取而不是直接访问该属性。若给定 node 没有定义变换,则返回其最近祖先的变换;若都不存在,返回恒等变换。
返回的变换对象表示一个二维仿射变换矩阵:
k 0 tx
0 k ty
0 0 1
该矩阵只能表示缩放和平移(文档注明:未来版本可能支持旋转,但那将是非向后兼容的变更)。位置 ⟨x, y⟩ 被变换为 ⟨xk + tx, yk + ty⟩。变换对象暴露三个只读属性:
| 属性 | 含义 |
|---|---|
transform.k |
缩放因子 k |
transform.x |
x 轴平移量 tx |
transform.y |
y 轴平移量 ty |
不要直接修改这三个属性,而应使用 transform.scale 和 transform.translate 派生新变换。构造变换的标准写法:
var t = d3.zoomIdentity.translate(x, y).scale(k);
3.2 zoomIdentity 与 new ZoomTransform(k, x, y)
d3.zoomIdentity:恒等变换,k = 1,tx = ty = 0。它是所有变换链的起点;new d3.ZoomTransform(k, x, y):构造器形式,返回缩放为 k、平移为 (x, y) 的变换。
3.3 派生变换:scale 与 translate 的复合规则
// 返回一个新变换,其缩放 k1 = k0 * k
transform.scale(k)
// 返回一个新变换,其平移 tx1 = tx0 + tx * k、ty1 = ty0 + ty * k
// 注意:平移量会乘以当前缩放因子 k(文档原文记作 t_k,即本变换的缩放)
transform.translate(x, y)
两个方法都返回新对象而不修改原变换,这使得变换组合可以像函数式一样层层叠加。
3.4 点的正变换与逆变换
这组方法是"屏幕坐标 ↔ 数据坐标"换算的核心,也是实现坐标轴联动缩放的基础:
| 方法 | 结果 |
|---|---|
transform.apply([x, y]) |
[xk + tx, yk + ty] |
transform.applyX(x) |
xk + tx |
transform.applyY(y) |
yk + ty |
transform.invert([x, y]) |
[(x - tx) / k, (y - ty) / k] |
transform.invertX(x) |
(x - tx) / k |
transform.invertY(y) |
(y - ty) / k |
apply 系列把数据平面上的点映射到屏幕(视口)位置;invert 系列做相反方向——把屏幕位置还原为数据平面上的点。*zoom*.constrain 的默认实现(见第四节)正是靠 invertX/invertY 来比较视口范围与平移范围的。
3.5 把变换应用到不同渲染目标
Canvas 2D 上下文:先 translate 再 scale(顺序不能反):
context.translate(transform.x, transform.y);
context.scale(transform.k, transform.k);
HTML 元素(CSS transform):
div.style("transform", "translate(" + transform.x + "px," + transform.y + "px) scale(" + transform.k + ")");
div.style("transform-origin", "0 0");
SVG 元素:
g.attr("transform", "translate(" + transform.x + "," + transform.y + ") scale(" + transform.k + ")");
// 更简洁:直接利用 toString()
g.attr("transform", transform);
transform.toString 的实现即:
function toString() {
return "translate(" + this.x + "," + this.y + ") scale(" + this.k + ")";
}
文档特别强调:变换顺序很重要,translate 必须先于 scale 应用。
四、zoom 的九个配置方法
以下方法均为 getter/setter 双模式:传入参数则设置并返回 zoom 行为(支持链式调用),不传参数则返回当前值。
4.1 zoom.filter(filter)——事件过滤器
若指定 filter,设置为过滤函数;否则返回当前 filter。默认 filter 为:
function filter(event) {
return (!event.ctrlKey || event.type === 'wheel') && !event.button;
}
filter 接收当前事件(event)和数据 d,this 为当前 DOM 元素。只要 filter 返回 falsy,发起事件就被忽略,不启动任何缩放手势——因此 filter 决定了哪些输入事件被忽略。默认 filter 忽略次级鼠标按钮的 mousedown(右键通常用于上下文菜单)。
从默认实现可以读出两条规则:按住 Ctrl 的普通事件被忽略,但 Ctrl + 滚轮例外(这是浏览器原生"Ctrl+滚轮缩放"习惯的保留);任何非左键(event.button 非 0)的鼠标事件一律忽略。
4.2 zoom.wheelDelta(delta)——滚轮灵敏度
默认实现:
function wheelDelta(event) {
return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * (event.ctrlKey ? 10 : 1);
}
返回值 Δ 决定对一次 WheelEvent 应用的缩放幅度:缩放因子 k 乘以 2^Δ——Δ = +1 使 k 翻倍,Δ = -1 使 k 减半。三个系数各有含义:
- 乘以
-event.deltaY:向上滚(deltaY 为负)放大,向下滚缩小; deltaMode分支:deltaMode === 1是"按行"计量的滚轮(0.05/行),deltaMode === 2是"按页"(系数 1),其余(像素模式,deltaMode === 0)用 0.002/像素;ctrlKey ? 10 : 1:按住 Ctrl 时缩放速度快 10 倍。
4.3 zoom.touchable(touchable)——触摸支持检测
默认检测器:
function touchable() {
return navigator.maxTouchPoints || ("ontouchstart" in this);
}
只有当检测器对相应元素返回 truthy 时,才会注册触摸事件监听器——这避免在纯桌面环境注册无用的 touch 监听。文档指出默认检测器对大多数支持触摸的浏览器有效,但对个别环境会失效(例如 Chrome 的移动设备模拟器)。此时可以传入自定义检测函数覆盖。
4.4 zoom.extent(extent)——视口范围
zoom.extent([[x0, y0], [x1, y1]]);
设置为视口范围:[x0, y0] 是视口左上角,[x1, y1] 是右下角。extent 也可以是返回该数组的函数(按每个选中元素调用,this 为 DOM 元素)。不指定时返回当前 accessor,默认是 [[0, 0], [width, height]],其中 width、height 取元素的 client width/height;对 SVG 元素则取最近祖先 SVG 元素的 viewBox 或 width/height 属性。文档也提示可考虑改用 element.getBoundingClientRect。
视口范围影响多处逻辑:*zoom*.scaleBy / *zoom*.scaleTo 变化时视口中心保持固定;interpolateZoom 选择平滑缩放路径时依赖视口中心和尺寸;强制可选的 translate extent 也需要它。
4.5 zoom.scaleExtent(extent)——缩放上下限
zoom.scaleExtent([k0, k1]); // k0 最小缩放,k1 最大缩放;默认 [0, ∞]
scale extent 限制放大/缩小范围,在交互以及 *zoom*.scaleBy、*zoom*.scaleTo、*zoom*.translateBy 时强制执行;但用 *zoom*.transform 显式设置变换时不强制。一个值得注意的交互行为:当用户已经在 scale extent 的某一极限处继续滚动滚轮时,wheel 事件会被忽略而不发起缩放手势——这让"放大后继续向下滚动页面"或"缩小后向上滚动"成为可能。如果你希望无论如何都阻止滚轮触发的页面滚动,可以自行注册 wheel 监听器来阻止浏览器默认行为:
selection
.call(zoom)
.on("wheel", event => event.preventDefault());
4.6 zoom.translateExtent(extent)——平移边界
zoom.translateExtent([[x0, y0], [x1, y1]]); // 世界的左上角/右下角;默认 [[-∞,-∞],[+∞,+∞]]
translate extent 限制平移范围,且缩小视口时可能引起额外平移(把缩回来的画面拉回边界内)。与 scale extent 相同,它在交互和 scaleBy/scaleTo/translateBy 时强制执行,但 *zoom*.transform 显式设置时不强制。
4.7 zoom.constrain(constrain)——自定义约束函数
constrain 必须是一个给定当前 transform、视口范围、平移范围 后返回 transform 的函数。默认实现是:
function constrain(transform, extent, translateExtent) {
var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0],
dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0],
dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1],
dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1];
return transform.translate(
dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),
dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)
);
}
这段默认实现的逻辑值得逐行拆解:
- 用
invertX/invertY把视口四角从屏幕坐标换算回世界坐标,再减去translateExtent的对应角,得到视口相对边界的"越界量" dx0/dx1、dy0/dy1; - 若
dx1 > dx0(视口比世界还宽,无法填满边界),取中点(dx0 + dx1) / 2居中显示; - 否则
Math.min(0, dx0) || Math.max(0, dx1):视口左边界越界(dx0 > 0)则拉回,右边界越界(dx1 < 0)则推回,均未越界则保持不动。
其语义就是"尽力保证视口范围不超出平移范围"。你传入自定义 constrain 即可完全接管约束策略(例如只约束 x 轴、或实现环绕平移)。
4.8 距离、时长与插值:clickDistance / tapDistance / duration / interpolate
*zoom*.clickDistance(distance):设置 mousedown 与 mouseup 之间鼠标允许移动的最大距离(以 client 坐标 event.clientX/clientY 计)。若期间移动距离达到该值,mouseup 后的 click 事件将被抑制。默认值为 0(任何移动都抑制后续点击)。*zoom*.tapDistance(distance):双击手势中第一次 touchstart 与第二次 touchend 之间允许移动的最大距离,超过则不触发双触事件。默认值为 10。*zoom*.duration(duration):设置双击/双触触发的缩放过渡时长(毫秒),默认 250。若时长不大于 0,双击和双触将触发即时(无动画)的变换。若要彻底禁用双击/双触缩放,可在挂载后移除 dblclick 监听器:
selection
.call(zoom)
.on("dblclick.zoom", null);
*zoom*.interpolate(interpolate):设置缩放过渡使用的插值工厂。默认是 interpolateZoom(实现"平滑缩放");若要两个视图之间的直接线性插值,可改用 d3-interpolate 的通用 interpolate。
4.9 zoom.on(typenames, listener)——监听缩放事件
*zoom*.on(*typenames*, *listener*) 为指定 typenames 设置事件 listener;同类型同名监听器会先移除旧监听再添加新的;listener 为 null 时移除;不传 listener 时返回当前匹配的第一个监听器。监听器被调用时的上下文与参数和 selection.on 一致:当前事件(event)、数据 d,this 为当前 DOM 元素。
typenames 是空格分隔的若干 typename;每个 typename 是一个 type,可选地跟一个点和 name(如 zoom.foo 与 zoom.bar 可以为同一 type 注册多个具名监听)。type 必须是以下三者之一:
| type | 触发时机 |
|---|---|
start |
缩放开始后(如 mousedown) |
zoom |
缩放变换发生变化后(如 mousemove) |
end |
缩放结束后(如 mouseup) |
更多细节参见 dispatch.on(zoom 行为的事件分发底层即 d3-dispatch)。
五、编程式控制:zoom.transform 与三个便捷方法
5.1 zoom.transform(selection, transform, point)
这是所有编程式缩放的基石:
- 若 selection 是选择:立即把选中元素的当前缩放变换设置为 transform,并立即发出 start、zoom、end 三个事件;
- 若 selection 是过渡(transition):用 interpolateZoom 定义一个 "zoom" tween 过渡到 transform——过渡开始时发 start,过渡的每一 tick 发 zoom 事件,过渡结束(或被中断)时发 end。过渡会尝试最小化指定 point 附近的视觉移动量;point 未指定时默认为视口 extent 的中心。
transform 可以是 zoom transform 本身,或返回它的函数;point 可以是 [x, y] 二元组或返回二元组的函数。作为函数时,对每个选中元素调用,传入当前事件(event)和数据 d,this 为当前 DOM 元素。
同样建议通过 selection.call 或 transition.call 调用。示例:
// 立即重置为恒等变换
selection.call(zoom.transform, d3.zoomIdentity);
// 750 毫秒平滑过渡回恒等变换
selection.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
重要限制:*zoom*.transform 要求你完整指定新变换,且不强制执行已定义的 scale extent 和 translate extent。若要从现有变换派生新变换并强制执行两个范围,请使用下面的便捷方法。
5.2 三个便捷方法:translateBy / translateTo / scaleBy / scaleTo
这四个便捷方法都以 *zoom*.transform 为基础,区别在于"以当前变换为起点增量派生",因此自动遵守 scale/translate extent:
| 方法 | 语义 |
|---|---|
*zoom*.translateBy(selection, x, y) |
平移当前变换:新 tx1 = tx0 + kx,ty1 = ty0 + ky(平移量乘当前缩放因子) |
*zoom*.translateTo(selection, x, y, p) |
让数据平面坐标 ⟨x,y⟩ 显示到屏幕点 p:新 tx = px − kx,ty = py − ky;p 默认为视口 extent 中心 |
*zoom*.scaleBy(selection, k, p) |
缩放当前变换 k 倍:新 k1 = k0 × k;参考点 p 保持不动,默认为视口 extent 中心 |
*zoom*.scaleTo(selection, k, p) |
把缩放设为绝对值:新 k1 = k;参考点 p 保持不动,默认为视口 extent 中心 |
四个方法的参数都支持"数值或返回数值的函数"两种形式;p 支持二元组 [px, py] 或返回二元组的函数。作为函数时传入当前数据 d 和索引 i,this 为当前 DOM 元素。且当 selection 是 transition 时,四个方法都会定义对应的 "zoom" tween 平滑过渡。
典型实战是缩放按钮——这也正是 zoomTransform 读取当前状态 + scaleBy/scaleTo 派生新状态的经典组合:
// 按钮放大 2 倍(过渡 250ms),scaleExtent 自动生效
selection.transition().duration(250)
.call(zoom.scaleBy, 2);
六、Zoom 事件详解
zoom 事件监听器被调用时,第一个参数是当前 zoom 事件对象,暴露四个字段:
| 字段 | 含义 |
|---|---|
event.target |
关联的 zoom 行为 |
event.type |
字符串 "start" / "zoom" / "end" |
event.transform |
当前的 zoom transform |
event.sourceEvent |
底层输入事件(如 mousemove、touchmove) |
zoom 行为处理的完整交互事件矩阵(照录自 docs/d3-zoom.md):
| 事件 | 监听元素 | 触发的 Zoom 事件 | 是否 preventDefault |
|---|---|---|---|
| mousedown⁵ | selection | start | 否¹ |
| mousemove² | window¹ | zoom | 是 |
| mouseup² | window¹ | end | 是 |
| dragstart² | window | - | 是 |
| selectstart² | window | - | 是 |
| click³ | window | - | 是 |
| dblclick | selection | 多个⁶ | 是 |
| wheel⁸ | selection | zoom⁷ | 是 |
| touchstart | selection | 多个⁶ | 否⁴ |
| touchmove | selection | zoom | 是 |
| touchend | selection | end | 否⁴ |
| touchcancel | selection | end | 否⁴ |
脚注解读(原文脚注的中文化说明):
- 必须在 window 上监听,才能捕获 iframe 外溢到视口之外部分的事件;
- 仅在实际进行的鼠标手势期间适用(mousemove/mouseup/dragstart/selectstart 均监听在 window 上,防止拖出元素后事件丢失);
- click 抑制仅在刚发生过鼠标手势后立即生效,阈值由 zoom.clickDistance 控制;
- touchstart/touchend/touchcancel 不 preventDefault,是为了允许触摸输入上的 click 模拟(Safari 等浏览器的点击兼容机制);
- mousedown 在 500ms 内(刚结束 touch 手势)会被忽略——假设浏览器做了 click 模拟;
- 双击/双触发起一次过渡,依次发出 start、zoom、end 事件;双触移动距离阈值由 zoom.tapDistance 控制;
- 第一个 wheel 事件发出 start,150ms 内再无 wheel 事件时发出 end(即滚轮"按住连滚"被视为一次持续手势);
- 若已处于 scale extent 对应极限,wheel 事件被忽略。
所有被"消费"的事件其传播都会立即停止(stopImmediatePropagation),这是 zoom 能与 drag、brush 等行为共存的底层机制——文档在 CHANGES.md 中亦明确记录了这一设计意图("zoom 行为消费已处理的事件,便于与 drag 等其他交互行为组合",见 CHANGES.md)。
最常用的一行式监听:
const zoom = d3.zoom()
.on("zoom", (event) => {
g.attr("transform", event.transform); // 把整组图形按变换平移+缩放
});
selection.call(zoom);
七、rescaleX / rescaleY:让坐标轴跟随缩放
这是 d3-zoom 与 d3-scale、d3-axis 联动缩放的核心。
*transform*.rescaleX(x) 返回连续比例尺 x 的副本,其 domain 被变换。实现分两步:先对比例尺的 range 逐点施加 inverse x-transform(invertX),再用 inverse scale(x.invert)把 range 值反算成新的 domain:
function rescaleX(x) {
var range = x.range().map(transform.invertX, transform),
domain = range.map(x.invert, x);
return x.copy().domain(domain);
}
*transform*.rescaleY(y) 完全对称:
function rescaleY(y) {
var range = y.range().map(transform.invertY, transform),
domain = range.map(y.invert, y);
return y.copy().domain(domain);
}
两条硬约束:
- 比例尺 x/y 必须使用 interpolateNumber(即普通数值插值),不要使用 continuous.rangeRound——取整会降低 continuous.invert 的精度,导致重算后的 domain 不准确;
- 该方法不修改传入的比例尺:x 代表未变换的比例尺,返回值才是它的"变换后视图"。
标准"缩放坐标轴"模式(在 zoom 回调里重建轴):
function zoomed(event) {
g.attr("transform", event.transform);
// 用当前变换重算 x 轴,再重绘
gx.call(d3.axisBottom(event.transform.rescaleX(x)));
}
八、平滑缩放插值 interpolateZoom 与版本说明
*zoom*.transform 作用于 transition 时,以及双击/双触触发的过渡,默认都经由 interpolateZoom 实现"平滑缩放"。它源自 van Wijk & Nuij 的论文 "Smooth and efficient zooming and panning":两个视图各用三元组 [cx, cy, width] 描述(视口中心 + 视口宽度),插值器沿一条曲率最优的路径过渡,并暴露 interpolate.duration 属性——推荐时长(毫秒)由 xy 空间曲线路径长度决定;想要更快/更慢就乘以一个任意缩放系数(论文中的 V 参数)。interpolateZoom.rho(rho) 可调曲率:rho 接近 0 时轨迹接近线性,默认曲率为 √2(详见 docs/d3-interpolate/zoom.md)。
版本与兼容性前提。本文所有 API 行为以本仓库 package.json 声明的依赖为准:d3 7.9.0,d3-zoom ^3.0.0,运行环境 Node >= 12,模块系统为 ESM("type": "module",src/index.js 即入口)。从 CHANGES.md 可以确认 zoom 行为在 d3 v4 时代的主要 API 定型事实:事件监听器直接收到 event、默认 filter 支持 Ctrl+滚轮、默认 wheelDelta 在 Ctrl 下加速、新增 zoom.tapDistance、以及"wheel 在 scale extent 极限处被忽略"的滚动穿透行为——这些正是上文各节描述的当前默认行为的历史由来。
九、完整可运行示例
下面把本文 API 串成一个最小可运行的可缩放散点图骨架(ESM 方式,等价于本仓库 src/index.js 导出的命名空间):
import * as d3 from "d3"; // 或浏览器 <script> 引入 dist/d3.min.js 后用全局 d3
const width = 600, height = 400;
const g = d3.select("svg").append("g");
// 1. 创建 zoom 行为并配置
const zoom = d3.zoom()
.scaleExtent([0.5, 8]) // 缩放范围:0.5x ~ 8x
.translateExtent([[0, 0], [width, height]]) // 平移不超出画布
.on("zoom", (event) => {
// 2. 用 event.transform 驱动整个分组
g.attr("transform", event.transform);
// 如需坐标轴联动:
// gx.call(d3.axisBottom(event.transform.rescaleX(x)));
});
d3.select("svg").call(zoom);
// 3. 编程式控制:双击后仍可继续用代码缩放
d3.select("#reset").on("click", () => {
d3.select("svg").transition().duration(750)
.call(zoom.transform, d3.zoomIdentity); // 平滑复位
});
// 4. 按钮放大 2 倍(自动遵守 scaleExtent)
d3.select("#in").on("click", () => {
d3.select("svg").transition().duration(250)
.call(zoom.scaleBy, 2);
});
// 5. 如需关闭滚轮缩放(不影响拖拽平移)
// d3.select("svg").on("wheel.zoom", null);
要点回顾:状态存在元素上(__zoom),用 d3.zoomTransform(node) 或 event.transform 读取;显式 zoom.transform 不强制 extent,增量操作一律走 scaleBy/scaleTo/translateBy/translateTo;监听器用 .zoom 命名空间前缀可被 selection.on(".zoom", null) 一键摘除。
十、参考索引
| 主题 | 仓库内位置 |
|---|---|
| d3-zoom 完整 API(本文主体来源) | docs/d3-zoom.md |
| interpolateZoom 平滑插值 | docs/d3-interpolate/zoom.md |
| 选择与 selection.call / .on | docs/d3-selection/control-flow.md、docs/d3-selection/events.md |
| 过渡与 transition.call | docs/d3-transition/control-flow.md |
| 连续比例尺 domain/range/invert | docs/d3-scale/linear.md |
| 通用插值 interpolate | docs/d3-interpolate/value.md |
| d3-dispatch(zoom.on 底层) | docs/d3-dispatch.md |
| 与 zoom 组合的 drag / brush | docs/d3-drag.md、docs/d3-brush.md |
| 版本依赖声明 | package.json |
| 全量导出测试 | test/d3-test.js |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00