D3 Treemap 完整指南:用 d3.treemap() 将层级数据铺满矩形的布局、平铺算法与动画实战
本篇基于 d3 官方文档 treemap 展开,系统讲解 D3 矩形树图(treemap)布局的完整 API——从 d3.treemap() 的基础用法、sum/sort 前置要求,到 tile、size、round、padding 系列配置项,以及全部六种内置平铺算法(squarify、resquarify、slice、dice、sliceDice、binary)的工作原理与选型依据,并结合当前仓库的依赖结构与文档交叉引用,给出可直接运行的实战代码。
什么是 Treemap:用面积编码层级的可视化技术
Treemap 由 Ben Shneiderman 于 1991 年提出,核心思想是递归地将一块区域划分为矩形,使每个节点对应的矩形面积与其关联的数值(value)成正比。它天然适合表达“整体—部分”(part-of-whole)的层级结构,例如磁盘空间占用、公司营收构成、文件树体积等场景:在固定画布上,面积大的矩形代表数值大的节点,父子关系通过矩形的嵌套体现。
D3 的 treemap 实现位于独立包 d3-hierarchy 中,并随主包 d3 一并发布。可以从当前仓库的依赖声明确认这一来源:package.json 中声明了 "d3-hierarchy": "^3.1.2"(当前 d3 主包版本为 7.9.0),而 src/index.js 第 16 行的 export * from "d3-hierarchy"; 说明 d3.treemap、d3.treemapSquarify 等 API 直接来自 d3-hierarchy 包的导出。因此下文所有 API 行为均以 d3-hierarchy 3.x 为准,适用于 d3 v7。
布局输出:rect 的四个坐标与前置条件
创建布局实例非常简单:
const treemap = d3.treemap();
调用 treemap(root) 时,它会以 hierarchy 为输入计算布局,并在 root 及其每个后代节点上写入四个坐标属性,构成该节点矩形的边界:
node.x0—— 矩形左边缘的 x 坐标node.y0—— 矩形上边缘的 y 坐标node.x1—— 矩形右边缘的 x 坐标node.y1—— 矩形下边缘的 y 坐标
两个关键前置条件(文档中用 “must” 强调了第一个):
- 必须先调用
node.sum(或node.count)求值:treemap 依赖node.value来确定面积,若节点没有 value,布局无法进行。 - 建议调用
node.sort排序:在计算布局前对子节点排序(通常按 value 降序),可以让重要节点聚集在画布左上角,改善视觉扫描顺序。
这一点在 hierarchy 文档 中有直接印证——其 node.sum 一节明确写道:“必须在调用需要 node.value 的层级布局(如 treemap)之前调用 node.sum 或 node.count”,并给出了官方示例:
// 构建 treemap 布局
const treemap = d3.treemap();
treemap.size([width, height]);
treemap.padding(2);
// 数据求值
const root = d3.hierarchy(data)
.sum(d => d.size)
.sort((a, b) => b.value - a.value);
// 计算 treemap 布局
treemap(root);
完整实战示例:从数据到 SVG 矩形
结合上述前置条件与配置项,一个完整可运行的 treemap 绘制流程如下(在浏览器环境中加载 d3 v7 后执行):
const width = 960, height = 600;
const treemap = d3.treemap()
.size([width, height]) // 画布尺寸 [宽, 高]
.padding(2) // 内外边距均为 2px
.round(true); // 坐标取整,避免亚像素模糊
const root = d3.hierarchy(data)
.sum(d => d.value) // 必须:自底向上累加 value
.sort((a, b) => b.value - a.value); // 必须前置的排序
treemap(root);
d3.select("svg")
.selectAll("rect")
.data(root.leaves()) // 通常只绘制叶子节点
.join("rect")
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("width", d => Math.max(0, d.x1 - d.x0)) // padding 可能造成负值,需保护
.attr("height", d => Math.max(0, d.y1 - d.y1));
几个容易踩的坑:
- 宽度/高度需做
Math.max(0, ...)保护:当节点数值过小或 padding 较大时,x1 - x0可能为 0 甚至负数,直接传给 SVG 的width属性会渲染失败; root.leaves()只返回叶子:若希望内层节点也有独立边框/标签(类似“分组标题”效果),应改用root.descendants().filter(d => d.children)分层处理,或对全部descendants()绘制并区分样式;- 数据更新后重新调用
treemap(root)即可:布局是幂等的,重复调用会重新计算所有坐标。
配置项全解:tile、size、round 与 padding 家族
除构造函数外,treemap() 实例提供了以下访问器,全部支持链式调用:
| 方法 | 作用 | 默认值 |
|---|---|---|
*treemap*.tile(tile) |
设置平铺算法(见下一节),参数为函数 | d3.treemapSquarify |
*treemap*.size(size) |
设置画布尺寸,二元数组 [width, height] |
[1, 1] |
*treemap*.round(round) |
布尔值,是否将输出坐标取整 | false |
*treemap*.padding(padding) |
同时设置内边距(paddingInner)和外边距(paddingOuter) | 0 |
*treemap*.paddingInner(padding) |
兄弟节点之间的间隔 | 0 |
*treemap*.paddingOuter(padding) |
同时设置上/右/下/左四边外边距 | 0 |
*treemap*.paddingTop / paddingRight / paddingBottom / paddingLeft |
分别设置单条外边距 | 0 |
关于 padding 的语义,需要特别区分两组概念:
- paddingInner(内边距):用于分隔一个节点的相邻子节点(即兄弟矩形之间的空隙)。
- paddingOuter(外边距):分为四条边——
paddingTop用于分离节点的上边缘与其子节点,paddingRight分离右边缘,paddingBottom分离下边缘,paddingLeft分离左边缘。它本质上是“父矩形与子矩形区域之间的留白”。 - 所有 padding 访问器都接受数字或函数;当传入函数时,它会在每个有子节点的节点上被调用,参数为该节点本身——这意味着可以实现“层级越深、边距越小”之类的自适应留白策略。
padding()是简写,等价于同时调用paddingInner()与paddingOuter();单独调用时,getter 方向返回的是 inner padding 函数(padding())或 top padding 函数(paddingOuter())。
示例:为层级留出呼吸感并只保留外框边距:
treemap.paddingInner(1) // 兄弟节点间 1px
.paddingOuter(10); // 四边外边距 10px
round(true) 的作用是把所有 x0/y0/x1/y1 取整,在 SVG 整数像素对齐时可减少抗锯齿造成的发虚;但若画布尺寸很小,取整误差的相对影响会被放大,可按需权衡。
六种内置平铺算法:从 slice-dice 到 squarify
*treemap*.tile(tile) 允许你替换整个平铺策略,这是 d3 treemap 最具扩展性的设计:平铺函数统一签名为 tile(node, x0, y0, x1, y1),即“在给定矩形区域内,如何为指定节点的子节点分配矩形”。内置了六种实现,各自的行为与适用场景如下。
d3.treemapSquarify:默认的金比例优化
treemap.tile(d3.treemapSquarify);
实现了 Bruls 等人的 squarified treemap 算法,目标是让生成的矩形尽量接近黄金比例(φ = (1 + √5) / 2)的宽高比。相比朴素的横竖切分,squarify 生成的矩形宽高比更均衡,文档明确指出这带来更好的可读性与面积估计能力——人眼对宽高比接近 1:1 到 1:2 的矩形面积判断最准确,极端细长的条状矩形则几乎无法比较大小。
squarify.ratio(ratio):自定义目标宽高比
d3.treemapSquarify.ratio(1.5);
treemap.tile(d3.treemapSquarify);
ratio必须是 ≥ 1 的数字,表示期望的宽高比;- 朝向不受 ratio 暗示:ratio 为 2 时,算法会尝试产出 2:1 或 1:2 的混合矩形,而不是只产出横矩形;
- ratio 只是给算法的提示(hint),不保证最终矩形精确达到该比例;
- 黄金比例作为默认值源自 Kong 等人关于 treemap 感知质量的研究结论。
文档还提示了一个实现“固定朝向”的技巧:先生成方形画布上的 treemap,再通过整体拉伸到目标宽高比(stretched treemap),可以近似得到统一朝向的矩形阵列。
d3.treemapResquarify:为动画而生的稳定版本
treemap.tile(d3.treemapResquarify);
行为类似 treemapSquarify,但有一个决定性差异:当上一次布局由 d3.treemapResquarify 计算且使用了相同的 target aspect ratio 时,它会保持既有的节点拓扑(相邻关系)不变,只改变节点尺寸。
这正是动画场景的首选:treemap 数据更新后,节点只“长大/缩小”而不会发生位置交换,避免了满屏矩形跳动、遮挡造成的视觉混乱。其代价文档也写得明白——后续更新得不到最优布局:只有第一次布局使用 Bruls 等算法的完整 squarify,之后的更新都在保持拓扑的前提下进行尺寸调整。换言之,resquarify 用“布局最优性”换“动画稳定性”,这是做动态 treemap 时的核心权衡。
d3.treemapSliceDice:按深度交替切分
treemap.tile(d3.treemapSliceDice);
若指定节点的深度为奇数,委托给 slice;否则委托给 dice。效果是奇偶层级分别在垂直/水平方向切分,形成交替的“田”字形结构。它的矩形宽高比不可控(可能出现细长条),但布局规则简单、位置稳定,常用作性能敏感或风格化场景的替代方案。
d3.treemapSlice 与 d3.treemapDice:单方向切分
treemap.tile(d3.treemapSlice); // 只垂直切分
treemap.tile(d3.treemapDice); // 只水平切分
treemapSlice(node, x0, y0, x1, y1):沿垂直方向按各子节点 value 切分矩形,子节点从矩形**顶边(y0)**起依次向下排列。treemapDice(node, x0, y0, x1, y1):沿水平方向切分,子节点从矩形**左边(x0)**起依次向右排列。
两者都处理了内部值(internal value)的情况:当子节点 value 之和小于父节点 value 时(即父节点自身携带非零内部值),剩余空白分别被放置在底边(y1)或右边(x1)。这一细节与 hierarchy 文档 中 node.sum 的说明呼应——若你只想让叶子节点贡献面积,应在 sum 的取值函数中对有子节点的节点返回 0,否则父节点的“自留地”会挤占子矩形的空间。
d3.treemapBinary:均衡二叉递归切分
treemap.tile(d3.treemapBinary);
递归地将节点集合对半切分成近似均衡的二叉树:区域较宽时选水平切分,较高时选垂直切分。每个父区域只产生两个子区域,递归直至叶子,产生的矩形形状受限于“二等分”,但算法开销低、结构规整,适合作为自定义平铺算法的实现起点。
选型速查与推荐组合
综合上述行为差异,可以按需求场景选择平铺方式:
| 场景 | 推荐 tile | 理由 |
|---|---|---|
| 静态展示、面积比较 | d3.treemapSquarify(默认) |
宽高比接近黄金比例,面积最易感知 |
| 数据动态更新 + 过渡动画 | d3.treemapResquarify |
保持拓扑,只有尺寸变化,无跳动遮挡 |
| 极简、规则外观 | d3.treemapSliceDice |
交替切分,结构简单、实现快 |
| 自定义/研究用途 | d3.treemapBinary 或自实现 |
二元签名 (node, x0, y0, x1, y1) 便于扩展 |
一个典型的“带动画更新”组合:
const treemap = d3.treemap()
.size([width, height])
.tile(d3.treemapResquarify) // 动画友好
.padding(2)
.round(true);
function update(newData) {
const root = d3.hierarchy(newData)
.sum(d => d.value)
.sort((a, b) => b.value - a.value);
treemap(root);
d3.selectAll("rect")
.data(root.leaves(), d => d.id) // key 函数稳定元素身份
.transition()
.duration(750)
.attr("x", d => d.x0)
.attr("y", d => d.y0)
.attr("width", d => Math.max(0, d.x1 - d.x0))
.attr("height", d => Math.max(0, d.y1 - d.y0));
}
配合 join 处理节点增删、用 d.id 之类的稳定 key 固定数据绑定,即可实现平滑的 treemap 数据更新动画。
仓库内如何进一步阅读与验证
- 本文主体文档:docs/d3-hierarchy/treemap.md,其内部对 hierarchy 的交叉引用是理解
sum/sort前置条件的关键; - 全局 API 索引:docs/api.md 第 637–655 行列出了 d3-hierarchy 中 treemap 相关的全部 18 个导出项(
d3.treemap、6 个平铺函数、squarify.ratio及各 padding 访问器),可作为速查表; - 同族的层级布局文档(cluster、tree、pack、partition、stratify)与 treemap 共享同一套
node.value前置约定,对照阅读可建立完整的 d3-hierarchy 布局体系认知; - 导出一致性保障:test/d3-test.js 遍历 package.json 中所有依赖模块并断言主包
d3导出了其中的每个属性(version除外),因此可以确定当前仓库的d3命名空间包含 d3-hierarchy 提供的全套 treemap API。
小结
d3 的 treemap 布局以“sum 求值 → sort 排序 → 设置 size/padding/tile → 调用 treemap(root) 得到 x0/y0/x1/y1”为主线,其核心竞争力在于可插拔的平铺算法:默认的 squarify 追求感知友好的黄金比例矩形,resquarify 则以牺牲后续布局最优性换取动画时的拓扑稳定,slice/dice/sliceDice/binary 则覆盖从单方向切分到均衡二分的各类简化需求。掌握 padding 内外两组语义、内部值的空白归属规则,以及 Math.max(0, …) 这类边界保护,就能把 treemap 用到磁盘分析、营收构成、文件树体积等任何“面积即数值”的场景中。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00