d3 圆形打包布局详解:用 d3.pack 构建层级包含图(Enclosure Diagram)
【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3
本文以 d3 仓库中的 圆形打包布局文档 为主体,完整讲解d3.pack系列 API(pack()、radius、size、padding、packSiblings、packEnclose)的语义、参数取值与默认值,并结合仓库中的 d3-hierarchy 模块说明、层级数据 API 文档 与 版本变更历史 补充前置调用链与底层算法依据。读完本文,你可以独立完成一棵「叶子圆大小编码数值、嵌套圆表示子树累计规模」的包含图布局,并理解其近似性来源与可复用的底层几何算法。
包含图与圆形打包:定位与适用场景
层级模块总览 将层级可视化归纳为三类:节点连线图(node-link,如树状图)、邻接图(adjacency,如 icicle/sunburst)、包含图(enclosure)。圆形打包(circle packing)属于包含图:用「包含(嵌套)」关系表达层级拓扑,用叶子圆的面积编码数据的量化维度。
原文档对这类图的刻画值得逐句重视:
- 叶子圆的大小编码数据的量化维度,是图中唯一可精确比较的部分;
- 外围的父级圆展示各子树的近似累计规模——由于圆嵌套必然产生「浪费」的空白空间,父级圆的面积存在失真;
- 与 矩形树图(treemap) 相比,circle packing 的空间利用率更低,但这些「浪费」的空间反而更突出地揭示了层级结构本身。
因此选型经验是:追求空间利用效率、强调各单元精确可比时选 treemap;强调嵌套关系可读性、允许父级近似时选 pack。
前置条件:先 sum、再 sort,最后 pack
pack(root)要求输入是一棵已经构建好的 hierarchy。pack 文档 明确要求:在把 hierarchy 传给 pack 布局之前,必须先调用root.sum(),并且通常还需要调用root.sort()来决定布局顺序。
这一点在 hierarchy.md 的node.sort一节 得到了呼应——文档专门给出了「circle-packing 推荐」的排序比较函数:
root .sum((d) => d.value) .sort((a, b) => b.value - a.value);两个容易踩坑的细节:
sum()的访问器对**每个节点(包括内部节点)**求值,若只有叶子携带数值,内部节点应返回 0(原文示例:root.sum((d) => d.value ? 1 : 0));- 比较函数收到的是两个节点
a、b而非两个 data 对象,所以要写b.value - a.value而不是b.data.value - a.data.value; sort必须在调用布局之前执行,排序结果才会影响布局。
API 全解:d3.pack 与它的四个访问器
pack() 与 pack(root)
d3.pack()以默认配置创建一个 pack 布局;调用pack(root)则对该roothierarchy 计算布局,在root及其每个后代上写入三个属性:
| 属性 | 含义 |
|---|---|
node.x | 圆心的 x 坐标 |
node.y | 圆心的 y 坐标 |
node.r | 圆的半径 |
这与 CHANGES.md 中记录的 v4 重写一致:布局以 root 节点为输入(而非直接操作 JSON 数据),布局结果挂载在节点对象上,输入数据与计算结果由此分离。
pack.radius(radius)
- 传入
radius时,设置为布局的半径访问器并返回该布局;不传时返回当前访问器,默认值为null。 - 访问器为
null时(默认),每个叶子圆的半径由叶子的node.value(即sum()的结果)推导,再按布局尺寸等比缩放。 - 访问器非
null时,叶子圆半径精确等于函数返回值,布局不再从value推导半径。
pack.size(size)
- 传入
size时,设置为[width, height]形式的二元数组并返回该布局;不传时返回当前尺寸,默认值为[1, 1]。 - 半径自动缩放、两遍填充算法都以这个尺寸为基准。
pack.padding(padding)
- 传入
padding时,设置为 padding 访问器(数字或函数,函数形式可按父节点动态计算,这一点是 v4 重写新增的能力);不传时返回当前访问器,默认值为常数 0。 - 作用范围:相邻兄弟圆之间的切线间隙约等于该 padding,父圆与其子圆之间的间隙也约等于该 padding(后者同样是 v4 新增,见 CHANGES.md 对
d3.pack重写的记录)。 - 重要限制:若未指定显式
radius,padding 只是近似值——因为要在布局尺寸内完成填充,需要两遍(two-pass)算法:先无 padding 打包,计算一个缩放因子并作用于指定 padding,最后带 padding 重新打包。
一个可直接复用的端到端示例
综合以上语义,一个完整的包含图计算与渲染流程如下(适用于当前仓库构建出的d3v7.9.0 产物,见 package.json):
// 1. 构建 hierarchy:先 sum(必需),再 sort(影响布局顺序) const root = d3.hierarchy(data) .sum(d => d.value ? d.value : 0) // 若只有叶子携带数值,内部节点返回 0 .sort((a, b) => b.value - a.value); // circle-packing 推荐排序 // 2. 创建并配置 pack 布局 const pack = d3.pack() .size([960, 960]) // 默认 [1, 1],务必设为实际画布尺寸 .padding(2); // 默认 0;兄弟间隙与父子间隙均受其影响 // 3. 计算布局:为 root 及所有后代写入 x / y / r pack(root); // 4. 渲染所有后代 d3.select("#chart") .append("svg") .attr("viewBox", "0 0 960 960") .selectAll("circle") .data(root.descendants()) .join("circle") .attr("cx", d => d.x) .attr("cy", d => d.y) .attr("r", d => Math.max(0, d.r)) .attr("fill", d => d.children ? "none" : "steelblue");其中d.children ? "none" : "steelblue"的写法体现了包含图的语义分工:父级圆只作轮廓(近似累计值),叶子圆才实心编码数值,避免把失真的父级面积当成精确数据来比较。
可独立复用的几何原语
pack 布局由两个非层级(non-hierarchical)的几何函数支撑,它们脱离 hierarchy 也可单独使用,在 CHANGES.md 中被明确记录为 v4 重写时新增/保留的公共 API。
packSiblings(circles)
对传入的圆数组做打包:每个圆必须有circle.r属性,函数为每个圆写入circle.x、circle.y。圆的位置按 CHANGES.md 所引 Wang 等人的front-chain packing 算法计算——即沿一条「前链」逐个把圆贴合到已有圆上,这是 pack 布局兄弟圆排布的实际算法基础。
packEnclose(circles)
计算能完全包住给定圆数组的最小圆(smallest enclosing circle)。输入要求每个圆具备circle.r、circle.x、circle.y,返回的包围圆同样带x、y、r。它采用Matoušek-Sharir-Welzl 算法求精确解。
从实现历史看,这个「精确」是 v4 重写的关键改进之一:CHANGES.md 记录 3.x 时代每个父圆的包围圆是用 Wang 等人算法的近似值算出的,而 4.0 起改用 Welzl 算法计算精确最小包围圆,同时修复了 major bugs 并改善了整体打包效果。仓库内正好保留了这两代输出的对比图(CHANGES.md 第 841 行附近):
左图为 3.x 输出,右图为 4.0 输出,可直观看到重写后父圆贴合更紧、结果更规整:
packEnclose也意味着 pack 布局「父圆半径 = 包住子圆的精确最小圆」这一行为有据可查——父级圆的近似失真主要来自圆嵌套的固有空白,而非包围圆计算误差。
在 d3 仓库中的落点与引用方式
- 本仓库是
d3主包(v7.9.0,见 package.json),通过依赖"d3-hierarchy": "^3.1.2"引入 d3-hierarchy,并在 src/index.js 中export * from "d3-hierarchy";整体再导出——因此d3.pack、d3.packSiblings、d3.packEnclose都以d3.命名空间暴露给使用者; - 构建入口为 bundle.js,经 rollup.config.js 产出
dist/d3.js(UMD)、dist/d3.mjs(ESM)、dist/d3.min.js(压缩版)三种产物,构建产物中 src/index.js 的再导出链条保证 pack API 完整包含; - 文档链接完整性由 test/docs-test.js 自动校验(爬取
docs/下所有 Markdown 的内部链接与锚点,断言无 404),这也是 docs/d3-hierarchy/pack.md 中内部锚点(如#pack_radius、#pack_size)能被稳定引用的工程保障; - 相关文档可进一步延伸阅读:hierarchy(节点构造、
sum/sort/descendants)、treemap(空间利用率的对照方案)、stratify(从表格数据构建 hierarchy 的前置步骤)。
小结
d3.pack的完整心智模型可以归纳为四条:
- 输入契约:先
root.sum()(必需)+root.sort()(推荐按聚合值降序),再交给pack(root); - 输出契约:每个节点得到
x、y、r,叶子圆可精确比较,父级圆是近似累计; - 三个访问器:
radius(默认null,由value推导)、size(默认[1, 1])、padding(默认0,支持函数,无显式半径时为两遍近似); - 可复用原语:
packSiblings(front-chain 打包)与packEnclose(Welzl 精确最小包围圆)可脱离层级单独使用,v4 重写正是靠它们获得了更精确、更规整的打包结果。
适用前提说明:以上内容以当前仓库(d3 v7.9.0、d3-hierarchy ^3.1.2、Node >= 12,见 package.json)的实际内容为准确认;布局算法的细节实现位于 d3-hierarchy 依赖包内部,本仓库以再导出与文档的形式承载其行为说明。
【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考