d3 Selection 元素选择完全指南:从 d3.select / selectAll 到子选择、过滤与底层 selector 工具
本文以 d3 官方文档 docs/d3-selection/selecting.md 为主体,系统讲解 d3 v7 中 d3-selection 的元素选择(Selecting elements) API:从顶层的 d3.select / d3.selectAll,到子选择方法 select / selectAll / selectChild / selectChildren / filter,再到 d3.selector、d3.matcher 等底层选择器工厂。读完本文,你将能够正确地在文档中定位 DOM 元素、理解"选择(selection)与分组(grouping)"的索引语义、掌握数据在父元素与子元素之间的传播规则,并能结合 src/index.js 与 test/d3-test.js 验证 d3 伞包对 d3-selection 全部 API 的导出关系。
一、什么是 selection(选择)?
在 d3 中,selection(选择)是 DOM 中一组元素(a set of elements from the DOM)。这些元素通常通过 CSS 选择器(selectors)来识别,例如:
.fancy—— 选出所有具有 class 名 fancy 的元素;div—— 选出所有 DIV 元素。
选择方法分为两种形式:select 与 selectAll。前者只选出第一个匹配的元素,后者按文档顺序(document order)选出所有匹配的元素。按作用范围,又分为两层:
| 方法类别 | 方法 | 作用范围 |
|---|---|---|
| 顶层选择 | d3.select、d3.selectAll |
查询整个文档 |
| 子选择(subselection) | selection.select、selection.selectAll |
限制在当前已选元素的**后代(descendants)**中查找 |
| 直接子元素子选择 | selection.selectChild、selection.selectChildren |
只查找当前元素的直接子节点(direct children) |
在 src/index.js 中,d3 伞包通过 export * from "d3-selection";(第 24 行)将 d3-selection 的全部导出合并进 d3 命名空间;package.json 中声明其版本依赖为 "d3-selection": "^3.0.0"。test/d3-test.js 会遍历 package.json 中的每个依赖模块并断言 d3 导出了该模块的每一个属性(version 除外),因此上述所有选择 API 都是 d3 v7 公共 API 的一部分。
缩进约定:用排版暴露"上下文切换"
d3 文档约定了一条非常有辨识度的代码风格:返回当前 selection 的方法(如 selection.attr)缩进 4 个空格,而返回新 selection 的方法只缩进 2 个空格。这样可以让人眼一眼看出链式调用中"当前上下文"在哪里发生了切换:
d3.select("body")
.append("svg") // 新的 selection:svg 元素
.attr("width", 960) // 仍作用在 svg 上
.attr("height", 500)
.append("g") // 新的 selection:g 元素
.attr("transform", "translate(20,20)")
.append("rect") // 新的 selection:rect 元素
.attr("width", 920)
.attr("height", 460);
每一处 2 空格"缩进回退",都意味着上下文切换到了刚刚 append 出来的新元素上;随后的 4 空格方法则继续作用在这个新元素上。写 d3 图表代码时遵循这一约定,链式调用的可读性会显著提升。
二、d3.selection():根选择与类型判断
const root = d3.selection();
d3.selection() 选出根元素 document.documentElement(即 <html>)。它还有两个常被忽略的用途:
- 类型判断:可以用
instanceof d3.selection测试一个值是否为 selection; - 扩展 selection 原型:向 selection 添加自定义方法。例如添加一个用来勾选复选框的
checked方法:
d3.selection.prototype.checked = function(value) {
return arguments.length < 1
? this.property("checked") // 无参数:读取
: this.property("checked", !!value); // 有参数:写入
};
之后即可像内置方法一样使用(property 的完整说明见 docs/d3-selection/modifying.md):
d3.selectAll("input[type=checkbox]").checked(true);
这个模式展示了 d3 selection 作为"可扩充分组对象"的设计:任何基于 this(当前 DOM 元素)的通用操作,都可以挂到原型上被全项目复用。
三、顶层选择:d3.select 与 d3.selectAll
d3.select(selector)
d3.select(selector) 选出第一个匹配指定 selector 字符串的元素:
const svg = d3.select("#chart");
关键行为:
- 若没有元素匹配,返回空选择(empty selection);
- 若多个元素匹配,只选文档顺序中的第一个。例如选出第一个锚点:
const anchor = d3.select("a"); - 若 selector 不是字符串,则直接选择该节点。这在已经持有节点引用时非常有用,例如
document.body:
d3.select(document.body).style("background", "red");
或者把被点击的段落变成红色(event.currentTarget 是事件触发时的 DOM 节点):
d3.selectAll("p").on("click", (event) => d3.select(event.currentTarget).style("color", "red"));
d3.select(document.body) 与 d3.select("body") 效果相同,但前者省去了字符串解析;在事件回调中用 d3.select(event.currentTarget) 拿到"被点击的那个元素"是最常见的交互入口。
d3.selectAll(selector)
d3.selectAll(selector) 选出所有匹配的元素,按文档顺序(自上而下)排列:
const p = d3.selectAll("p");
关键行为:
- 若没有匹配元素,或 selector 为
null/undefined,返回空选择; - 若 selector 不是字符串,则选择给定的节点数组。这在已经持有节点集合时很有用,例如事件监听器里的
this.childNodes或全局集合document.links。参数也可以是可迭代对象(iterable)或伪数组(如 NodeList):
d3.selectAll(document.links).style("color", "red");
这一点对迁移旧代码很重要:d3 4 起(见 CHANGES.md 中 d3-selection 相关条目),d3.selectAll 与 selection.selectAll 接受可迭代对象,数组型对象(array-likes,如 live NodeList)会被转换为数组。因此在回调里可以直接把 event.target.children、Array.from(node.childNodes) 等交给 selectAll,而不必先手工 Array 化。
四、子选择:selection.select 与 selection.selectAll
子选择方法作用于"已选元素的集合":对当前 selection 中的每个元素,在其内部再执行一次查找。理解子选择的关键,是理解分组(grouping)。
selection.select(selector)
对当前 selection 的每个元素,选出第一个匹配的后代元素:
const b = d3.selectAll("p").select("b"); // 每个 <p> 里的第一个 <b>
语义细节:
- 索引保持一一对应:若某个当前元素内部找不到匹配项,返回的 selection 中对应索引的元素为
null(如果 selector 为null,则返回选择的所有元素都是null,即空选择)。 - 数据会向下传播:若当前元素关联了数据(通过
data绑定),该数据会被传播到选中的对应子元素上。这一点值得警惕——文档明确提示"selection.select propagates the parent's data to the selected child"。 - 多个匹配时,只取文档顺序中第一个。
selector 可以是函数:它会按序为每个选中元素求值,参数为当前数据 (d)、当前索引 (i)、当前组 (nodes),this 绑定为当前 DOM 元素 (nodes[i])。函数必须返回一个元素;没有匹配时返回 null。例如选出每个段落的前一个兄弟:
const previous = d3.selectAll("p").select(function() {
return this.previousElementSibling;
});
注意这里不能写成箭头函数,因为需要 this 指向当前元素。
与 selectAll 的核心区别:selection.select 不影响分组——它保留现有的分组结构和索引,并把(若有)数据传播到选中的子元素。分组在 data join 中扮演关键角色,因此在数据驱动更新中,select 是"父子同数据"场景下的标准下钻方式。
selection.selectAll(selector)
对当前 selection 的每个元素,选出所有匹配的后代元素:
const b = d3.selectAll("p").selectAll("b"); // 每个 <p> 里的所有 <b>
语义细节:
- 返回的 selection 中的元素按父节点分组:来自同一个父元素的匹配结果构成同一个子组。若某个父元素内部没有匹配项(或 selector 为
null),则该索引处的组为空数组。 - 不会继承父选择的数据:与
selection.select不同,selectAll选出的元素不带父级数据;需要把数据传给子元素时,应显式调用selection.data(见 docs/d3-selection/joining.md 的 data join)。这正是 d3 "enter / update / exit" 模式的基础:父级数据留在父组上,子组重新走一遍 join。
selector 可以是函数:同样按序为每个元素求值,参数为 (d, i, nodes),this 为当前 DOM 元素。函数必须返回一个元素数组(或可迭代对象、伪数组如 NodeList);没有匹配时返回空数组。例如选出每个段落的前、后兄弟:
const sibling = d3.selectAll("p").selectAll(function() {
return [
this.previousElementSibling,
this.nextElementSibling
];
});
把 select 与 selectAll 的差异浓缩成一句话:select 是"每行对一列,索引不变,数据跟随";selectAll 是"每行展开成多行,重新分组,数据不跟随,需要重新 join"。
五、只选直接子节点:selectChild 与 selectChildren
select / selectAll 查找的是任意深度的后代;如果只想约束到直接子节点,使用 v4 起新增的 selectChild / selectChildren(CHANGES.md 在 d3-selection 小节中记录了这两者的加入):
selection.selectChild(selector)
返回一个新 selection,包含当前 selection 每个元素中第一个匹配 selector 的子元素:
d3.selectAll("p").selectChild("b") // 每个 <p> 的第一个 <b> 子节点
- 不指定 selector 时,选第一个子节点(若存在);
- selector 为字符串时,选第一个匹配的子节点(若存在);
- selector 为函数时,按序对每个子节点求值,参数为子节点 (child)、子节点索引 (i)、子节点列表 (children);方法选出**第一个使函数返回真值(truthy)**的子节点。
与 selection.select 相同,selectChild 会把父元素的数据传播到选中的子节点。
selection.selectChildren(selector)
返回一个新 selection,包含当前 selection 每个元素中所有匹配 selector 的子元素:
- 不指定 selector:选所有子节点;
- selector 为字符串:选匹配的子节点(若有);
- selector 为函数:按序对每个子节点求值(参数同样是 (child, i, children)),选出所有使函数返回真值的子节点。
六、selection.filter(filter):过滤现有选择
selection.filter 对当前 selection 过滤,返回一个新 selection,只包含 filter 为真的元素。例如把表格行过滤为"偶数行"(按选择索引):
const even = d3.selectAll("tr").filter(":nth-child(even)");
这与直接使用 d3.selectAll 大致等价,但索引可能不同:
const even = d3.selectAll("tr:nth-child(even)");
filter 参数支持两种形式:
- selector 字符串:如
":nth-child(even)"; - 函数:按序为每个选中元素求值,参数为 (d, i, nodes),
this为当前 DOM 元素,返回真值即保留:
const even = d3.selectAll("tr").filter((d, i) => i & 1);
也可以用 selection.select 实现同样的过滤(需要 this,所以用普通函数而非箭头函数):
const even = d3.selectAll("tr").select(function(d, i) { return i & 1 ? this : null; });
两个容易踩坑的细节,文档均有说明:
:nth-child是一基(one-based)索引,而 filter 函数拿到的是零基的选择索引,两者不能直接等同;- 上述 filter 函数依赖的是选择索引 i,而不是"该元素在 DOM 中前面有几个兄弟"——也就是说
i & 1过滤的是选择内的第 2、4、6…个元素,与 CSS 的:nth-child(2n)语义不同; - 过滤后的选择保留父组结构,但不保留索引(类似
Array.prototype.filter,元素移除会导致后续索引前移)。若需要保留索引,改用selection.select(如上例,用null占位)。
七、辅助 API:selection.selection() 与底层 selector 工具
selection.selection()
selection.selection() 返回当前 selection 本身,目的是与 transition.selection(见 docs/d3-transition/selecting.md)保持 API 对称——在 selection 与 transition 之间来回切换上下文时,调用形态可以统一。
d3.matcher(selector)
给定 selector,返回一个函数:当 this 元素与该 selector 匹配时返回 true。selection.filter 内部就使用它——selection.filter("div") 等价于 selection.filter(d3.matcher("div"))。该实现对 element.matches 做了供应商前缀(vendor-prefixed)回退支持。
d3.selector(selector)
给定 selector,返回一个函数:返回 this 元素中第一个匹配的后代元素。selection.select 内部就使用它——selection.select("div") 等价于 selection.select(d3.selector("div"))。
d3.selectorAll(selector)
给定 selector,返回一个函数:返回 this 元素中所有匹配的后代元素。selection.selectAll 内部就使用它——selection.selectAll("div") 等价于 selection.selectAll(d3.selectorAll("div"))。
这三个工厂函数把"选择器字符串 → 按元素求值的函数"这一过程显式暴露出来:如果你要写自定义的迭代器(如配合 selection.each,见 docs/d3-selection/control-flow.md)或手写遍历逻辑,可以直接复用它们,保证行为与内置子选择完全一致。
d3.window(node) 与 d3.style(node, name)
d3.window(node):返回指定节点的宿主窗口(owner window)。若 node 是元素,返回其宿主文档的 default view;若是 document,返回其 default view;否则原样返回 node。这是事件坐标、getBoundingClientRect等场景下定位正确 window 的工具函数;d3.style(node, name):返回指定节点上指定名称的样式属性值——若有同名内联样式则返回内联值,否则返回计算后的属性值(computed property value)。它与selection.style(见 docs/d3-selection/modifying.md)配套:一个操作单个节点,一个操作整个 selection。
八、如何选择方法:决策小结
| 场景 | 推荐方法 | 关键点 |
|---|---|---|
| 在整个文档找一个元素 | d3.select("#id") |
无匹配返回空选择;也接受节点对象 |
| 在整个文档找所有元素 | d3.selectAll("li") |
文档顺序;接受数组/可迭代/伪数组 |
| 每个父元素取第一个匹配后代 | selection.select(...) |
索引不变,父数据传播到子 |
| 每个父元素取所有匹配后代 | selection.selectAll(...) |
按父重新分组,数据不传播,需重新 join |
| 只找直接子节点 | selectChild / selectChildren |
函数形式参数为 (child, i, children) |
| 过滤当前选择 | selection.filter(...) |
不保索引;保索引请改用 select 返回 null 占位 |
九、与 data join 的关系及仓库中的验证方式
本文涉及的"分组""数据传播"语义最终服务于 data join:selection.selectAll 按父分组后,join 会对每个父组独立计算 enter / update / exit;而 selection.select 的索引保持性则让"父-子数据对齐"成为可能。理解这一点后,嵌套图表(如多层树状图、分面图)的更新逻辑就都建立在这两个子选择的语义差异之上。
在仓库层面可以这样验证本篇内容的边界:
- src/index.js 第 24 行
export * from "d3-selection";确认了 d3 伞包完整转发 d3-selection 的全部 API; - test/d3-test.js 逐属性断言 d3 导出了 package.json 中每个依赖(含
d3-selection^3.0.0)的全部导出,保证d3.select、d3.selectAll、d3.selector、d3.selectorAll、d3.matcher等都可从d3直接调用; - d3-selection 各选择 API 的单元级测试位于 d3-selection 子仓库(本仓库以其依赖包形式引入,package.json 声明
^3.0.0),本文所有语义描述均以 docs/d3-selection/selecting.md 官方文档为准; - test/docs-test.js 会爬取
docs/下所有 Markdown 的内部链接并校验锚点有效,说明本文引用的 docs/d3-selection/modifying.md、docs/d3-selection/joining.md、docs/d3-transition/selecting.md 等文档入口与主文档是同一套受链接完整性测试保护的资料体系。
适用前提:本文基于 d3 v7(package.json 版本 7.9.0)的 d3-selection 3.x API;selectChild / selectChildren 自 v4 引入,事件参数风格(只传 event,不再传 index 和 group)为 v4+ 的行为,详见 CHANGES.md 中 d3-selection 的相关记录。
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