编程语言研究书库

树与系统发育图 — 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 方法上):

  1. 路径高亮:悬停节点 → node.path(targetNode) 取路径节点集 → 路径上的节点/边加 highlighted 类、其余加 faded 类(CSS 管样式); 点击换目标节点12。
  2. 换根聚焦:点击节点 → tree(点击的节点) 重新布局(子树自成一个 hierarchy, depth 从它起算)→ 重画;再点根复位。叶子 pointer-events: none 不可点13。
  3. 展开/折叠:节点上挂 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 画边。

  1. 层级流水线四步:数据 → hierarchy → 布局 → 图形;hierarchy 是五布局共享的接口;
  2. d3.hierarchy 吃嵌套对象(孩子访问器可换名),d3.stratify().id().parentId() 吃父引用表;
  3. 节点属性:data(原始)/parent(画线靠它)/depth(层)/height(到叶距离)/value(count/sum 才有);
  4. descendants() 全体节点、links() 连边、path(target) 两点路径——交互全靠这三个;
  5. tree 同层并排、cluster 叶子对齐;dendrogram = cluster;
  6. size(图尺寸,可能重叠)vs nodeSize(节点尺寸,不重叠但可能超界);
  7. 连线三选:line + parent 坐标 / linkVertical(纵)/ linkHorizontal(横,x/y 互换);
  8. 径向:size([2π, r]),x=角度 y=半径,rotate 记得 −90°,linkRadial 出曲边;
  9. 大树 Canvas:生成器 .context(ctx),文字 save/restore 包裹;
  10. 交互三件套:path() 高亮、换根(tree(子树) 重布局)、展开折叠(key 函数必备)。

6. 原文地图

主题原书章原文位置
布局生成器与转换函数两表Creating Tree Visualizationstext/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 Visualizationstext/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 Visualizationstext/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 与 sumCreating Tree Visualizationstext/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/nodeSizeCreating Tree Visualizationstext/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 Visualizationstext/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 Visualizationstext/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 Visualizationstext/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 Visualizationstext/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 Visualizationstext/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 Visualizationstext/23-ch15-chapter-15-creating-tree-visualizations.txt:637(搜「highlightPath」)
换根Creating Tree Visualizationstext/23-ch15-chapter-15-creating-tree-visualizations.txt:684(搜「changeRoot」)
展开折叠与 key 函数Creating Tree Visualizationstext/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 Visualizationstext/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

  1. 出处:「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」)。 ↩

  2. 出处:「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)」)。 ↩

  3. 出处:「Creating Tree Visualizations」第 159–162 段(text/23-ch15-chapter-15-creating-tree-visualizations.txt:160,搜「many roots」)。 ↩ ↩2

  4. 出处:「Creating Tree Visualizations」第 165–177 段(text/23-ch15-chapter-15-creating-tree-visualizations.txt:170,搜「Contains the original data object」)。属性表照原书 Table 15.3 转写。 ↩

  5. 出处:「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 转写。 ↩

  6. 出处:「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)」)。 ↩

  7. 出处:「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」)。 ↩

  8. 出处:「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)」)。 ↩

  9. 出处:「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]})」)。 ↩

  10. 出处:「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

  11. 出处:「Creating Tree Visualizations」第 464–531 段(text/23-ch15-chapter-15-creating-tree-visualizations.txt:469,搜「Using Canvas」;:481,搜「.context(ctx)」;:513,搜「textBaseline」)。 ↩ ↩2

  12. 出处:「Creating Tree Visualizations」第 603–662 段(text/23-ch15-chapter-15-creating-tree-visualizations.txt:637,搜「highlightPath」;:631,搜「targetNode = root」)。 ↩

  13. 出处:「Creating Tree Visualizations」第 665–704 段(text/23-ch15-chapter-15-creating-tree-visualizations.txt:684,搜「changeRoot」;:686,搜「tree(currentRoot)」;:702,搜「pointer-events: none」)。 ↩

  14. 出处:「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

On this page