# d3-zoom

视图移动以及缩放是一种将用户注意力聚焦在感兴趣区域的一种流行的交互技术。操作直接,容易理解: 点击并拖拽平移,使用滚轮进行缩放,当然也可以通过触摸进行。平移和缩放被广泛的应用在地图中,但是也可被应用到其他的可视化比如时间序列以及散点图中。

缩放行为通过 d3-zoom 模块实现,能方便且灵活到 selections (opens new window) 上。它处理了许多 input events 以及浏览器的怪异模式。缩放行为与 DOM 无关,可以应用于 SVG, HTML 或者 Canvas

Canvas Zooming (opens new window)SVG Zooming (opens new window)

缩放行为可也以与 d3-scale (opens new window)d3-axis (opens new window) 一起工作; 参考 transform.rescaleX and transform.rescaleY. 你可以使用 zoom.scaleExtent 显示平移范围或使用 zoom.translateExtent 来显示缩放尺度。

Axis Zooming (opens new window)

缩放行为可以用来和其他的交互结合,比如用以拖拽的 d3-drag (opens new window) 和用以刷取的 d3-brush (opens new window)

Drag & Zoom II (opens new window)Brush & Zoom (opens new window)

缩放行为可以通使用 zoom.transform 以编程的方式控制,允许使用用户操作面板以及根据数据来驱动显示状态的变化。平滑缩放过渡基于 Jarke J. van WijkWim A.A. Nuij“Smooth and efficient zooming and panning” (opens new window)

Zoom Transitions (opens new window)

参考 d3-tile (opens new window) 获取关于平移和缩放地图的例子.

# Installing

NPM 安装: npm install d3-zoom. 此外还可以下载 latest release (opens new window). 可以直接从 d3js.org (opens new window), 以 standalone library (opens new window) 或作为 D3 4.0 (opens new window) 的一部分引入. 支持 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>

在浏览器中测试 d3-zoom. (opens new window)

# 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 (opens new window)

¹ 在 iframe 之外捕获事件的必要条件; 参考 d3-drag#9 (opens new window).
² 只适用于以鼠标为基础的活动; 参考 d3-drag#9 (opens new window).
³ 只在一些基于鼠标的手势之后立即应用; 参考 zoom.clickDistance.
⁴ 在触摸输入上允许 click emulation(点击模拟) (opens new window) 的必要条件; 参考 d3-drag#9 (opens new window).
⁵ 忽略触摸事件结束后的 500ms 之内的事件; 假设 click emulation(点击模拟) (opens new window).
⁶ 双击和双击启动一个转换,它会发出开始、缩放和结束事件。
⁷ 第一次滚轮事件发出一个启动事件; 当在 150ms 之内没有再次收到滚轮事件时,就会发出一个结束事件.
⁸ 如果已经达到了 scale extent 的限制,则忽略.

# d3.zoom() <源码> (opens new window)

创建一个新的缩放行为,并返回该行为。zoom 既是一个对象又是一个函数,通过情况下通过 selection.call (opens new window) 来应用到元素上。

# zoom(selection) <源码> (opens new window)

将缩放行为应用到指定的 selection (opens new window),并绑定必要的事件监听器用来允许平移和缩放,如果元素上没有定义 zoom transform 的话会将其初始化为 identity transform。这个函数通常不会被直接调用,而是通过 selection.call (opens new window) 来调用。例如,初始化一个缩放行为并将其应用到指定的选择集:

selection.call(d3.zoom().on("zoom", zoomed));

在内部,缩放行为使用 selection.on (opens new window) 去绑定缩放必需的一些事件句柄。事件句柄会以 .zoomname,因此你可以使用如下方式来取消元素上所有的缩放行为的事件句柄:

selection.on(".zoom", null);

如果仅仅想将滚动滚轮事件取消(不干扰原生滚轮事件),则可以单独将缩放的滚轮事件取消:

selection
    .call(zoom)
    .on("wheel.zoom", null);

此外还可以使用 zoom.filter 来控制哪些事件可以开始缩放手势.

应用缩放行为也会将 -webkit-tap-highlight-color (opens new window) 设置为透明,禁用 iOS 上的高亮显示功能. 如果你需要一个不一样的高亮颜色则可以在应用缩放行为之后重新设置这个样式。

# zoom.transform(selection, transform) <源码> (opens new window)

如果 selection 为选择集,则设置已选中元素的 current zoom transform 为指定的 transform, 并立即触发 start, zoom 以及 end events。如果 selection 为过渡则使用 d3.interpolateZoom (opens new window) 定义一个 “zoom” 补间并在过渡开始时触发 start 事件,过渡中的每一帧都触发一次 zoom 事件,结束或中断时触发 end 事件。transform 可以是 zoom transform(缩放变换) 也可以是返回缩放变换的函数。如果是函数则会为选择集中的每一个元素调用,并传递当前元素数据 d 以及索引 i, 函数内部 this 指向当前 DOM 元素。

这个函数通常不会直接调用,而是通过 selection.call (opens new window)transition.call (opens new window) 调用的。比如重置缩放变换为 identity transform:

selection.call(zoom.transform, d3.zoomIdentity);

或者平滑的重置缩放(750ms):

selection.transition().duration(750).call(zoom.transform, d3.zoomIdentity);

这个方法要求指定新的缩放变换,不会受 scale extenttranslate extent 的影响。如果要从已有的变化出发并考虑缩放限制则可以使用 zoom.translateBy, zoom.scaleByzoom.scaleTo

# zoom.translateBy(selection, x, y) <源码> (opens new window)

如果 selection 为选择集,则将选中元素上的 current zoom transform translates xy(相对于当前位置),新的 tx1 = tx0 + kx ,新的 ty1 = ty0 + ky. 如果 selection 则会创建一个 “zoom” 补间。这个方法是 zoom.transform 的方便用法。xy 可以是数字或者返回数字的函数。如果是函数则会为每个选中的元素调用,并传递当前元素绑定的数据 d 以及索引 i,函数内部 this 指向当前 DOM 元素。

# zoom.translateTo(selection, x, y) <源码> (opens new window)

如果 selection 为选择集,则将选中元素上的 current zoom transform translates 设置为指定的 ⟨x,y⟩ (不考虑当前变换), 并且移动之后 viewport extent 中心为指定的坐标。新的 tx = cx - kx,新的 ty = cy - ky,其中 ⟨cx,cy⟩ 为视图中心。如果 selection 为过渡,则会定义一个 “zoom” 补间来过渡当前变换。这个方法是 zoom.transform 的便利用法。xy 坐标必须为数值或者返回数值的函数,如果是函数则会为每个已选中的元素调用,并传递当前元素绑定的数据 d 以及索引 i,函数内部 this 指向当前 DOM 元素。

# zoom.scaleBy(selection, k) <源码> (opens new window)

如果 selection 为选择集,则将已经选中的元素的 current zoom transform scales 指定的倍数(在当前缩放基础上叠加缩放),新的 k₁ = k₀k。如果 selection 为过渡,则会为当前变换定义一个 “zoom” 补间。这个方法是 zoom.transform 的便捷用法。t 必须为数值或者返回数值的函数。如果是函数则会为每个已选中的元素调用,并传递当前元素绑定的数据 d 以及索引 i,函数内部 this 指向当前 DOM 元素。

# zoom.scaleTo(selection, k) <源码> (opens new window)

如果 selection 为选择集,则将已经选中的元素的 current zoom transform scales 到指定的倍数(不考虑当前缩放),新的 k₁ = k。如果 selection 为过渡,则会为当前变换定义一个 “zoom” 补间。这个方法是 zoom.transform 的便捷用法。t 必须为数值或者返回数值的函数。如果是函数则会为每个已选中的元素调用,并传递当前元素绑定的数据 d 以及索引 i,函数内部 this 指向当前 DOM 元素。

# zoom.constrain([constrain]) <源码> (opens new window)

如果指定了 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 extenttranslate extent 并返回 transform。默认的实现是试图确保 viewport 的范围不会超出缩放的平移区间范围。

# zoom.filter([filter]) <源码> (opens new window)

如果指定了 filter,则将过滤器设置为指定的函数并返回缩放行为。如果没有指定 filter 则返回当前过滤器,默认为:

function filter() {
  return !d3.event.button;
}

如果过滤器返回假值,则初始事件会被忽略并不会触发缩放手势的开始。因此过滤器可以用来设置哪些事件被忽略。默认的过滤器忽略了次要按钮上的 mousedown 事件,因为这些按钮通常被用作其他用途,比如菜单。

# zoom.touchable([touchable]) <源码> (opens new window)

如果指定了 touchable 则将触摸支持检测器设置为指定的函数并返回缩放行为。如果没有指定 touchable 则返回当前触摸支持检测器,默认为:

function touchable() {
  return "ontouchstart" in this;
}

触摸事件通常在触摸支持检测器返回真的时候才会在缩放被 applied 被注册到对应的元素。对于大多数能够触摸输入的浏览器来说,默认的检测器很好用,但不是全部; 例如,Chrome 的移动设备模拟器无法有效检测。

# zoom.wheelDelta([delta]) <源码> (opens new window)

如果指定了 delta 则将滚轮的 delta 函数设置为指定的函数并返回缩放行为。如果没有指定 delta 则返回滚轮当前的 delta 函数,默认为:

function wheelDelta() {
  return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1) / 500;
}

delata 函数返回的值 Δ 定义了 WheelEvent (opens new window) 与缩放量的影响。缩放因子 transform.k 会乘以 2Δ。例如 Δ + 1 会使缩放因子增加一倍而 Δ - 1 会使缩放因子减少一倍。

# zoom.extent([extent]) <源码> (opens new window)

如果指定了 extent 则将当前视口范围设置为指定的数组 [[x0, y0], [x1, y1]],其中 [x0, y0] 是视口的左上角坐标,[x1, y1] 是视口的右下角坐标,并返回缩放行为。extent 也可以是一个返回数组的函数。如果为函数,则会为每个选中的元素调用,并传递当前数据 d 以及索引 i,其中 this 指向当前 DOM 元素。

如果没有指定 extent 则返回去当前 extent 访问器,默认为 [[0, 0], [width, height]], 其中 width 为元素的 client width (opens new window)height 为元素的 client height (opens new window);对于 SVG 元素(属于 SVG 的元素)来说会使用最近的祖先 SVG 元素的 width (opens new window)height (opens new window)。在这种情况下,所属 SVG 元素必须包含 width (opens new window)height (opens new window) 而不是依赖 CSS 样式或者 viewBox 属性;SVG 不提供检索 initial viewport size(初始视口大小) (opens new window) 的编程方法。可以考虑使用 element.getBoundingClientRect (opens new window) (在火狐中,SVGelement.clientWidth (opens new window)element.clientHeight (opens new window) 为零)

视口的范围影响以下几个函数: 由 zoom.scaleByzoom.scaleTo 引起的改变则视口中心会保持不变;视口中心和维度会影响 d3.interpolateZoom (opens new window) 的路径;为了执行 translate extent 视口范围是需要的。

# zoom.scaleExtent([extent]) <源码> (opens new window)

如果指定了 extent 则将缩放范围设置为指定的 [k0, k1],其中 k0 为允许缩放的最小因子而 k1 为缩放的最大缩放因子,并返回缩放行为。如果没有指定 extent 则返回当前缩放范围,默认为 [0, ∞]。缩放范围约束了缩小和放大。在手动交互阶段或者使用 zoom.scaleBy, zoom.scaleTozoom.translateBy 的时候执行;然而如果使用 zoom.transform 来明确的指定变换则不会被执行。

如果用户在已经达到缩放边界的情况下继续缩放,则滚轮事件会被忽略并不会初始化缩放手势。这样就允许用户在缩放后滚动到可缩放的区域,或者在缩小后向上滚动。如果你希望无论缩放尺度大小都始终避免上下滚动,则在可以在选择集上注册一个事件监听器来阻止浏览器的默认行为:

selection
    .call(zoom)
    .on("wheel", function() { d3.event.preventDefault(); });

# zoom.translateExtent([extent]) <源码> (opens new window)

如果指定了 extent 则将当前的平移区间设置为指定的数组: [[x0, y0], [x1, y1]], 其中 [x0, y0] 为世界的左上角而 [x1, y1] 为世界的右下角,并返回缩放行为。如果没有指定 extent 则返回当前的平移范围,默认为 [[-∞, -∞], [+∞, +∞]]。平移范围限制了视口的移动以及因为缩小引起的平移。它在交互以及使用 zoom.scaleBy, zoom.scaleTozoom.translateBy 的时候执行;然而如果使用 zoom.transform 来明确的指定变换则不会被执行。

# zoom.clickDistance([distance]) <源码> (opens new window)

如果指定了 distance 则将click 事件的触发条件: mousedownmouseup 之间鼠标移动的距离设置为指定的距离。如果鼠标按下时的坐标与鼠标抬起时的坐标之间的距离大于或等于 distance 则不会触发随后的 click 事件。如果没有指定 distance 则返回当前的默认值,默认为 0。距离阈值通过坐标系统 (event.clientX (opens new window)event.clientY (opens new window)) 测量得到。

# zoom.duration([duration]) <源码> (opens new window)

如果指定了 duration 则将双击放大的过渡事件设置为指定的数值(毫秒)并返回缩放行为,如果没有指定 duration 则返回当前的过渡时长,默认为 250ms。如果过渡时长小于等于 0 则在双击放大时不会有平滑过渡效果。

如果要禁用双击放大则可以在应用缩放行为之后移除选择集的缩放行为的双击事件:

selection
    .call(zoom)
    .on("dblclick.zoom", null);

# zoom.interpolate([interpolate]) <源码> (opens new window)

如果指定了 interpolate 则设置缩放过渡插值工厂为指定的函数。如果没有指定 interpolate 则返回当前的插值器工厂,默认为实现平滑缩放过渡的 d3.interpolateZoom (opens new window)。如果在两个视图之间直接插值,则可以尝试使用 d3.interpolate (opens new window) 代替。

# zoom.on(typenames[, listener]) <源码> (opens new window)

如果指定了 listener 则将 listener 设置为指定 typenames 的监听器并返回缩放新闻。如果已经注册了同类型以及同名的事件监听器则会被覆盖。如果 listenernull 则表示移除指定 typenames 的事件监听器(如果存在的话)。如果没有指定 listener 则返回当前第一个已经被分配的并且匹配 typenames 的事件监听器。当指定的事件被触发的时候,对应的 listener 会被执行,参数与上下文与 selection.on (opens new window) 一致: 当前绑定的数据 d 以及索引 ithis 指向当前 DOM 元素。

typenames 是一个包含一个或多个由空格隔开的 typename 的字符串。每个 typename 都是一个 type,后面可以跟一个由 . 隔开的 name, 比如 zoom.foozoom.bar;通过 name 可以为同一种 type 同时指定多个事件监听器,type 为以下几种:

  • start - zooming 开始之后(比如mousedown)
  • zoom - zoom 的变换发生了变化(比如 mousemove)
  • end - zoom 结束(比如 mouseup)

参考 dispatch.on (opens new window) 获取更多信息.

# Zoom Events

zoom event listener 被调用时,d3.event (opens new window) 被设置为当前缩放事件。event 对象暴露以下几个字段:

  • event.target - 关联的 zoom behavior.
  • event.type - 字符串 “start”, “zoom” 或 “end”; 参考 zoom.on.
  • event.transform - 当前 zoom transform.
  • event.sourceEvent - 底层输入事件,比如 mousemovetouchmove.

# Zoom Transforms

缩放行为载缩放被 applied 的时候将缩放状态存储在元素上,而不是存储在缩放行为自身。这是因为缩放行为可能同时应用在多个元素上,并且每个元素的缩放都是独立的。缩放状态可以通过交互或 通过 zoom.transform 编程的形式改变。

为了检索缩放的状态,请在当前 zoom event 事件监听器内使用 event.transform 来获取(参考 zoom.on),或者使用为给定节点使用 d3.zoomTransform。后者在通过编程的形式缩放状态时很有用,比如要实现缩小和放大的按钮。

# d3.zoomTransform(node) <源码> (opens new window)

返回指定 node 上当前缩放变换。node 通常是 DOM 元素而不是 selection。(选择集中可能包含多个状态不同的元素,而这个方法会返回一个单一的变换)。如果你有一个选择集则可以先调用 selection.node (opens new window):

var transform = d3.zoomTransform(selection.node());

event listener (opens new window) 的上下文中,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.scaletransform.translate 去获取一个新的转换。参考 zoom.scaleBy, zoom.scaleTozoom.translateBy 这些更方便的方法。根据给定的 k, tx, 和 ty 创建一个变换:

var t = d3.zoomIdentity.translate(x, y).scale(k);

将变换应用到 Canvas 2D context (opens new window),则使用在 context.translate (opens new window) 之后使用 context.scale (opens new window):

context.translate(transform.x, transform.y);
context.scale(transform.k, transform.k);

类似的将变换通过 CSS (opens new window) 应用到 HTML 元素:

div.style("transform", "translate(" + transform.x + "px," + transform.y + "px) scale(" + transform.k + ")");
div.style("transform-origin", "0 0");

将变换应用到 SVG (opens new window):

g.attr("transform", "translate(" + transform.x + "," + transform.y + ") scale(" + transform.k + ")");

更方便的是,使用 transform.toString 的优势:

g.attr("transform", transform);

要注意变换的顺序,平移一定要在缩放前。

# transform.scale(k) <源码> (opens new window)

返回一个 k₁ 等于 k₀k 的变换,其中 k₀ 是当前变换的缩放因子(也就是将当前变换缩小或放大一个倍数)。

# transform.translate(x, y) <源码> (opens new window)

返回一个平移量 tx1ty1 分别等于 tx0 + xty0 + y 的变换,其中 tx0ty0 当前变换的平移量。

# transform.apply(point) <源码> (opens new window)

返回指定的点经过当前变换变换后的坐标,其中 point 由二元数字数组 [x, y] 表示。返回的坐标等价于 [xk + tx, yk + ty].

# transform.applyX(x) <源码> (opens new window)

返回指 x 坐标经过变换后的 x 坐标,等于 xk + tx

# transform.applyY(y) <源码> (opens new window)

返回指 y 坐标经过变换后的 y 坐标,等于 yk + ty

# transform.invert(point) <源码> (opens new window)

返回指定 point 的逆变换,point 由二元数组 [x, y] 表示。返回的点 point 等价于 [(x - tx) / k, (y - ty) / k]。

# transform.invertX(x) <源码> (opens new window)

返回指定 x 坐标的逆变换,等于 (x - tx) / k

# transform.invertY(y) <源码> (opens new window)

返回指定 y 坐标的逆变换,等于 (y - ty) / k

# transform.rescaleX(x) <源码> (opens new window)

返回指定 continuous scale (opens new window) x,且 domain (opens new window) 结果变换的拷贝。这是通过首先在刻度范围内应用 inverse x-transform ,然后使用 inverse scale (opens new window) 来计算响应的值域实现的:

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 (opens new window);而不能使用 continuous.rangeRound (opens new window),因为会降低 continuous.invert (opens new window) 的准确性并且可能导致缩放之后 domain 的错乱。这个方法不会修改输入比例尺 x;因此 x 表示未变换的比例尺,而返回的比例尺则表示经过缩放变换之后的比例尺。

# transform.rescaleY(y) <源码> (opens new window)

返回指定 continuous scale (opens new window) y,且 domain (opens new window) 结果变换的拷贝。这是通过首先在刻度范围内应用 inverse y-transform ,然后使用 inverse scale (opens new window) 来计算响应的值域实现的:

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 (opens new window);而不能使用 continuous.rangeRound (opens new window),因为会降低 continuous.invert (opens new window) 的准确性并且可能导致缩放之后 domain 的错乱。这个方法不会修改输入比例尺 y;因此 y 表示未变换的比例尺,而返回的比例尺则表示经过缩放变换之后的比例尺。

# transform.toString() <源码> (opens new window)

返回一个能直接被 SVG transform (opens new window) 识别的表示变换的字符串,通过一下方式实现:

function toString() {
  return "translate(" + this.x + "," + this.y + ") scale(" + this.k + ")";
}

# d3.zoomIdentity <源码> (opens new window)

恒等变换,其中 k = 1, tx = ty = 0.

最后更新: 2019/5/18 下午4:11:02