首页
/ D3 Treemap 完整指南:用 d3.treemap() 将层级数据铺满矩形的布局、平铺算法与动画实战

D3 Treemap 完整指南:用 d3.treemap() 将层级数据铺满矩形的布局、平铺算法与动画实战

2026-09-04 18:15:38作者:昌雅子Ethen

本篇基于 d3 官方文档 treemap 展开,系统讲解 D3 矩形树图(treemap)布局的完整 API——从 d3.treemap() 的基础用法、sum/sort 前置要求,到 tilesizeroundpadding 系列配置项,以及全部六种内置平铺算法(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.treemapd3.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” 强调了第一个):

  1. 必须先调用 node.sum(或 node.count)求值:treemap 依赖 node.value 来确定面积,若节点没有 value,布局无法进行。
  2. 建议调用 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 访问器),可作为速查表;
  • 同族的层级布局文档(clustertreepackpartitionstratify)与 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 用到磁盘分析、营收构成、文件树体积等任何“面积即数值”的场景中。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384