D3.js d3-brush brushing 选区 API 详解:从交互手势到程序化控制的完整指南
本文基于 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() |
创建二维刷选器,同时约束 x 和 y 两个方向 | 散点图选区、矩形区域筛选 |
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、索引i,this指向当前 DOM 元素,返回值即为该元素的选区。
brush.clear(group, event)
*brush*.clear 就是 selection 取 null 的 *brush*.move 的别名,用于清空选区。典型用法是绑定双击事件一键清空:
group
.on("dblclick", () => group.call(brush.clear));
五、可配置项详解:extent、filter、touchable、keyModifiers、handleSize
brush.extent(extent)
若指定 extent,把可刷选范围设为给定的点数组 [[x0, y0], [x1, y1]](左上角与右下角),并返回该 brush;也接受返回该数组的函数,按元素逐一带 d、i 调用,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 已有监听器,先移除旧监听器再添加新的;
- listener 为
null:移除 typenames 对应的当前监听器(若有); - 不指定 listener:返回当前匹配 typenames 的第一个已注册监听器。
typenames 是一个或多个以空白分隔的 typename 字符串,每个 typename 为 type 加上可选的点号(.)与 name(如 brush.foo、brush.bar),name 允许同一 type 上注册多个互不覆盖的监听器。合法的 type 只有三种:
| 类型 | 触发时机 |
|---|---|
start |
brush 手势开始,如 mousedown |
brush |
brush 移动过程中,如 mousemove |
end |
brush 手势结束,如 mouseup |
事件触发时,每个 listener 以与 *selection*.on 监听器相同的上下文和参数被调用:当前事件 event 与 datum d,this 为当前 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.js 中
export * from "d3-brush";将 brush 的全部导出并入 D3 总入口,因此d3.brush、d3.brushX、d3.brushY、d3.brushSelection在d3命名空间下直接可用; - package.json 声明依赖
"d3-brush": "^3.0.0",对应 d3-brush 3.x 系列; - docs/api.md 将 d3-brush 的全部 12 个 API 条目(三个工厂函数、应用、
move、clear、extent、filter、touchable、keyModifiers、handleSize、on、brushSelection)编入官方 API 索引,与本文覆盖的方法一一对应; - brush 的完整实现位于上游 d3-brush 包的
src/brush.js(文档中每个 API 条目的 Source 均指向该文件),本仓库不内嵌其源码,node_modules未安装,故本文的机制说明以 docs/d3-brush.md 的文档契约为准。
九、使用要点与常见限制汇总
- 选区是屏幕坐标:
move、clear、brushSelection、event.selection均工作在屏幕(像素)坐标系;换算到数据域请用*scale*.invert,这也是与 D3 v3 时代 brush 依赖 scale 的本质区别(CHANGES.md)。 - extent 的默认实现有前提:依赖宿主 SVG 的
viewBox或width/height属性;跨页面嵌入、动态尺寸场景建议显式调用*brush*.extent,并注意 Firefox 中 SVG 元素clientWidth/clientHeight恒为 0 的坑。 - handleSize 有时机限制:必须在 brush 应用之前设置,且默认值为 6;对已渲染的 brush 无效。
- 状态挂在元素上:选区存储在元素的
__brush属性中,务必通过d3.brushSelection或event.selection读取,不要直接操作内部属性。 - 键盘修饰键默认开启:META 新建选区、ALT 围绕中心重定位、SPACE 锁定尺寸只允许平移;需要与其他组件(如 d3-zoom 的按键行为)共存时,可用
*brush*.keyModifiers(false)关闭 brush 的按键监听。 - 触摸检测并非万全:默认
touchable检测器对 Chrome 移动模拟器会失败,此类环境需注入自定义检测函数。 - 事件类型只有三种:
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。
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 StartedRust0624
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