首页
/ d3 Selection 元素选择完全指南:从 d3.select / selectAll 到子选择、过滤与底层 selector 工具

d3 Selection 元素选择完全指南:从 d3.select / selectAll 到子选择、过滤与底层 selector 工具

2026-09-06 12:50:56作者:尤辰城Agatha

本文以 d3 官方文档 docs/d3-selection/selecting.md 为主体,系统讲解 d3 v7 中 d3-selection 的元素选择(Selecting elements) API:从顶层的 d3.select / d3.selectAll,到子选择方法 select / selectAll / selectChild / selectChildren / filter,再到 d3.selectord3.matcher 等底层选择器工厂。读完本文,你将能够正确地在文档中定位 DOM 元素、理解"选择(selection)与分组(grouping)"的索引语义、掌握数据在父元素与子元素之间的传播规则,并能结合 src/index.jstest/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 元素。

选择方法分为两种形式:selectselectAll。前者只选出第一个匹配的元素,后者按文档顺序(document order)选出所有匹配的元素。按作用范围,又分为两层:

方法类别 方法 作用范围
顶层选择 d3.selectd3.selectAll 查询整个文档
子选择(subselection) selection.selectselection.selectAll 限制在当前已选元素的**后代(descendants)**中查找
直接子元素子选择 selection.selectChildselection.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>)。它还有两个常被忽略的用途:

  1. 类型判断:可以用 instanceof d3.selection 测试一个值是否为 selection;
  2. 扩展 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.selectAllselection.selectAll 接受可迭代对象,数组型对象(array-likes,如 live NodeList)会被转换为数组。因此在回调里可以直接把 event.target.childrenArray.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
  ];
});

selectselectAll 的差异浓缩成一句话:select 是"每行对一列,索引不变,数据跟随";selectAll 是"每行展开成多行,重新分组,数据不跟随,需要重新 join"。

五、只选直接子节点:selectChild 与 selectChildren

select / selectAll 查找的是任意深度的后代;如果只想约束到直接子节点,使用 v4 起新增的 selectChild / selectChildrenCHANGES.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 参数支持两种形式:

  1. selector 字符串:如 ":nth-child(even)"
  2. 函数:按序为每个选中元素求值,参数为 (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 匹配时返回 trueselection.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 joinselection.selectAll 按父分组后,join 会对每个父组独立计算 enter / update / exit;而 selection.select 的索引保持性则让"父-子数据对齐"成为可能。理解这一点后,嵌套图表(如多层树状图、分面图)的更新逻辑就都建立在这两个子选择的语义差异之上。

在仓库层面可以这样验证本篇内容的边界:

适用前提:本文基于 d3 v7(package.json 版本 7.9.0)的 d3-selection 3.x API;selectChild / selectChildren 自 v4 引入,事件参数风格(只传 event,不再传 index 和 group)为 v4+ 的行为,详见 CHANGES.md 中 d3-selection 的相关记录。

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