D3.js 中 d3.stratify 详解:把 CSV 表格与路径字符串转化为分层数据的实战指南
本文基于 D3.js 官方文档 Stratify 章节 展开,讲解 d3.stratify 如何将扁平的表格数据(如 CSV 解析出的记录数组)或 Unix 风格的分隔路径,重建为可供树形、矩形树图等布局算法消费的层次结构(hierarchy)。读完本文,你将掌握 stratify、stratify(data)、stratify.id、stratify.parentId、stratify.path 五个 API 的默认行为、参数约定与约束条件,并能独立完成「CSV → 层级节点」这一 D3 层次数据可视化的标准预处理流程。
为什么需要 stratify:表格数据与层级数据之间的桥梁
D3 的层次布局(如 tidy 树、dendrogram、treemap、circle-packing)都要求输入是一棵以根节点为起点的树。官方在 d3-hierarchy 模块说明 中把这一模块定位为「组织层次数据并可视化」,而 Hierarchies 章节 开篇就点明了两条数据入口:
- 数据本身已经是嵌套结构(如 JSON),可以直接交给
d3.hierarchy(data); - 数据是扁平的表格形式(如 CSV),则需要先用 stratify 把表格「重排」为层次结构。
现实中的组织关系、文件清单、分类目录绝大多数以扁平表格形式存储——每行一条记录,用「自身 id + 父级 id」两列描述从属关系。d3.stratify 正是把这类「父子邻接表」(parent pointer table)转换为树的操作算子。
从关系表格到 CSV:完整的输入示例
官方文档以一张人物关系表作为贯穿始终的示例:
| Name | Parent |
|---|---|
| Eve | |
| Cain | Eve |
| Seth | Eve |
| Enos | Seth |
| Noam | Seth |
| Abel | Eve |
| Awan | Eve |
| Enoch | Awan |
| Azura | Eve |
由于这些名字互不重复,这份关系表可以无歧义地表示为一个 CSV 文件:
name,parent
Eve,
Cain,Eve
Seth,Eve
Enos,Seth
Noam,Seth
Abel,Eve
Awan,Eve
Enoch,Awan
Azura,Eve
用 csvParse 解析该文本:
const table = d3.csvParse(text);
它返回一个由 {name, parent} 对象组成的数组(注意:CSV 解析出的所有值默认都是字符串):
[
{"name": "Eve", "parent": ""},
{"name": "Cain", "parent": "Eve"},
{"name": "Seth", "parent": "Eve"},
{"name": "Enos", "parent": "Seth"},
{"name": "Noam", "parent": "Seth"},
{"name": "Abel", "parent": "Eve"},
{"name": "Awan", "parent": "Eve"},
{"name": "Enoch", "parent": "Awan"},
{"name": "Azura", "parent": "Eve"}
]
注意根节点 Eve 的 parent 是空字符串 ""——这一点与后面 stratify 的约定直接相关。
核心用法:stratify(data) 构建层次结构
拿到表格数据后,调用 stratify 算子即可生成层次结构:
const root = d3.stratify()
.id((d) => d.name)
.parentId((d) => d.parent)
(table);
返回的 root 就是一个标准的 hierarchy 根节点,可以立即传给任意的层次布局算子,例如 tree、treemap、pack 进行可视化。
这里有一个值得留意的细节:CSV 中 Eve 的 parent 是空字符串,而 stratify.parentId 的约定是「空字符串与 null 等价于 undefined」,因此空字符串恰好被识别为「无父节点」,即根节点。表格中根行 parent 列留空的写法与 stratify 的语义天然吻合。
API 详解:五个方法及其默认值
stratify 算子的完整 API 共五个方法,全部实现在 d3-hierarchy 模块的 src/stratify.js(在本仓库中通过 package.json 以 d3-hierarchy: ^3.1.2 依赖引入,并由 src/index.js 的 export * from "d3-hierarchy" 统一导出为全局 d3.stratify)。
stratify()
构造一个使用默认设置的 stratify 算子:
const stratify = d3.stratify();
它本身只是一个可配置的工厂;真正执行转换要等到把数据传入算子。
stratify(data)
从给定的表格 data 生成一棵新的层次结构:
const root = stratify(data);
这是唯一「执行型」方法,返回 hierarchy 根节点,其内部会按 id / parentId(或 path)访问器建立节点间的父子指针。
stratify.id(id)
若传入 id,将 id 访问器设置为给定函数并返回该算子(支持链式调用);否则返回当前 id 访问器。默认访问器为:
function id(d) {
return d.id;
}
id 访问器会对传入 stratify 算子的每条记录调用一次,参数依次为当前数据项 (d) 和当前索引 (i)。返回值(字符串)用于结合 parent id 识别节点之间的从属关系。约束条件如下:
- 叶子节点的 id 可以为 undefined;
- 非叶子节点的 id 必须唯一;
- null 和空字符串等价于 undefined。
默认访问器读的是记录的 d.id 字段;如果数据里用别的字段名(如示例中的 name),就必须像上文那样显式调用 .id((d) => d.name) 覆盖。
stratify.parentId(parentId)
若传入 parentId,将父节点 id 访问器设置为给定函数并返回该算子;否则返回当前访问器。默认访问器为:
function parentId(d) {
return d.parentId;
}
parent id 访问器同样对每条记录调用,参数为 (d, i)。返回值与 id 访问器配合确定节点的父子关系。关键约束:
- 根节点的 parent id 应为 undefined(空字符串/null 等价于 undefined);
- 输入数据中必须恰好存在一个根节点;
- 不允许出现循环引用。
这三条约束是排查 stratify 报错(如找不到根、出现孤儿节点)时的第一检查点:常见原因是数据里混入了第二行 parent 为空的记录,或 parent 指向了不存在于表中的 id。
stratify.path(path)
若传入 path,将路径访问器设置为给定函数并返回该算子;否则返回当前路径访问器,默认值为 undefined。
一旦设置了 path 访问器,id 与 parentId 访问器即被忽略,stratify 改为基于路径访问器返回的斜杠(/)分隔字符串计算类 Unix 文件系统式的层次结构,并在必要时自动补全中间层父节点和父节点 id——这是 path 模式与 id/parentId 模式最重要的行为差异:表格里不需要显式列出每一层目录。
path 模式实战:把 find 命令输出变成目录树
官方文档给出了一个非常典型的场景:对当前目录执行 UNIX find 命令,得到一组相对路径:
const paths = [
"axes.js",
"channel.js",
"context.js",
"legends.js",
"legends/ramp.js",
"marks/density.js",
"marks/dot.js",
"marks/frame.js",
"scales/diverging.js",
"scales/index.js",
"scales/ordinal.js",
"stats.js",
"style.js",
"transforms/basic.js",
"transforms/bin.js",
"transforms/centroid.js",
"warnings.js",
];
一行即可构建目录层次:
const root = d3.stratify().path((d) => d)(paths);
这里 (d) => d 表示数据项本身就是字符串,直接作为路径使用。stratify 会把 marks/dot.js 拆分为 marks → marks/dot.js 两级,并自动推断出中间节点 marks 的 id 和父节点 ""(根)。
这种用法在本仓库的文档站点中就有真实应用。what-is-d3 页面 的源码脚本里,先用侧边栏配置递归生成一组 {path, link} 路径数据,随后计算树图宽度时直接组合了 stratify 的 path 模式与 tree 布局:
const root = d3.tree().nodeSize([1, 1])(d3.stratify().path((d) => d.path)(paths));
这恰好演示了「路径数组 → stratify.path → 层次布局」的完整链路:访问器改为 (d) => d.path,从记录对象中取出路径字段即可。
组合使用:从 CSV 到可视化的完整管线
把前面各节串起来,一个典型的表格数据树状图管线为:
// 1. 解析 CSV 文本为记录数组
const table = d3.csvParse(text);
// 2. 用 id / parentId 访问器把表格转成层次结构
const root = d3.stratify()
.id((d) => d.name)
.parentId((d) => d.parent)
(table);
// 3. 交给层次布局算子,例如 tidy 树
d3.tree()(root);
得到 root 之后,就可以使用 Hierarchies 文档 中介绍的全部节点方法:root.descendants()、root.links()、root.sum(...)、root.eachBefore(...) 等,驱动 SVG/Canvas 的绘制。
小结与检查清单
d3.stratify()是「扁平表格 → 树」的转换算子,stratify(data)执行转换,.id()/.parentId()配置两列 id,.path()切换到按斜杠路径自动推断层级的模式;- id 默认读
d.id,parentId 默认读d.parentId;字段名不同时必须显式指定访问器; - 空字符串与 null 等价于 undefined,因此 CSV 中根节点 parent 列留空即可;
- 输入必须恰好有一个根节点、非叶子节点 id 唯一、无循环引用;
- 设置了 path 访问器后,id 与 parentId 访问器失效,中间层节点会被自动补全;
- 输出是标准 hierarchy 根节点,可直接接入 tree、cluster、treemap、partition、pack 等全部 d3-hierarchy 布局算法(见 d3-hierarchy 模块总览 与 API 索引 中 d3.stratify 相关条目)。
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
