跳转至

分区与包含 — 冰柱图、旭日图、矩形树图与圈包

这一章讲什么: 树的另外两副面孔。分区(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. 可带走的

  1. partition/treemap 出 x0,y0,x1,y1,pack 出 x,y,r——一套坐标三种画法;
  2. 冰柱 = 矩形直接画;火焰 = 整图 scale(1,-1) 翻转(文字要翻回来);
  3. sum(() => 1) 出锥形(节点也计数),sum(字段) 出满幅(只有叶子值);
  4. sunburst = partition 的 size([2π, 半径]) + d3.arc();标签挂 centroid、按角宽取舍;
  5. treemap 默认只见叶子,padding 露中间层,paddingTop 留标签栏;
  6. tile() 换铺砖算法;默认 squarify 黄金比 1.61;
  7. pack 必须加 padding,否则单链节点的父圆不可见;packSiblings/packEnclose 可单独借来排圆;
  8. 旋转旭日图: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」)

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

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

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

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

  5. 出处:「Creating Partitions and Enclosures」第 203–299 段(text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:240,搜「rectangular enclosure」;:289,搜「padding(5)」;:294,搜「paddingTop()」)。 ↩

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

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

  8. 出处:「Creating Partitions and Enclosures」第 381–405 段(text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:386,搜「packSiblings」;:388,搜「packEnclose」)。 ↩

  9. 出处:「Creating Partitions and Enclosures」第 423–467 段(text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:439,搜「dragBehavior」;:443,搜「atan2」;:460,搜「smoother dragging effect」)。 ↩

  10. 出处:「Creating Partitions and Enclosures」第 468–546 段(text/24-ch16-chapter-16-creating-partitions-and-enclosures.txt:532,搜「changeRoot」;:538,搜「target.copy()」;:539,搜「treemap(subtree)」)。 ↩↩