树与系统发育图 — d3-hierarchy 第一讲
树与系统发育图 — d3-hierarchy 第一讲
这一章讲什么: 层级数据(文件系统、分类学、组织结构)的可视化,从本章开始。 流水线固定为三节:数据 → hierarchy 对象 → 布局生成器 → 图形。 本章走通前两节,并用最经典的树图(tidy tree 与 dendrogram)把全部环节演示一遍; 同一套对象,下一章拿去画矩形树图和圈包,第 17 章拿去画网络。
1. 顶层全景:固定的四步流水线
① 数据整形(二选一)
嵌套对象(JSON 树)──► d3.hierarchy(数据, 孩子访问器)
父引用表格(CSV)──► d3.stratify().id().parentId()
│
▼
② hierarchy 对象:每个节点带 data/parent/depth/height/children
+ 方法:descendants() links() leaves() ancestors() path() copy()
+ 可选:count()(数叶子)/ sum(访问器)(求和) → 生成 value
│
▼
③ 布局生成器:d3.tree()(整洁树)或 d3.cluster()(系统发育图)
.size([w,h]) 或 .nodeSize([w,h]) → 给每个节点加 x/y
│
▼
④ 图形:圆点 + 连线(line/path) + 标签,或 Canvas 版
图说:②是枢纽——五个布局生成器(tree/cluster/partition/treemap/pack)共享同一个 hierarchy 输入。2. 核心原理
2.1 造 hierarchy 对象:两种数据源,两个函数
嵌套数据:d3.hierarchy(根对象, 孩子访问器)。默认找 children 属性,换了名字就传访问器1:
// 默认 children
const dataObj = {id:"root", children:[{id:"a", children:[{id:"aa"},{id:"ab"}]}]};
const root = d3.hierarchy(dataObj);
// 孩子叫 items
const root2 = d3.hierarchy(dataObj2, d => d.items);表格数据:每行一个节点、某列指回父节点的 id——d3.stratify() 造转换函数2:
const dataArray = [
{id:"root"},
{id:"a", context:"root"},
{id:"aa", context:"a"},
{id:"ab", context:"a"}
];
const toHierarchy = d3.stratify().id(d => d.id).parentId(d => d.context);
const root = toHierarchy(dataArray); // 与嵌套版得到同一棵树约束:单根、每个节点至多一个父(树的定义);数据不符要先补假根或中间节点3。
2.2 hierarchy 对象的属性与方法:本章真正的主角
每个节点自带的属性4:
| 属性 | 内容 |
|---|---|
data | 原始数据(对象或行)——标签、取值全从这里取 |
parent | 父节点引用(根为 null)——画连线就靠它 |
depth | 距根的层数(根 = 0)——配色、缩进 |
height | 距最远叶子的层数(叶子 = 0)——线宽、字号分级 |
children | 孩子数组(仅非叶节点) |
value | 不自动有:count()/sum() 之后才有,分区与圈包必需 |
方法三族:取子集(descendants() 全体后代、leaves() 全体叶子、ancestors() 祖先链、
links() 连边列表 {source, target}、path(target) 两点间唯一路径)、
复制(copy() 深拷贝子树)、遍历/配置(sum/count/sort/eachBefore/eachAfter)5:
root.count(); // value = 后代叶子数(叶子 value = 1)
root.sum(d => d.fileSize); // value = 后代某字段之和(文件系统大小)
root.sum(() => 1); // value = 后代总节点数2.3 两个布局:tree 与 cluster
d3.tree()(整洁树):同层节点并排、布局紧凑。
d3.cluster()(系统发育图/dendrogram):所有叶子对齐到最底层——生物学谱系图的标准长相。
两者配置相同,输出都是「每个节点多了 x/y」6:
const tree = d3.tree().size([500, 200]); // 整张图 500×200,坐标落在界内
tree(root); // root 的每个节点现在有了 x, y
// 或者按节点定尺寸(节点绝不重叠,但整树可能超出画布;根在 0,0 要自己挪):
const tree2 = d3.tree().nodeSize([30, 100]);
// 层间距微调:同母 1 格、异母 2 格(默认)
.separation((a,b) => a.parent == b.parent ? 1 : 2)size() 不保证节点不重叠(原书明写);nodeSize() 保证不重叠但可能超界——二选一7。
2.4 画图:点、线、标签
const nodes = root.descendants();
const links = root.links();
// 节点(按 depth 上色)
chart.selectAll("g").data(nodes).join("g")
.attr("transform", d => `translate(${[d.x, d.y]})`)
.append("circle").attr("r", d => (d.height + 1) * 10)
.style("fill", d => color(d.depth));
// 连法一:line,parent 引用直接取坐标(根要 filter 掉,它没有 parent)
chart.selectAll("line").data(nodes.filter(d => d.parent)).join("line")
.attr("x1", d => d.parent.x).attr("y1", d => d.parent.y)
.attr("x2", d => d.x).attr("y2", d => d.y);
// 连法二:path + linkVertical,平滑曲边
const verticalLink = d3.linkVertical().x(d => d.x).y(d => d.y);
chart.selectAll("path").data(links).join("path").attr("d", verticalLink);
// verticalLink({source:{x:150,y:0}, target:{x:50,y:100}})
// → "M150,0C150,50,50,50,50,100"标签的巧思值得抄:叶子(height === 0)文字旋转 −90° 竖排、内部节点白字居中;
字号随 height 平方增长——层级的视觉权重不靠手调8。
2.5 横放与径向:坐标换个解释
横放(根在左):一切 x/y 互换;d3.linkHorizontal().x(d => d.y).y(d => d.x)9。
径向(根在圆心):把 size 解释成 [2π, 半径],x 变角度、y 变半径;
节点容器 rotate(角度×180/π − 90) translate(半径)——减 90° 是补偿
(极坐标的 0 在 12 点,SVG 旋转的 0 在 3 点);连边用 d3.linkRadial().angle(d => d.x).radius(d => d.y),
或 d3.pointRadial 逐点转 Cartesian10。
2.6 Canvas 版与交互三件套
大树(千级节点)DOM 吃不消:Canvas 重画——生成器加 .context(ctx),
圆用 ctx.arc,文字逐节点 save()/translate/rotate/fillText/restore(),
代码量与 SVG 版相当,性能换交互便利11。
交互三件套(全部建立在 hierarchy 方法上):
- 路径高亮:悬停节点 →
node.path(targetNode)取路径节点集 → 路径上的节点/边加highlighted类、其余加faded类(CSS 管样式); 点击换目标节点12。 - 换根聚焦:点击节点 →
tree(点击的节点)重新布局(子树自成一个 hierarchy, depth 从它起算)→ 重画;再点根复位。叶子pointer-events: none不可点13。 - 展开/折叠:节点上挂
expanded标记,draw() 只渲染「当前路径祖先 + 已展开分支 到指定深度」;join 的 data 必须带 key 函数d => d.data.id(节点不按序进出, 按下标配对会错位)——第 05 章 key 函数的第一个实战14。
3. 作者的判断与证据
| 判断 | 性质 | 依据 |
|---|---|---|
| 「hierarchy 不可变,增删节点要重建」 | 设计限制 | 原书明说 not designed to be mutable14 |
| 「径向树好看但难读」 | 经验之谈 | 标签互相遮挡、根不明显两个具体理由10 |
| 「Canvas 是静态大树的优化方向」 | 工程权衡 | DOM 对象数量的内存问题11 |
4. 边界与局限
sort()、eachBefore/eachAfter只在方法表里,没有展开示例。- 树布局的算法细节(Tidy 算法本身)不在书中——只教用法。
- 径向树标签的两个方案给了文件路径,没展开讲10。
- 「多根数据怎么办」只说去 Examples 找,没给正文章节3。
判断(我们的,不是书里的): 本章最该带走的是那层中间的 hierarchy 对象: 它是「数据」与「图」之间的稳定接口——五种布局共享它、交互逻辑全部建立在它的 导航方法(path/ancestors/descendants)上。想给树图换一张脸(比如从 tree 换成 cluster 或径向),图层的代码几乎不动,动的只是布局那一行。这种「中间表示」设计 是 D3 全书最一致的建筑决策,也解释了为什么 15–17 章能连续复用同一批数据集。 如果错,会错在: 中间表示的代价是双重内存(原始数据 + 节点对象 + 坐标), 对百万级节点的树,hierarchy 对象本身就会成为瓶颈——那时要换 Canvas/WebGL 或分块加载,而这超出本书范围。
5. 可带走的
流水线一行背下: d3.hierarchy(嵌套数据) 或 d3.stratify()(表格) →
可选 .count()/.sum() → d3.tree().size([w,h])(root) →
root.descendants() 画点、root.links() + linkVertical 画边。
- 层级流水线四步:数据 → hierarchy → 布局 → 图形;hierarchy 是五布局共享的接口;
d3.hierarchy吃嵌套对象(孩子访问器可换名),d3.stratify().id().parentId()吃父引用表;- 节点属性:data(原始)/parent(画线靠它)/depth(层)/height(到叶距离)/value(count/sum 才有);
descendants()全体节点、links()连边、path(target)两点路径——交互全靠这三个;- tree 同层并排、cluster 叶子对齐;dendrogram = cluster;
- size(图尺寸,可能重叠)vs nodeSize(节点尺寸,不重叠但可能超界);
- 连线三选:line + parent 坐标 / linkVertical(纵)/ linkHorizontal(横,x/y 互换);
- 径向:size([2π, r]),x=角度 y=半径,rotate 记得 −90°,linkRadial 出曲边;
- 大树 Canvas:生成器 .context(ctx),文字 save/restore 包裹;
- 交互三件套:path() 高亮、换根(tree(子树) 重布局)、展开折叠(key 函数必备)。
6. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 布局生成器与转换函数两表 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:48(搜「d3.tree()」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:63(搜「d3.hierarchy(object,accessor)」) |
| 嵌套与表格两条造树路径 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:90(搜「id:"root", children」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:122(搜「d => d.items」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:146(搜「d3.stratify().id」) |
| hierarchy 属性与方法 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:170(搜「data」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:184(搜「descendants()」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:201(搜「sum(function)」) |
| count 与 sum | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:220(搜「root.count()」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:228(搜「d.fileSize」) |
| tree/cluster 与 size/nodeSize | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:261(搜「d3.tree() or d3.cluster()」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:285(搜「size([w,h])」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:272(搜「does not guarantee」) |
| 画点与连线 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:307(搜「root.descendants()」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:334(搜「nodes.filter(d => d.parent)」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:342(搜「root.links()」) |
| 标签与 link 生成器 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:377(搜「join("g").attr("class", "node")」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:403(搜「d3.linkHorizontal()」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:429(搜「M150,0C150,50」) |
| 横放 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:449(搜「linkHorizontal().x(d => d.y)」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:454(搜「translate(${[d.y, d.x]})」) |
| Canvas 树 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:469(搜「Using Canvas」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:481(搜「.context(ctx)」) |
| 径向树 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:542(搜「2 * Math.PI」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:552(搜「rotate(${d.x * 180/Math.PI」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:579(搜「linkRadial().angle」) |
| 路径高亮 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:637(搜「highlightPath」) |
| 换根 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:684(搜「changeRoot」) |
| 展开折叠与 key 函数 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:719(搜「maxDepth」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:730(搜「key function」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:743(搜「toggle(evt, node)」) |
| 不可变性与章小结 | Creating Tree Visualizations | text/23-ch15-chapter-15-creating-tree-visualizations.txt:758(搜「not designed to be mutable」) · text/23-ch15-chapter-15-creating-tree-visualizations.txt:763(搜「Summary」) |
Footnotes
-
出处:「Creating Tree Visualizations」第 84–126 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:90,搜「id:"root", children」;:102,搜「d3.hierarchy(dataObj)」;:122,搜「d => d.items」)。 ↩ -
出处:「Creating Tree Visualizations」第 129–162 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:146,搜「parentId(d => d.context)」;:146,搜「d3.stratify().id」;:150,搜「toHierarchy(dataArray)」)。 ↩ -
出处:「Creating Tree Visualizations」第 159–162 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:160,搜「many roots」)。 ↩ ↩2 -
出处:「Creating Tree Visualizations」第 165–177 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:170,搜「Contains the original data object」)。属性表照原书 Table 15.3 转写。 ↩ -
出处:「Creating Tree Visualizations」第 179–211 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:184,搜「descendants()」;:191,搜「path(target)」;:201,搜「sum(function)」)。方法表照原书 Table 15.4、15.5 转写。 ↩ -
出处:「Creating Tree Visualizations」第 245–293 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:253,搜「tidy tree」;:261,搜「d3.tree() or d3.cluster()」;:291,搜「separation(function)」)。 ↩ -
出处:「Creating Tree Visualizations」第 268–279 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:272,搜「does not guarantee」;:275,搜「root node will be positioned at the origin」)。 ↩ -
出处:「Creating Tree Visualizations」第 368–395 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:377,搜「attr("class", "node")」;:387,搜「d.data.id」;:392,搜「14 + (d.height*d.height)」)。 ↩ -
出处:「Creating Tree Visualizations」第 445–462 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:449,搜「linkHorizontal().x(d => d.y)」;:454,搜「translate(${[d.y, d.x]})」)。 ↩ -
出处:「Creating Tree Visualizations」第 533–583 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:542,搜「2 * Math.PI」;:552,搜「rotate(${d.x * 180/Math.PI」;:579,搜「linkRadial().angle」;:535,搜「harder to read」)。 ↩ ↩2 ↩3 -
出处:「Creating Tree Visualizations」第 464–531 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:469,搜「Using Canvas」;:481,搜「.context(ctx)」;:513,搜「textBaseline」)。 ↩ ↩2 -
出处:「Creating Tree Visualizations」第 603–662 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:637,搜「highlightPath」;:631,搜「targetNode = root」)。 ↩ -
出处:「Creating Tree Visualizations」第 665–704 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:684,搜「changeRoot」;:686,搜「tree(currentRoot)」;:702,搜「pointer-events: none」)。 ↩ -
出处:「Creating Tree Visualizations」第 706–761 段(
text/23-ch15-chapter-15-creating-tree-visualizations.txt:719,搜「maxDepth」;:730,搜「key function」;:743,搜「toggle(evt, node)」;:758,搜「not designed to be mutable」)。 ↩ ↩2