首页
/ D3.js d3-brush brushing 选区 API 详解:从交互手势到程序化控制的完整指南

D3.js d3-brush brushing 选区 API 详解:从交互手势到程序化控制的完整指南

2026-09-06 11:10:31作者:傅爽业Veleda

本文基于 D3(v7)官方文档 d3-brush 章节 编写,系统讲解 d3-brush 模块的三种刷选器创建方式、SVG 元素结构、选区读写 API(move/clear/brushSelection)、全部可配置项(extent/filter/touchable/keyModifiers/handleSize)以及 start/brush/end 事件体系。读完后,你可以为散点图、直方图实现可拖拽、可缩放的交互式选区,并掌握“松手后吸附到刻度”“点击外部重定位”“双击清除”等程序化控制模式。

一、什么是 Brushing,d3-brush 解决了什么问题

Brushing 是通过指点手势(如鼠标点击并拖拽)交互式地指定一维或二维选区的技术。它常用于选择离散元素(如散点图中的点、桌面上的文件),也常用于放大感兴趣的区域、选择连续区间做交叉筛选(cross-filtering)或实时直方图联动。

d3-brush 模块基于 SVG 为鼠标和触摸事件实现了 brushing 行为。其核心交互约定如下:

  • 点击并拖拽选区本身:整体平移选区(translate);
  • 点击并拖拽选区边缘/角部的 handle:移动选区对应的边(或角点),改变选区大小;
  • 点击并拖拽透明的 overlay 区域:从零开始定义一个新的选区;按住 META(⌘)键点击并拖拽同样可以新建选区;
  • 按住 ALT(⌥)键移动选区:选区以中心为基准重新定位(center 模式);
  • 按住 SPACE 空格键移动选区:锁定当前选区尺寸,只允许平移。

除了手势交互,d3-brush 同样支持程序化控制。例如:监听 end 事件后用 *brush*.move 发起 transition,让选区吸附到语义化边界(如坐标轴刻度);或者在用户点击当前选区之外时让选区平滑地重新居中。

二、创建刷选器:brush()、brushX()、brushY()

d3-brush 提供三个工厂函数,分别创建二维、x 维、y 维三种刷选器:

工厂函数 行为 适用场景
d3.brush() 创建二维刷选器,同时约束 xy 两个方向 散点图选区、矩形区域筛选
d3.brushX() 创建沿 x 维度的一维刷选器 时间轴区间选择(focus-context 类联动)
d3.brushY() 创建沿 y 维度的一维刷选器 水平条形图的范围选择

在 D3 v4 及之后(见 CHANGES.md 的历史记录),刷选器不再依赖 scale:每个 brush 的选区直接以屏幕坐标定义。如果你需要对应的数据域,用 *scale*.invert 将选区边界反变换回数据空间即可,这也是“选区吸附到刻度”模式的实现基础。

将刷选器应用到选择集

创建出的 brush 是一个行为对象(behavior),通过 *brush*(*group*) 应用到某个 group 上——该 group 必须是 SVG <g> 元素的 selection。该函数通常不直接调用,而是经由 *selection*.call 应用。渲染一个二维刷选器的标准写法:

svg.append("g")
    .attr("class", "brush")
    .call(d3.brush().on("brush", brushed));

内部机制上,brush 使用 *selection*.on 绑定拖拽所需的事件监听器,这些监听器的名称都是 .brush,因此你随后可以用同一条规则解绑全部 brush 监听器:

group.on(".brush", null);

刷选器生成的 SVG 结构

应用 brush 时,它还会自动创建用于显示选区和接收输入事件的 SVG 元素。你可以增删或修改这些元素来改变外观,也可以直接通过样式表定制。二维刷选器生成的 DOM 结构如下(引自 docs/d3-brush.md):

<g class="brush" fill="none" pointer-events="all" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">
  <rect class="overlay" pointer-events="all" cursor="crosshair" x="0" y="0" width="960" height="500"></rect>
  <rect class="selection" cursor="move" fill="#777" fill-opacity="0.3" stroke="#fff" shape-rendering="crispEdges" x="112" y="194" width="182" height="83"></rect>
  <rect class="handle handle--n" cursor="ns-resize" x="107" y="189" width="192" height="10"></rect>
  <rect class="handle handle--e" cursor="ew-resize" x="289" y="189" width="10" height="93"></rect>
  <rect class="handle handle--s" cursor="ns-resize" x="107" y="272" width="192" height="10"></rect>
  <rect class="handle handle--w" cursor="ew-resize" x="107" y="189" width="10" height="93"></rect>
  <rect class="handle handle--nw" cursor="nwse-resize" x="107" y="189" width="10" height="10"></rect>
  <rect class="handle handle--ne" cursor="nesw-resize" x="289" y="189" width="10" height="10"></rect>
  <rect class="handle handle--se" cursor="nwse-resize" x="289" y="272" width="10" height="10"></rect>
  <rect class="handle handle--sw" cursor="nesw-resize" x="107" y="272" width="10" height="10"></rect>
</g>

三个关键部分的职责:

  • rect.overlay:覆盖 *brush*.extent 定义的可刷选区域,光标为 crosshair,在它上面拖拽即可新建选区;
  • rect.selection:覆盖当前 brush selection 的区域,cursor="move" 表示可整体拖动;
  • rect.handle(8 个):覆盖选区的四条边(n/e/s/w)与四个角(nw/ne/se/sw),各自的 cursor 值与对应的缩放方向一一对应,让用户可以交互式地修改选区的对应边界。

注意 handle 的尺寸由 *brush*.handleSize 控制(上例中边条宽 10 像素、角块 10×10 像素)。这些默认外观在 D3 4 起由属性直接给出,无需再像早期版本那样手写 .brush .extent 等样式(见 CHANGES.md 的说明)。

三、读选区:brushSelection(node)

d3.brushSelection(*node*) 返回指定 DOM 节点 node 上当前 brush 的选区。底层实现上,每个元素的 brush 状态被存储在该元素的 element.__brush 属性上,但文档明确要求通过该方法读取而不是直接访问内部属性。若给定节点没有选区,返回 null;否则返回值是一个数字数组:

  • 二维 brush:[[x0, y0], [x1, y1]],其中 x0/y0 为最小值、x1/y1 为最大值;
  • x-brush:[x0, x1]
  • y-brush:[y0, y1]

与 D3 v4 的历史说明一致(CHANGES.md):brush 自身不再内部保存位置,选区位置存储在被应用 brush 的那些元素上,事件回调里也可以通过 event.selection 拿到同一份数据。

四、写选区:brush.move 与 brush.clear

brush.move(group, selection, event)

*brush*.move 把 brush 的当前选区设置为指定值。参数要点:

  • group 必须是 SVG <g> 元素选择集的 selection transition——传入 transition 时,选区会以补间动画方式移动到目标位置,这是“吸附/重定位”效果的关键;
  • selection 是一个数字数组,语义与 brushSelection 的返回值完全一致(二维为 [[x0, y0], [x1, y1]],一维为 [x0, x1] / [y0, y1]);
  • null 表示清除选区;
  • selection 也可以是函数:对每个选中元素调用一次,参数为当前 datum d、索引 ithis 指向当前 DOM 元素,返回值即为该元素的选区。

brush.clear(group, event)

*brush*.clear 就是 selectionnull*brush*.move 的别名,用于清空选区。典型用法是绑定双击事件一键清空:

group
    .on("dblclick", () => group.call(brush.clear));

五、可配置项详解:extent、filter、touchable、keyModifiers、handleSize

brush.extent(extent)

若指定 extent,把可刷选范围设为给定的点数组 [[x0, y0], [x1, y1]](左上角与右下角),并返回该 brush;也接受返回该数组的函数,按元素逐一带 di 调用,this 为当前 DOM 元素。不传参时返回当前的 extent 访问器,其默认实现为:

function defaultExtent() {
  var svg = this.ownerSVGElement || this;
  if (svg.hasAttribute("viewBox")) {
    svg = svg.viewBox.baseVal;
    return [[svg.x, svg.y], [svg.x + svg.width, svg.y + svg.height]];
  }
  return [[0, 0], [svg.width.baseVal.value, svg.height.baseVal.value]];
}

默认实现要求宿主 SVG 元素定义了 viewBox 属性,或显式的 width/height 属性;文档同时提示:在 Firefox 中 SVG 元素的 clientWidth/clientHeight 为 0,因此更稳妥的做法是显式设置 extent,或使用 *element*.getBoundingClientRect

extent 的两重作用:它决定透明 overlay 的大小,并约束选区——选区不允许移出 extent 之外。

brush.filter(filter)

若指定 filter,设置事件过滤函数并返回该 brush;不传参时返回当前 filter,默认为:

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

filter 返回假值(falsey)时,触发的输入事件被忽略,不会启动 brush 手势,因此 filter 决定了哪些输入事件会被忽略。默认实现忽略非主按钮的 mousedown(例如右键——通常用于唤起上下文菜单),这与 CHANGES.md 中“brush 默认忽略面向上下文菜单的右键点击”的说明相互印证。

brush.touchable(touchable)

设置触摸支持检测函数。不传参时返回当前检测器,默认为:

function touchable() {
  return navigator.maxTouchPoints || ("ontouchstart" in this);
}

只有当 brush 被应用到某个元素时,检测器对该元素返回真值,才会注册触摸事件监听器。默认检测器在大多数支持触摸的浏览器上工作良好,但并非全部——例如 Chrome 的移动设备模拟器就会检测失败,遇到此类环境时可自行注入检测函数。

brush.keyModifiers(modifiers)

设置 brush 是否在刷选过程中监听按键事件,不传参时返回当前行为,默认 true。关闭后,META/ALT/SPACE 等修饰键行为将不再生效。

brush.handleSize(size)

设置 handle 尺寸为给定数值,不传参时返回当前值,默认为 6。两个使用限制必须记住:

  • 必须在 brush 应用之前调用;
  • 更改 handle 尺寸不会影响之前已渲染的 brush。

六、事件体系:brush.on 与 Brush events

brush.on(typenames, listener)

  • 指定 listener:为 typenames 设置事件监听器并返回该 brush;若同一 type+name 已有监听器,先移除旧监听器再添加新的;
  • listenernull:移除 typenames 对应的当前监听器(若有);
  • 不指定 listener:返回当前匹配 typenames 的第一个已注册监听器。

typenames 是一个或多个以空白分隔的 typename 字符串,每个 typenametype 加上可选的点号(.)与 name(如 brush.foobrush.bar),name 允许同一 type 上注册多个互不覆盖的监听器。合法的 type 只有三种:

类型 触发时机
start brush 手势开始,如 mousedown
brush brush 移动过程中,如 mousemove
end brush 手势结束,如 mouseup

事件触发时,每个 listener 以与 *selection*.on 监听器相同的上下文和参数被调用:当前事件 event 与 datum dthis 为当前 DOM 元素。事件分发的底层机制可参见 d3-dispatch 文档

Brush event 对象的字段

当 brush 事件监听器被调用时,它会收到当前 brush 事件对象,其中暴露以下字段:

字段 含义
target 关联的 brush 行为对象
type 字符串 "start""brush""end"
selection 当前的 brush 选区,格式同 brushSelection 返回值
sourceEvent 底层输入事件,如 mousemove 或 touchmove
mode 字符串 "drag""space""handle""center",表示当前的刷选模式

从仓库的变更历史看(CHANGES.md),d3-brush 2.0 为事件对象新增了 event.mode 字段,并让 *brush*.on 直接把 event 传给监听器,同时改进了多点(双指)触摸交互——这解释了上表五个字段的由来,也意味着你可以在 brush 事件中通过 mode 区分用户是在整体拖拽、调整 handle、锁定尺寸平移还是围绕中心重定位。

七、程序化控制实战模式

以下模式均直接由文档 API 支撑,可按需组合。

1. 松手吸附:end 事件 + transition + move

这是文档给出的旗舰场景:监听 end 事件后,用 *brush*.move 发起 transition,让选区平滑地“吸附”到语义化边界(例如坐标轴刻度)。思路是:在 end 回调里取 event.selection,通过 *scale*.invert 反变换为数据域,把数据域两端对齐到最近的刻度,再用对应的 *scale* 正向变换回屏幕坐标,最后:

group.transition()
    .call(brush.move, [[x0, y0], [x1, y1]]);

注意 end 回调中若手势由 brush.move 自身触发(程序化移动),应通过 event.sourceEvent 是否为 null 判断以避免死循环——这正是 sourceEvent 字段的用途。

2. 点击选区外部时重新居中

在 overlay 上点击(而非拖拽)时,将选区以相同尺寸平移到点击位置:读取 event.selection 得到当前宽高等尺寸信息,以点击坐标(可从 d3.pointer 获取)为新的中心构造新的选区数组,再调用 group.call(brush.move, newSelection)(配合 transition 可获得平滑动画)。

3. 双击清除选区

如前文 *brush*.clear 小节所示,只需:

group.on("dblclick", () => group.call(brush.clear));

4. 解绑与复用

若需彻底移除 brush 的交互(例如切换图表交互模式),执行 group.on(".brush", null) 解绑所有 brush 监听器即可,无需删除 brush 生成的 SVG 子元素。

八、仓库中的 d3-brush 集成方式与验证线索

从本仓库(D3 v7 发行包)的源码结构可以确认 d3-brush 的集成方式:

  • src/index.jsexport * from "d3-brush"; 将 brush 的全部导出并入 D3 总入口,因此 d3.brushd3.brushXd3.brushYd3.brushSelectiond3 命名空间下直接可用;
  • package.json 声明依赖 "d3-brush": "^3.0.0",对应 d3-brush 3.x 系列;
  • docs/api.md 将 d3-brush 的全部 12 个 API 条目(三个工厂函数、应用、moveclearextentfiltertouchablekeyModifiershandleSizeonbrushSelection)编入官方 API 索引,与本文覆盖的方法一一对应;
  • brush 的完整实现位于上游 d3-brush 包的 src/brush.js(文档中每个 API 条目的 Source 均指向该文件),本仓库不内嵌其源码,node_modules 未安装,故本文的机制说明以 docs/d3-brush.md 的文档契约为准。

九、使用要点与常见限制汇总

  1. 选区是屏幕坐标moveclearbrushSelectionevent.selection 均工作在屏幕(像素)坐标系;换算到数据域请用 *scale*.invert,这也是与 D3 v3 时代 brush 依赖 scale 的本质区别(CHANGES.md)。
  2. extent 的默认实现有前提:依赖宿主 SVG 的 viewBoxwidth/height 属性;跨页面嵌入、动态尺寸场景建议显式调用 *brush*.extent,并注意 Firefox 中 SVG 元素 clientWidth/clientHeight 恒为 0 的坑。
  3. handleSize 有时机限制:必须在 brush 应用之前设置,且默认值为 6;对已渲染的 brush 无效。
  4. 状态挂在元素上:选区存储在元素的 __brush 属性中,务必通过 d3.brushSelectionevent.selection 读取,不要直接操作内部属性。
  5. 键盘修饰键默认开启:META 新建选区、ALT 围绕中心重定位、SPACE 锁定尺寸只允许平移;需要与其他组件(如 d3-zoom 的按键行为)共存时,可用 *brush*.keyModifiers(false) 关闭 brush 的按键监听。
  6. 触摸检测并非万全:默认 touchable 检测器对 Chrome 移动模拟器会失败,此类环境需注入自定义检测函数。
  7. 事件类型只有三种start/brush/end(v4 起由旧的 brushstart/brushend 重命名而来,且事件本身携带 mode 区分拖拽、handle 缩放、锁定尺寸与中心模式)。

小结

d3-brush 用“行为对象 + SVG 子树 + 三事件模型”的组合,把 brushing 这一经典交互压缩成非常小的 API 面:brush()/brushX()/brushY() 创建,selection.call 应用,extent/filter/touchable/keyModifiers/handleSize 五个方法定制,move/clear/brushSelection 读写选区,start/brush/end 事件驱动下游联动。掌握了“屏幕坐标选区 + transition 移动 + 刻度反变换”这条主线,即可组合出吸附、重定位、清除等常见的数据筛选交互。完整文档契约见 docs/d3-brush.md,API 索引导航见 docs/api.md

登录后查看全文
热门项目推荐
相关项目推荐