d3-zoom
视图移动以及缩放是一种将用户注意力聚焦在感兴趣区域的一种流行的交互技术。操作直接,容易理解: 点击并拖拽平移,使用滚轮进行缩放,当然也可以通过触摸进行。平移和缩放被广泛的应用在地图中,但是也可被应用到其他的可视化比如时间序列以及散点图中。
缩放行为通过 d3-zoom
模块实现,能方便且灵活到 selections 上。它处理了许多 Installing
NPM
安装: npm install d3-zoom
. 此外还可以下载 latest release. 可以直接从 d3js.org, 以 standalone library 或作为 D3 4.0 的一部分引入. 支持 AMD
, CommonJS
以及基本的标签引入形式,如果使用标签引入则会暴露全局 d3
变量:
<script src="https://d3js.org/d3-color.v1.min.js"></script>
<script src="https://d3js.org/d3-dispatch.v1.min.js"></script>
<script src="https://d3js.org/d3-ease.v1.min.js"></script>
<script src="https://d3js.org/d3-interpolate.v1.min.js"></script>
<script src="https://d3js.org/d3-selection.v1.min.js"></script>
<script src="https://d3js.org/d3-timer.v1.min.js"></script>
<script src="https://d3js.org/d3-transition.v1.min.js"></script>
<script src="https://d3js.org/d3-drag.v1.min.js"></script>
<script src="https://d3js.org/d3-zoom.v1.min.js"></script>
<script>
var zoom = d3.zoom();
</script>
API Reference
下面这个表描述了缩放行为如何利用原生事件:
Event | Listening Element | Zoom Event | Default Prevented? |
---|---|---|---|
mousedown⁵ | selection | start | no¹ |
mousemove² | window¹ | zoom | yes |
mouseup² | window¹ | end | yes |
dragstart² | window | - | yes |
selectstart² | window | - | yes |
click³ | window | - | yes |
dblclick | selection | multiple⁶ | yes |
wheel⁸ | selection | zoom⁷ | yes |
touchstart | selection | multiple⁶ | no⁴ |
touchmove | selection | zoom | yes |
touchend | selection | end | no⁴ |
touchcancel | selection | end | no⁴ |
所有事件的传播会被 immediately stopped
¹ 在 iframe
之外捕获事件的必要条件; 参考 d3-drag"https://github.com/d3/d3-drag/issues/9" target="_blank" rel="noopener noreferrer">d3-drag"https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html" target="_blank" rel="noopener noreferrer">click emulation(点击模拟) 的必要条件; 参考 d3-drag"https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html" target="_blank" rel="noopener noreferrer">click emulation(点击模拟).
⁶ 双击和双击启动一个转换,它会发出开始、缩放和结束事件。
⁷ 第一次滚轮事件发出一个启动事件; 当在 150ms
之内没有再次收到滚轮事件时,就会发出一个结束事件.
⁸ 如果已经达到了 scale extent 的限制,则忽略.
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
创建一个新的缩放行为,并返回该行为。zoom 既是一个对象又是一个函数,通过情况下通过 selection.call 来应用到元素上。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
将缩放行为应用到指定的 selection,并绑定必要的事件监听器用来允许平移和缩放,如果元素上没有定义 zoom transform 的话会将其初始化为 identity transform
。这个函数通常不会被直接调用,而是通过 selection.call 来调用。例如,初始化一个缩放行为并将其应用到指定的选择集:
selection.call(d3.zoom().on("zoom", zoomed));
在内部,缩放行为使用 selection.on 去绑定缩放必需的一些事件句柄。事件句柄会以 .zoom
为 name
,因此你可以使用如下方式来取消元素上所有的缩放行为的事件句柄:
selection.on(".zoom", null);
如果仅仅想将滚动滚轮事件取消(不干扰原生滚轮事件),则可以单独将缩放的滚轮事件取消:
selection
.call(zoom)
.on("wheel.zoom", null);
此外还可以使用 zoom.filter 来控制哪些事件可以开始缩放手势.
应用缩放行为也会将 -webkit-tap-highlight-color 设置为透明,禁用 iOS
上的高亮显示功能. 如果你需要一个不一样的高亮颜色则可以在应用缩放行为之后重新设置这个样式。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果 selection 为选择集,则设置已选中元素的 current zoom transform 为指定的 transform, 并立即触发 start
, zoom
以及 end
events。如果 selection 为过渡则使用 d3.interpolateZoom 定义一个 “zoom” 补间并在过渡开始时触发 start
事件,过渡中的每一帧都触发一次 zoom
事件,结束或中断时触发 end
事件。transform 可以是 zoom transform(缩放变换) 也可以是返回缩放变换的函数。如果是函数则会为选择集中的每一个元素调用,并传递当前元素数据 d
以及索引 i
, 函数内部 this
指向当前 DOM
元素。
这个函数通常不会直接调用,而是通过 selection.call 或 transition.call 调用的。比如重置缩放变换为 identity transform:
selection.call(zoom.transform, d3.zoomIdentity);
或者平滑的重置缩放(750ms
):
selection.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
这个方法要求指定新的缩放变换,不会受 scale extent 和 translate extent 的影响。如果要从已有的变化出发并考虑缩放限制则可以使用 zoom.translateBy, zoom.scaleBy 和 zoom.scaleTo。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果 selection 为选择集,则将选中元素上的 current zoom transform translates x 和 y(相对于当前位置),新的 tx1 = tx0 + kx ,新的 ty1 = ty0 + ky. 如果 selection 则会创建一个 “zoom” 补间。这个方法是 zoom.transform 的方便用法。x 和 y 可以是数字或者返回数字的函数。如果是函数则会为每个选中的元素调用,并传递当前元素绑定的数据 d
以及索引 i
,函数内部 this
指向当前 DOM
元素。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果 selection 为选择集,则将选中元素上的 current zoom transform translates 设置为指定的 ⟨x,y⟩ (不考虑当前变换), 并且移动之后 viewport extent 中心为指定的坐标。新的 tx = cx - kx,新的 ty = cy - ky,其中 ⟨cx,cy⟩ 为视图中心。如果 selection 为过渡,则会定义一个 “zoom” 补间来过渡当前变换。这个方法是 zoom.transform 的便利用法。x 和 y 坐标必须为数值或者返回数值的函数,如果是函数则会为每个已选中的元素调用,并传递当前元素绑定的数据 d
以及索引 i
,函数内部 this
指向当前 DOM
元素。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果 selection 为选择集,则将已经选中的元素的 current zoom transform scales 指定的倍数(在当前缩放基础上叠加缩放),新的 k₁ = k₀k。如果 selection 为过渡,则会为当前变换定义一个 “zoom” 补间。这个方法是 zoom.transform 的便捷用法。t 必须为数值或者返回数值的函数。如果是函数则会为每个已选中的元素调用,并传递当前元素绑定的数据 d
以及索引 i
,函数内部 this
指向当前 DOM
元素。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果 selection 为选择集,则将已经选中的元素的 current zoom transform scales 到指定的倍数(不考虑当前缩放),新的 k₁ = k。如果 selection 为过渡,则会为当前变换定义一个 “zoom” 补间。这个方法是 zoom.transform 的便捷用法。t 必须为数值或者返回数值的函数。如果是函数则会为每个已选中的元素调用,并传递当前元素绑定的数据 d
以及索引 i
,函数内部 this
指向当前 DOM
元素。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果指定了 constrain 则将变换限制设置为指定的函数并返回缩放行为。如果没有指定 constrain 则返回当前的限制函数,默认为:
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)
);
}
限制函数必须给定当前 transform, viewport extent 和 translate extent 并返回 transform。默认的实现是试图确保 viewport
的范围不会超出缩放的平移区间范围。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果指定了 filter,则将过滤器设置为指定的函数并返回缩放行为。如果没有指定 filter 则返回当前过滤器,默认为:
function filter() {
return !d3.event.button;
}
如果过滤器返回假值,则初始事件会被忽略并不会触发缩放手势的开始。因此过滤器可以用来设置哪些事件被忽略。默认的过滤器忽略了次要按钮上的 mousedown
事件,因为这些按钮通常被用作其他用途,比如菜单。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果指定了 touchable 则将触摸支持检测器设置为指定的函数并返回缩放行为。如果没有指定 touchable 则返回当前触摸支持检测器,默认为:
function touchable() {
return "ontouchstart" in this;
}
触摸事件通常在触摸支持检测器返回真的时候才会在缩放被 applied 被注册到对应的元素。对于大多数能够触摸输入的浏览器来说,默认的检测器很好用,但不是全部; 例如,Chrome
的移动设备模拟器无法有效检测。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果指定了 delta 则将滚轮的 delta
函数设置为指定的函数并返回缩放行为。如果没有指定 delta 则返回滚轮当前的 delta
函数,默认为:
function wheelDelta() {
return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1) / 500;
}
delata
函数返回的值 Δ 定义了 WheelEvent 与缩放量的影响。缩放因子 transform.k 会乘以 2Δ。例如 Δ + 1 会使缩放因子增加一倍而 Δ - 1 会使缩放因子减少一倍。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果指定了 extent 则将当前视口范围设置为指定的数组 [[x0, y0], [x1, y1]],其中 [x0, y0] 是视口的左上角坐标,[x1, y1] 是视口的右下角坐标,并返回缩放行为。extent 也可以是一个返回数组的函数。如果为函数,则会为每个选中的元素调用,并传递当前数据 d
以及索引 i
,其中 this
指向当前 DOM
元素。
如果没有指定 extent 则返回去当前 extent
访问器,默认为 [[0, 0], [width, height]], 其中 width 为元素的 client width 而 height 为元素的 client height;对于 SVG
元素(属于 SVG
的元素)来说会使用最近的祖先 SVG
元素的 width 和 height。在这种情况下,所属 SVG
元素必须包含 width 和 height 而不是依赖 CSS
样式或者 viewBox
属性;SVG
不提供检索 initial viewport size(初始视口大小) 的编程方法。可以考虑使用 element.getBoundingClientRect (在火狐中,SVG
的 element.clientWidth 和 element.clientHeight 为零)
视口的范围影响以下几个函数: 由 zoom.scaleBy 和 zoom.scaleTo 引起的改变则视口中心会保持不变;视口中心和维度会影响 d3.interpolateZoom 的路径;为了执行 translate extent 视口范围是需要的。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果指定了 extent 则将缩放范围设置为指定的 [k0, k1],其中 k0 为允许缩放的最小因子而 k1 为缩放的最大缩放因子,并返回缩放行为。如果没有指定 extent 则返回当前缩放范围,默认为 [0, ∞]。缩放范围约束了缩小和放大。在手动交互阶段或者使用 zoom.scaleBy, zoom.scaleTo 和 zoom.translateBy 的时候执行;然而如果使用 zoom.transform 来明确的指定变换则不会被执行。
如果用户在已经达到缩放边界的情况下继续缩放,则滚轮事件会被忽略并不会初始化缩放手势。这样就允许用户在缩放后滚动到可缩放的区域,或者在缩小后向上滚动。如果你希望无论缩放尺度大小都始终避免上下滚动,则在可以在选择集上注册一个事件监听器来阻止浏览器的默认行为:
selection
.call(zoom)
.on("wheel", function() { d3.event.preventDefault(); });
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果指定了 extent 则将当前的平移区间设置为指定的数组: [[x0, y0], [x1, y1]], 其中 [x0, y0] 为世界的左上角而 [x1, y1] 为世界的右下角,并返回缩放行为。如果没有指定 extent 则返回当前的平移范围,默认为 [[-∞, -∞], [+∞, +∞]]。平移范围限制了视口的移动以及因为缩小引起的平移。它在交互以及使用 zoom.scaleBy, zoom.scaleTo 和 zoom.translateBy 的时候执行;然而如果使用 zoom.transform 来明确的指定变换则不会被执行。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果指定了 distance 则将click
事件的触发条件: mousedown
和 mouseup
之间鼠标移动的距离设置为指定的距离。如果鼠标按下时的坐标与鼠标抬起时的坐标之间的距离大于或等于 distance 则不会触发随后的 click
事件。如果没有指定 distance 则返回当前的默认值,默认为 0。距离阈值通过坐标系统 (event.clientX 和 event.clientY) 测量得到。
"https://github.com/d3/d3-zoom/blob/master/src/zoom.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
如果指定了 duration 则将双击放大的过渡事件设置为指定的数值(毫秒)并返回缩放行为,如果没有指定 duration 则返回当前的过渡时长,默认为 250ms
。如果过渡时长小于等于 0
则在双击放大时不会有平滑过渡效果。
如果要禁用双击放大则可以在应用缩放行为之后移除选择集的缩放行为的双击事件:
selection
.call(zoom)
.on("dblclick.zoom", null);
Zoom Events
当 Zoom Transforms
缩放行为载缩放被 applied 的时候将缩放状态存储在元素上,而不是存储在缩放行为自身。这是因为缩放行为可能同时应用在多个元素上,并且每个元素的缩放都是独立的。缩放状态可以通过交互或 通过 zoom.transform 编程的形式改变。
为了检索缩放的状态,请在当前 zoom event 事件监听器内使用 event.transform 来获取(参考 zoom.on),或者使用为给定节点使用 d3.zoomTransform。后者在通过编程的形式缩放状态时很有用,比如要实现缩小和放大的按钮。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回指定 node 上当前缩放变换。node 通常是 DOM
元素而不是 selection。(选择集中可能包含多个状态不同的元素,而这个方法会返回一个单一的变换)。如果你有一个选择集则可以先调用 selection.node:
var transform = d3.zoomTransform(selection.node());
在 event listener 的上下文中,node 通常是接收事件输入的元素(应该等价于 event.transform),this:
var transform = d3.zoomTransform(this);
在内部,元素的变换存储在 element.__zoom;但是应该通过方法去修改它,而最好不要直接修改。如果给定的 node 没有定义变换,则返回 identity transformation。返回的变换表示了一个变换矩阵:
k 0 tx
0 k ty
0 0 1
(这个矩阵只能表示缩放和平移,未来可能会支持旋转,尽管不会向后兼容)。坐标 ⟨x,y⟩ 被变换到 ⟨xk + tx,yk + ty⟩。变换对象暴露以下属性:
- transform.x - 在x-轴上的平移量 tx
- transform.y - 在y-轴上的平移量 ty
- transform.k - 缩放因子 k
这些属性应该被认为是只读的;不要直接去修改变换,而是通过 transform.scale 和 transform.translate 去获取一个新的转换。参考 zoom.scaleBy, zoom.scaleTo 和 zoom.translateBy 这些更方便的方法。根据给定的 k, tx, 和 ty 创建一个变换:
var t = d3.zoomIdentity.translate(x, y).scale(k);
将变换应用到 Canvas 2D context,则使用在 context.translate 之后使用 context.scale:
context.translate(transform.x, transform.y);
context.scale(transform.k, transform.k);
类似的将变换通过 CSS 应用到 HTML
元素:
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 + ")");
更方便的是,使用 transform.toString 的优势:
g.attr("transform", transform);
要注意变换的顺序,平移一定要在缩放前。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回一个 k₁ 等于 k₀k 的变换,其中 k₀ 是当前变换的缩放因子(也就是将当前变换缩小或放大一个倍数)。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回一个平移量 tx1 和 ty1 分别等于 tx0 + x 和 ty0 + y 的变换,其中 tx0 和 ty0 当前变换的平移量。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回指定的点经过当前变换变换后的坐标,其中 point 由二元数字数组 [x, y] 表示。返回的坐标等价于 [xk + tx, yk + ty].
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回指 x 坐标经过变换后的 x 坐标,等于 xk + tx。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回指 y 坐标经过变换后的 y 坐标,等于 yk + ty。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回指定 point 的逆变换,point 由二元数组 [x, y] 表示。返回的点 point 等价于 [(x - tx) / k, (y - ty) / k]。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回指定 x 坐标的逆变换,等于 (x - tx) / k。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回指定 y 坐标的逆变换,等于 (y - ty) / k。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回指定 continuous scale x,且 domain 结果变换的拷贝。这是通过首先在刻度范围内应用 inverse x-transform ,然后使用 inverse scale 来计算响应的值域实现的:
function rescaleX(x) {
var range = x.range().map(transform.invertX, transform),
domain = range.map(x.invert, x);
return x.copy().domain(domain);
}
比例尺 x 必须使用 d3.interpolateNumber;而不能使用 continuous.rangeRound,因为会降低 continuous.invert 的准确性并且可能导致缩放之后 domain
的错乱。这个方法不会修改输入比例尺 x;因此 x 表示未变换的比例尺,而返回的比例尺则表示经过缩放变换之后的比例尺。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回指定 continuous scale y,且 domain 结果变换的拷贝。这是通过首先在刻度范围内应用 inverse y-transform ,然后使用 inverse scale 来计算响应的值域实现的:
function rescaleY(y) {
var range = y.range().map(transform.invertY, transform),
domain = range.map(y.invert, y);
return y.copy().domain(domain);
}
比例尺 y 必须使用 d3.interpolateNumber;而不能使用 continuous.rangeRound,因为会降低 continuous.invert 的准确性并且可能导致缩放之后 domain
的错乱。这个方法不会修改输入比例尺 y;因此 y 表示未变换的比例尺,而返回的比例尺则表示经过缩放变换之后的比例尺。
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
返回一个能直接被 SVG transform 识别的表示变换的字符串,通过一下方式实现:
function toString() {
return "translate(" + this.x + "," + this.y + ") scale(" + this.k + ")";
}
"https://github.com/d3/d3-zoom/blob/master/src/transform.js" title="Source" target="_blank" rel="noopener noreferrer"><源码>
恒等变换,其中 k = 1, tx = ty = 0.