Skip to content

D3.js - 拖拽 API

D3 拖拽 API(d3-drag)提供了一种方便的方式,用于为你的 SVG 或 HTML 元素添加拖放交互。它处理鼠标和触摸事件,允许用户在界面上移动元素。

如果你使用的是完整的 D3 打包版本,那么 d3-drag 已经包含在内。对于模块化项目,你需要安装和导入它:

// With npm or yarn
npm install d3-drag
// yarn add d3-drag
// In your JavaScript
import { drag } from 'd3-drag';
// d3-selection is also usually needed
import { select } from 'd3-selection';

如果通过 CDN 使用 D3:

<script src="https://d3js.org/d3.v7.min.js"></script>

与 D3 拖拽 API 相关的主要方法:

此函数创建一个新的拖拽行为(drag behavior)。然后可以将此行为应用于 D3 选择集(selection)。

const dragBehavior = d3.drag();

将拖拽行为应用于指定的 D3 selection(选择集)。这通常使用 selection.call(dragBehavior) 来完成。

// 示例:使所有 class 为 'draggable' 的圆响应拖拽
d3.selectAll("circle.draggable")
.call(d3.drag().on("start drag end", handleDragEvent));
function handleDragEvent(event, d) {
// 'event' 是 D3 拖拽事件对象
// 'd' 是绑定到被拖拽元素的数据项(datum)
if (event.type === "start") {
// console.log("Drag started!", event.subject);
} else if (event.type === "drag") {
// d3.select(this).attr("cx", event.x).attr("cy", event.y);
} else if (event.type === "end") {
// console.log("Drag ended!");
}
}

设置 container(容器)访问器(accessor),它决定了拖拽事件(event.x、event.y)的坐标系。container 函数在每次事件发生时被调用,并传入当前事件和数据项(datum),this 上下文是被拖拽的 DOM 元素。它应该返回用于坐标系的 DOM 元素。 对于 SVG,默认为被拖拽元素的父元素或所属的 SVG 元素。对于 HTML 元素,通常是父元素。要在 Canvas 内拖拽元素,通常需要将容器设置为 canvas 元素本身。

dragBehavior.container(function() { return this.parentNode; });
// Or for canvas:
dragBehavior.container(function() { return canvasElement; });

设置事件 filter(过滤器)函数。只有当过滤器函数返回 true 时,拖拽行为才会开始。过滤器函数会接收当前的原生事件(例如 mousedown、touchstart)和数据项 d。this 是当前的 DOM 元素。 默认的过滤器是 (event) => !event.ctrlKey && !event.button,它会忽略按下 Ctrl 键或使用非鼠标主键(例如右键)发起的拖拽。

dragBehavior.filter(function(event) {
// Only allow dragging with the primary mouse button (button 0)
// 只允许使用鼠标主键(button 0)进行拖拽
return event.button === 0;
});

设置 subject(主题)访问器。主题代表被拖拽的事物。此函数在拖拽手势开始时(鼠标按下或触摸开始时)被调用。它会接收原生事件和数据项 d,this 是当前的 DOM 元素。它应该返回一个代表主题的对象。这个主题对象将在后续的拖拽事件(start、drag、end)中作为 event.subject 提供。 默认的主题是被拖拽元素的数据项 d:(event, d) => d。 如果主题对象具有 x 和 y 属性,D3 会使用它们来计算 event.dx 和 event.dy(自上次拖拽事件以来位置的变化)。

dragBehavior.subject(function(event, d) {
// If 'd' is the datum, it might already have x and y, or you can define them here
// 如果 'd' 是数据项,它可能已经有 x 和 y,或者你可以在这里定义它们
return d.dataForDragging || { x: event.x, y: event.y, originalData: d };
});

设置鼠标按下/触摸开始与鼠标抬起/触摸结束之间指针移动的最大距离(以像素为单位),超过此距离将被视为拖拽而非点击(因此不会触发 ‘drag’ 事件)。默认为 0。这有助于区分点击和拖拽。

为指定的 typenames 添加事件 listener(监听器)。typenames 是一个或多个事件类型组成的字符串,各类型之间用空格分隔(例如 “start drag end”)。 有效的事件类型有:

  • start: 拖拽手势开始时触发(在鼠标按下/触摸开始且过滤器通过后)。
  • drag: 在活动的拖拽手势中,指针移动时触发。
  • end: 拖拽手势结束时触发(在鼠标抬起/触摸结束时)。

listener 函数被调用时接收参数 (event, d),其中 event 是 D3 拖拽事件对象(见下文),d 是被拖拽元素的数据项(datum)。listener 函数内的 this 上下文是被拖拽的 DOM 元素。

一个工具函数,用于阻止指定 window 上的浏览器默认拖放和文本选择行为。如果原生拖拽交互干扰了 D3 拖拽,可以调用此函数。

如果之前通过 d3.dragDisable 禁用了默认行为,此函数可以重新启用。如果 noclick 为 true,它还会抑制可能紧随鼠标抬起事件之后的点击事件。

当拖拽事件监听器被调用时,它会接收一个 D3 event 对象,该对象具有以下有用的属性:

  • target: 分派事件的拖拽行为实例。
  • type: 拖拽事件的类型字符串:“start”、“drag” 或 “end”。
  • subject: 当前的拖拽主题(subject),由 drag.subject() 定义。
  • x: 指针相对于 container 的新 x 坐标。
  • y: 指针相对于 container 的新 y 坐标。
  • dx: 自上次拖拽事件以来 x 坐标的变化(对于 “drag” 事件)。如果 event.subject.x 可用,则基于它计算。
  • dy: 自上次拖拽事件以来 y 坐标的变化(对于 “drag” 事件)。如果 event.subject.y 可用,则基于它计算。
  • identifier: 鼠标事件时为字符串 “mouse”,触摸事件时为触摸标识符。
  • active: 活动的拖拽手势数量(对于多点触控场景有用,尽管 d3-drag 主要处理单点拖拽)。
  • sourceEvent: 底层浏览器原生事件(例如 MouseEvent、TouchEvent)。

简单可拖拽圆的示例:

<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
.draggable { cursor: grab; }
.dragging { cursor: grabbing; fill: steelblue; }
</style>
</head>
<body>
<svg width="400" height="300" style="border:1px solid #ccc;">
<circle class="draggable" cx="50" cy="50" r="20" fill="orange"></circle>
</svg>
<script>
function dragstarted(event, d) {
d3.select(this).raise().classed("dragging", true);
}
function dragged(event, d) {
// Update the circle's subject (its data object `d` if it has x,y)
// Or directly update the element's attributes
// If `d` is the subject and has x,y, D3 updates them internally for dx/dy calculation
// 更新圆的主题(如果其数据对象 `d` 包含 x,y)
// 或直接更新元素的属性
// 如果 `d` 是主题且包含 x,y,D3 会在内部更新它们以计算 dx/dy
// event.subject.x = event.x;
// event.subject.y = event.y;
d3.select(this).attr("cx", event.x).attr("cy", event.y);
}
function dragended(event, d) {
d3.select(this).classed("dragging", false);
}
const dragBehavior = d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
d3.selectAll("circle.draggable").call(dragBehavior);
</script>
</body>
</html>

在此示例中,圆的 cx 和 cy 属性直接使用 event.x 和 event.y 进行更新。对于更复杂的场景,如果你的渲染逻辑依赖于数据对象而不是在拖拽处理程序中直接操作属性,你可能需要更新 event.subject 的 x 和 y 属性。