首页
/ D3 7 中 d3.hierarchy 根节点 API 详解:层级数据的表示、遍历与操作

D3 7 中 d3.hierarchy 根节点 API 详解:层级数据的表示、遍历与操作

2026-09-04 15:42:30作者:薛曦旖Francesca

本文基于 d3 仓库的官方文档 hierarchy 展开,系统讲解 d3.hierarchy 根节点的构造方式、六个核心属性、遍历/查找/排序/求值全套方法(ancestorsdescendantsfindpathlinkssumsorteach 系列等)。读完本文,你可以将 JSON 或分组(Map)数据转成标准层级树,并正确地把 node.value 与排序结果喂给 treemap、tree、pack 等层级布局。

d3 层级树示意图:同一组 Eve 家族数据的 tidy tree 布局结果

一、hierarchy API 在 d3-hierarchy 模块中的定位

很多数据天然是层级结构的(行政区划、组织架构、文件系统、软件包),d3-hierarchy 模块为此提供了几类经典可视化:节点连线图(treecluster 树状图)、邻接图(partition 冰柱图)、包围图(treemappack 圆形嵌套),详见模块总览 d3-hierarchy

所有布局的输入都不是原始 JSON,而是一个根节点(root node)。要得到根节点有两条路:

  • 数据已经是 JSON 等层级格式:直接传给 d3.hierarchy(data)
  • 数据是 CSV 等扁平表格:先用 stratify 把「name,parent」两列重组为层级,再交给布局。

本文聚焦前者,即 d3.hierarchy 本身。在 d3 7 中,该 API 通过汇总包统一导出:src/index.js 中的 export * from "d3-hierarchy";d3.hierarchyd3.stratify 等全部可用,而 package.json 声明的依赖为 "d3-hierarchy": "^3.1.2"(d3 版本 7.9.0)。

二、用 d3.hierarchy(data, children) 构造根节点

d3.hierarchy(data[, children]) 从指定的层级数据构造根节点,要求 data 必须是代表根节点的对象。文档给出的标准示例:

const data = {
  name: "Eve",
  children: [
    {name: "Cain"},
    {name: "Seth", children: [{name: "Enos"}, {name: "Noam"}]},
    {name: "Abel"},
    {name: "Awan", children: [{name: "Enoch"}]},
    {name: "Azura"}
  ]
};

构造层级:

const root = d3.hierarchy(data);

可选的 children 访问函数会对每个数据项(从根 data 开始)调用,须返回代表子节点的可迭代对象(iterable);不指定时默认为:

function children(d) {
  return d.children;
}

Map 数据的隐式转换:若 data 是一个 Map,它会被隐式转换为条目 [undefined, data],同时 children 访问函数的默认值变为:

function children(d) {
  return Array.isArray(d) ? d[1] : null;
}

这让你可以把 grouprollup 的结果直接传给 d3.hierarchy——分组得到的 Map(键为分组键、值为该组的条目数组)天然匹配「键 → 子数据数组」的结构,无需手工展开。

此外,该方法也可用于判断节点是否为 instanceof d3.hierarchy,以及扩展节点原型。

节点属性:root 与每个子孙共有六个字段

属性 含义
node.data 传给 d3.hierarchy 的原始数据(与输入共享引用)
node.depth 根节点为 0,每向后代深一层加 1
node.height 到任意后代叶节点的最大距离;叶节点为 0
node.parent 父节点;根节点为 null
node.children 子节点数组;叶节点为 undefined
node.value 可选的聚合值,为节点与其后代 descendants 之和

注意 node.data 与布局写入的 x/y/r 等坐标字段并存于同一对象上:d3 v4 起,布局直接以这些根节点为输入,而不是操作原始 JSON,从而把输入数据与计算结果分离(见 CHANGES.md 中 d3-hierarchy 一节的设计说明),这也是后文 node.copy() 能单独隔离布局变更的前提。

三、祖先、后代与叶节点:ancestors / descendants / leaves

  • node.ancestors():返回祖先节点数组,从当前节点开始,依次向上直到根节点。典型用途是鼠标悬停时高亮某节点的全部上级。

  • node.descendants():返回后代节点数组,从当前节点开始,按拓扑顺序(父先于子)排列全部子孙。布局完成后调用它,即可拿到带坐标的全部节点数组:

    const nodes = root.descendants();
    
  • node.leaves():按遍历顺序返回叶节点数组。*叶节点(leaf)*指没有 children 的节点。

四、查找与路径:find / path

  • node.find(filter):返回从当前 node 出发、第一个使 filter 返回真值的节点;找不到返回 undefinedfind 是 d3-hierarchy 3.x 新增的 API(CHANGES.md 记录:"Add node.find")。
  • node.path(target):返回从当前 node 到指定 target 节点的最短路径:从起点上溯到两者的最近公共祖先(LCA),再下降到 target。该方法取代了 d3 v3 时代的 d3.layout.bundle,是层级边缘捆绑(hierarchical edge bundling)的基础原语。

五、生成边数据:links()

node.links() 返回当前节点及其全部后代的边(link)数组,每条边是带 sourcetarget 属性的对象:source 为父节点,target 为子节点。

const links = root.links();
// 每条 link:{ source: 父节点, target: 子节点 }

配合 d3-shape 的 link 生成器即可渲染节点连线图。从 CHANGES.md 可见,links() 自 d3 v4 起取代了 treemap.links 等各布局私有的边生成方法,成为所有层级布局的统一接口。

六、值聚合:sum(value) 与 count()

node.sum(value)

后序遍历(post-order)对当前节点及每个后代求值,并返回当前 node。每个节点的 node.value 被设为:该节点访问函数的返回值 + 所有子节点 value 之和。访问函数接收节点的 data,必须返回非负数

两个关键细节:

  1. value 访问函数会对节点和全部后代求值(含内部节点);若只希望叶节点贡献值,请对含子节点的节点返回 0。例如作为 node.count 的替代,按叶节点计数:

    root.sum((d) => d.value ? 1 : 0);
    
  2. 必须在调用需要 node.value 的层级布局之前调用 sumcount,例如 treemap

    // Construct the treemap layout.
    const treemap = d3.treemap();
    treemap.size([width, height]);
    treemap.padding(2);
    
    // Sum and sort the data.
    root.sum((d) => d.value);
    root.sort((a, b) => b.height - a.height || b.value - a.value);
    
    // Compute the treemap layout.
    treemap(root);
    
    // Retrieve all descendant nodes.
    const nodes = root.descendants();
    

    由于 API 支持方法链式调用,同样可以写成:

    d3.treemap()
        .size([width, height])
        .padding(2)
      (root
          .sum((d) => d.value)
          .sort((a, b) => b.height - a.height || b.value - a.value))
      .descendants()
    

    此示例假设节点数据带有 value 字段。

node.count()

统计当前节点下的叶节点数量并赋给 node.value,其每个后代同理;若当前节点本身是叶节点,计数为 1;返回当前 node。与 sum 的关系见上文。

七、重排子节点:sort(compare)

node.sort(compare) 按指定 compare 函数,对当前节点及其每个后代的 children 执行**前序遍历(pre-order)**排序,并返回当前 node

sum 的一个重要区别:compare 函数接收的是两个节点ab),而不是两个节点的 data。约定与 Array.prototype.sort 一致:a 应在 b 之前返回负值,反之返回正值,否则相对顺序未定义。

文档给出的三组典型用法(均建议先 sum):

  1. 按「聚合值」降序——circle-packing 的推荐排序:

    root
        .sum((d) => d.value)
        .sort((a, b) => b.value - a.value);
    
  2. 先按高度降序、再按值降序——treemapicicle 图 的推荐排序:

    root
        .sum((d) => d.value)
        .sort((a, b) => b.height - a.height || b.value - a.value);
    
  3. 先按高度降序、再按 id 升序——treedendrogram 的推荐排序:

    root
        .sum((d) => d.value)
        .sort((a, b) => b.height - a.height || d3.ascending(a.id, b.id));
    

调用时机:若希望新的排序顺序影响布局,必须在调用布局之前执行 node.sort

八、可迭代与三种遍历:Symbol.iterator / each / eachAfter / eachBefore

nodeSymbol.iterator

返回按**广度优先顺序(breadth-first order)**迭代 node 后代的迭代器:

for (const descendant of node) {
  console.log(descendant);
}

这是 d3-hierarchy 3.x 新增的能力:层级从此可直接 for...of 迭代(CHANGES.md 记录:"Add node[Symbol.iterator]; hierarchies are now iterable")。

node.each(function, that)

广度优先顺序node 及每个后代调用 function:某节点只有在所有更浅层节点及同层前序节点都访问完后才被访问。回调参数依次为:当前 descendant、零基遍历 index、当前 node(即调用对象);指定 that 时作为回调的 this 上下文。

node.eachAfter(function, that)

以**后序遍历(post-order)**调用:节点在所有后代都被访问之后才被访问。这是 sum 内部使用的遍历方向。

node.eachBefore(function, that)

以**前序遍历(pre-order)**调用:节点在所有祖先都被访问之后才被访问,sort 即以前序执行。

三个 each 方法的回调自 d3-hierarchy 3.x 起都会传入遍历 index(CHANGES.md:"Change node.each / eachAfter / eachBefore to pass the traversal index")。从仓库历史看(CHANGES.md 4.0 一节),这些非递归遍历方法正是层级布局内部实现的基础——布局(tree、treemap 等)改为用它们重写,以避免递归在大数据集上的开销。

九、深拷贝子树:copy()

node.copy() 返回以当前 node 为根的子树深拷贝(但拷贝共享同一份 data)。返回节点是一棵新树的根:其 parent 恒为 nulldepth 恒为 0。

典型用途是隔离布局副作用:布局会把 x/y/r 等坐标写回节点对象,若想在同一数据上尝试不同布局而不互相污染,可以先 root.copy() 再计算新布局(CHANGES.md 4.0 一节即以此为例:"use node.copy to isolate layout changes")。

十、实战要点小结

  1. 扁平表格数据先经 stratify(配合 d3.csvParse),层级 JSON 直接经 d3.hierarchyMap(如 group 结果)可直接传入并自动映射。
  2. 需要面积编码的布局(treemap、partition、pack)要求节点带 value,务必在布局前 sumcount;需要自定义顺序时在其后 sort,且 sort 的 compare 比较的是节点而非 data。
  3. 渲染时以 root.descendants() 取节点、root.links() 取边;交互高亮用 ancestors(),边缘捆绑用 path(),快速检索用 find()
  4. 遍历策略按需选择:按层处理用 each(BFS)、自底向上聚合用 eachAfter、自顶向下展开用 eachBefore、最简写法用 for...of
  5. 同一数据尝试多种布局时用 copy() 隔离坐标副作用。

进一步阅读:d3-hierarchy 模块总览stratifytreeclusterpartitionpacktreemap

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