D3 7 中 d3.hierarchy 根节点 API 详解:层级数据的表示、遍历与操作
本文基于 d3 仓库的官方文档 hierarchy 展开,系统讲解 d3.hierarchy 根节点的构造方式、六个核心属性、遍历/查找/排序/求值全套方法(ancestors、descendants、find、path、links、sum、sort、each 系列等)。读完本文,你可以将 JSON 或分组(Map)数据转成标准层级树,并正确地把 node.value 与排序结果喂给 treemap、tree、pack 等层级布局。
一、hierarchy API 在 d3-hierarchy 模块中的定位
很多数据天然是层级结构的(行政区划、组织架构、文件系统、软件包),d3-hierarchy 模块为此提供了几类经典可视化:节点连线图(tree、cluster 树状图)、邻接图(partition 冰柱图)、包围图(treemap、pack 圆形嵌套),详见模块总览 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.hierarchy 与 d3.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;
}
这让你可以把 group 或 rollup 的结果直接传给 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 返回真值的节点;找不到返回undefined。find是 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)数组,每条边是带 source 与 target 属性的对象: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,必须返回非负数。
两个关键细节:
-
value 访问函数会对节点和全部后代求值(含内部节点);若只希望叶节点贡献值,请对含子节点的节点返回 0。例如作为
node.count的替代,按叶节点计数:root.sum((d) => d.value ? 1 : 0); -
必须在调用需要
node.value的层级布局之前调用sum或count,例如 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 函数接收的是两个节点(a 和 b),而不是两个节点的 data。约定与 Array.prototype.sort 一致:a 应在 b 之前返回负值,反之返回正值,否则相对顺序未定义。
文档给出的三组典型用法(均建议先 sum):
-
按「聚合值」降序——circle-packing 的推荐排序:
root .sum((d) => d.value) .sort((a, b) => b.value - a.value); -
先按高度降序、再按值降序——treemap 与 icicle 图 的推荐排序:
root .sum((d) => d.value) .sort((a, b) => b.height - a.height || b.value - a.value); -
先按高度降序、再按 id 升序——tree 与 dendrogram 的推荐排序:
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 恒为 null,depth 恒为 0。
典型用途是隔离布局副作用:布局会把 x/y/r 等坐标写回节点对象,若想在同一数据上尝试不同布局而不互相污染,可以先 root.copy() 再计算新布局(CHANGES.md 4.0 一节即以此为例:"use node.copy to isolate layout changes")。
十、实战要点小结
- 扁平表格数据先经 stratify(配合 d3.csvParse),层级 JSON 直接经
d3.hierarchy;Map(如 group 结果)可直接传入并自动映射。 - 需要面积编码的布局(treemap、partition、pack)要求节点带
value,务必在布局前sum或count;需要自定义顺序时在其后sort,且sort的 compare 比较的是节点而非 data。 - 渲染时以
root.descendants()取节点、root.links()取边;交互高亮用ancestors(),边缘捆绑用path(),快速检索用find()。 - 遍历策略按需选择:按层处理用
each(BFS)、自底向上聚合用eachAfter、自顶向下展开用eachBefore、最简写法用for...of。 - 同一数据尝试多种布局时用
copy()隔离坐标副作用。
进一步阅读:d3-hierarchy 模块总览、stratify、tree、cluster、partition、pack、treemap。
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 StartedRust0623
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
