分区与包含 — 冰柱图、旭日图、矩形树图与圈包¶
这一章讲什么: 树的另外两副面孔。分区(partitions):把每个节点的 value 按比例切成矩形带——竖着是冰柱图、倒过来是火焰图、卷成圆是旭日图。 包含(enclosures):用「套」而不是「连」表达父子——矩形树图(treemap,面积精确) 和圈包(circle pack,包含关系醒目)。 上一章的树讲的是「连接」,这一章讲的是「占比」与「包含」——三种布局共享同一个 hierarchy 输入。
1. 顶层全景:同一个 root,三种布局¶
root(hierarchy 对象,已 count/sum)
├─ d3.tree / d3.cluster → 每节点 x, y → 第 15 章:节点 + 连线
├─ d3.partition → 每节点 x0,y0,x1,y1 → 本章:矩形带(冰柱/火焰/旭日)
├─ d3.treemap → 每节点 x0,y0,x1,y1 → 本章:嵌套矩形
└─ d3.pack → 每节点 x, y, r → 本章:嵌套圆
图说:列名不同,玩法不同;数据流水线一行都不用改。
2. 分区:d3.partition()¶
布局给每个节点算一个矩形:x0/x1(横向跨度)与 y0/y1(纵向层带),
尺寸由 size([w,h]) 与节点的 value 共同决定;padding 留缝、round 取整1。
冰柱图(icicle):每层一条横带,带宽 = 节点 value 占比:
root.sum(() => 1); // 每节点都算 1 → 冰柱的锥形
d3.partition().size([cht.w, cht.h])(root); // 布局
chart.selectAll("rect").data(root.descendants()).join("rect")
.attr("x", d => d.x0).attr("y", d => d.y0)
.attr("width", d => d.x1 - d.x0).attr("height", d => d.y1 - d.y0);
// 火焰图 = 整个图翻过来:
chart.attr("transform", `translate(${[0, dim.height]}) scale(1,-1)`);
// 图翻了文字也要跟着翻回来:文字再 translate(0,h) scale(1,-1)
sum(() => 1) 与 sum(取字段) 的区别原书点透了:前者连中间节点也计数,
子带比父带窄,冰柱/火焰呈锥形;后者纯按叶子值,空间填满2。
旭日图(sunburst) = 极坐标化的分区:size([2π, 半径]),x 解释成角度、
y 解释成半径,配第 12 章的 d3.arc() 出扇环3:
d3.partition().size([Math.PI * 2, cht.h * .6])(root);
const arc = d3.arc()
.startAngle(d => d.x0).endAngle(d => d.x1)
.innerRadius(d => d.y0).outerRadius(d => d.y1);
const chart = svg.append("g").attr("transform", `translate(${[w/2, h/2]})`); // 圆心居中
chart.selectAll("path").data(root.descendants()).join("path").attr("d", arc);
标签放 arc.centroid(d),按角宽取舍(d.x1 - d.x0 > Math.PI/10 才写字),
transform 里把弧度换算成角度再加 90°——和第 15 章径向树同款手法4。
3. 包含一:treemap(矩形树图)¶
d3.treemap().size([w,h])(root) 给每个节点算嵌套矩形。不加 padding 只能看到叶子
——中间节点的矩形被孩子完全盖住,这是 treemap 的默认形态5:
root.sum(d => d.weight);
const treemap = d3.treemap().size([cht.w, cht.h]).padding(5); // 5px 缝露出中间层
treemap(root);
chart.selectAll("rect").data(root.descendants()).join("rect")
.attr("x", d => d.x0).attr("y", d => d.y0)
.attr("width", d => d.x1 - d.x0).attr("height", d => d.y1 - d.y0)
.style("fill", d => color(d.height));
配置细节(原书 Table 16.2):padding/paddingInner/paddingOuter 三级缝宽,
paddingTop 单独给上层留标签栏;tile() 选铺砖算法——
默认 d3.treemapSquarify.ratio(1.61)(黄金比,砖块接近正方形最好读),
可换 treemapResquarify.ratio(1.5) 等六种,原书有对照图6。
4. 包含二:pack(圈包)¶
d3.pack().size([w,h])(root) 给每个节点算 x, y, r。两个专属细节7:
- 单链节点不可见:父圆只有一个孩子时,孩子填满父圆——必须加
padding(10)才分得开(加了 padding 后所有圆的相对大小位置都会变,原书有对比图); radius(访问器)可以不按 value 而按自定义值定半径;- 想提空间利用率:
data.filter(d => d.parent)把根圆去掉。
圈包的算法还能单拎出来用:d3.packSiblings(带 r 的圆数组) 把圆们紧凑排开
(返回加好 x/y 的数组);d3.packEnclose(同数组) 返回一个包住全部圆的圆 {x,y,r}——
散点气泡图、标签云都能借这两个函数8。
5. 交互:旋转、换根¶
旋转旭日图:拖拽角度差旋转——'start' 记起始角(Math.atan2(y,x)),'drag' 里
当前角与起始角的差累加进 state.r;用 getCTM 的平移量而不是 d3.pointer,旋转下更平滑。
仓库的 js/rotate-drag.js 有现成 addRotateDrag()9。
换根(钻取):点击节点 → target.copy() 取脱离的子树(depth 归零、parent 为 null)
→ 布局对子树重算(坐标按整幅画布重新分配)→ 重画;点当前根复位。
treemap 与 pack 同法,draw() 里 join 带 key 函数(d => d.data.id)10。
6. 作者的判断与证据¶
| 判断 | 性质 | 依据 |
|---|---|---|
| 「treemap 面积精确、pack 包含关系醒目」 | 选型判据 | 原书对两种包含图的定位对比(见 §3、§4 开头) |
| 「深层级 treemap 难读,换根/缩放改善」 | 经验之谈 | 交互一节的开场理由10 |
| 「旭日图 padding 要用弧度」 | 接口细节 | padding 语义随 size 的坐标单位走3 |
7. 边界与局限¶
- treemap 的六种铺砖算法只有一张对照图,各算法的适用场景没有文字分析6。
- pack 的布局算法(哪些启发式)不展开,只给了 packSiblings/packEnclose 两个出口。
- 旭日图标签的 textPath 弧形文字只在 Examples 里指路4。
- 径向 treemap 被一句「readability issues」带过6。
判断(我们的,不是书里的): 本章三个布局的对比,本质是一条「精度 vs 关系」的谱: treemap 用面积编码 value(读数最准,但中间层被压成缝),pack 用包含编码结构 (关系最清,但面积不准、空间浪费),partition/sunburst 折中(角度可读、层级可见)。 选型先问「读者要拿准数还是看懂结构」。 如果错,会错在: 这条谱对「浅层宽树」最成立;层级一深,sunburst 的外圈薄环 和 treemap 的细条同样不可读——那时该先换根/折叠而不是换布局,书里也确实是这个建议。
8. 可带走的¶
- partition/treemap 出
x0,y0,x1,y1,pack 出x,y,r——一套坐标三种画法; - 冰柱 = 矩形直接画;火焰 = 整图
scale(1,-1)翻转(文字要翻回来); sum(() => 1)出锥形(节点也计数),sum(字段)出满幅(只有叶子值);- sunburst = partition 的
size([2π, 半径])+d3.arc();标签挂 centroid、按角宽取舍; - treemap 默认只见叶子,
padding露中间层,paddingTop留标签栏; tile()换铺砖算法;默认 squarify 黄金比 1.61;- pack 必须加 padding,否则单链节点的父圆不可见;
packSiblings/packEnclose可单独借来排圆; - 旋转旭日图:atan2 记角差 + CTM 定位;换根钻取:
copy()子树 → 重算布局 → key join 重画。
9. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| partition 与四属性 | Creating Partitions \nand Enclosures | text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:45(搜「d3.partition()」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:66(搜「size([w,h])」) |
| 冰柱/火焰与 sum 语义 | Creating Partitions \nand Enclosures | text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:86(搜「root.sum(() => 1)」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:105(搜「scale(1,-1)」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:77(搜「conical shape」) |
| 旭日图 | Creating Partitions \nand Enclosures | text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:151(搜「Math.PI * 2」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:153(搜「startAngle(d => d.x0)」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:179(搜「arc.centroid(d)[0]」) |
| treemap 与配置 | Creating Partitions \nand Enclosures | text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:233(搜「d3.treemap()」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:244(搜「size([w,h])」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:292(搜「padding(5)」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:306(搜「treemapResquarify.ratio(1.5)」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:310(搜「1.61」) |
| pack 与算法 | Creating Partitions \nand Enclosures | text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:319(搜「d3.pack()」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:374(搜「Without padding」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:386(搜「packSiblings」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:388(搜「packEnclose」) |
| 旋转与换根 | Creating Partitions \nand Enclosures | text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:439(搜「dragBehavior」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:443(搜「atan2」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:532(搜「changeRoot」) · text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:538(搜「target.copy()」) |
| 章小结 | Creating Partitions \nand Enclosures | text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:558(搜「Summary」) |
-
出处:「Creating Partitions and Enclosures」第 44–78 段(
text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:45,搜「d3.partition()」;:65,搜「size([w,h])」;:68,搜「padding should be an angle」)。 ↩ -
出处:「Creating Partitions and Enclosures」第 74–78 段(
text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:77,搜「conical shape」)与第 82–143 段(:86,搜「root.sum(() => 1)」;:105,搜「scale(1,-1)」;:115,搜「d => d.weight」)。 ↩ -
出处:「Creating Partitions and Enclosures」第 146–186 段(
text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:151,搜「Math.PI * 2」;:152,搜「startAngle(d => d.x0)」;:170,搜「padding should be an angle」)。 ↩↩ -
出处:「Creating Partitions and Enclosures」第 173–192 段(
text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:179,搜「arc.centroid(d)[0]」;:181,搜「Math.PI/10」;:185,搜「converted to degrees」)。 ↩↩ -
出处:「Creating Partitions and Enclosures」第 203–299 段(
text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:240,搜「rectangular enclosure」;:289,搜「padding(5)」;:294,搜「paddingTop()」)。 ↩ -
出处:「Creating Partitions and Enclosures」第 301–316 段(
text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:306,搜「treemapResquarify.ratio(1.5)」;:310,搜「1.61」;:315,搜「readability issues」)。 ↩↩↩ -
出处:「Creating Partitions and Enclosures」第 318–379 段(
text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:333,搜「Adds space」;:330,搜「radius(accessor)」;:377,搜「removing the root node」)。 ↩ -
出处:「Creating Partitions and Enclosures」第 381–405 段(
text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:386,搜「packSiblings」;:388,搜「packEnclose」)。 ↩ -
出处:「Creating Partitions and Enclosures」第 423–467 段(
text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:439,搜「dragBehavior」;:443,搜「atan2」;:460,搜「smoother dragging effect」)。 ↩ -
出处:「Creating Partitions and Enclosures」第 468–546 段(
text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:532,搜「changeRoot」;:538,搜「target.copy()」;:539,搜「treemap(subtree)」)。 ↩↩