如何用 d3.treemap 把层级数据渲染为矩形布局?
【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3
如果你手里有一份带数值的层级数据(比如按目录统计的文件大小、按部门拆分的营收),想把它画成一块被递归细分的矩形区域——即 treemap——那么 d3-hierarchy 的d3.treemap()就是布局计算工具。本文的完整目标是:在浏览器里加载 D3,从一份层级 JSON 出发,调用d3.treemap()为每个节点算出x0, y0, x1, y1四个矩形坐标,再把它们渲染成 SVG 矩形并验证结果。依据是仓库中的 d3-hierarchy/treemap 与 d3-hierarchy/hierarchy 文档。
D3 在任何 JavaScript 环境都能运行。按 getting started 的说明,官方推荐使用 CDN 托管的 ES module 包;如果你需要不依赖模块加载的环境,还有 UMD 包和 npm 安装两种替代方式。
准备:加载 D3 并确认数据形态
treemap 文档要求布局的输入是一个 hierarchy 根节点,因此数据必须满足两点:
- 数据本身是层级结构。文档给出的标准形态是一个根对象,其
children数组逐层嵌套子节点:
const data = { name: "Eve", children: [ {name: "Cain"}, {name: "Seth", children: [{name: "Enos"}, {name: "Noam"}]}, {name: "Abel"}, {name: "Awan", children: [{name: "Enoch"}]}, {name: "Azura"} ] };如果你的数据还是 CSV 等表格形式,hierarchy 文档指出需要先用 stratify 整理成层级后再传给d3.hierarchy。
- 每个参与面积计算的节点要有非负数值。
node.sum的文档明确要求 value 访问函数返回非负数。下面把上面的示例数据补充value字段(叶子节点取数值,内部节点不取值),后面代码即按这份数据编写:
const data = { name: "Eve", children: [ {name: "Cain", value: 45}, {name: "Seth", children: [{name: "Enos", value: 10}, {name: "Noam", value: 20}]}, {name: "Abel", value: 30}, {name: "Awan", children: [{name: "Enoch", value: 15}]}, {name: "Azura", value: 40} ] };加载 D3 有三种方式,任选其一,后文代码对三者通用:
- CDN ES module(文档推荐):
<!DOCTYPE html> <div id="container"></div> <script type="module"> import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm"; // ……后文的完整代码…… container.append(svg.node()); </script>- CDN UMD:用
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>加载,它导出全局d3,后续代码不变。 - npm:执行
npm install d3(文档也列出 yarn / pnpm 对应命令),然后在模块里import * as d3 from "d3";。
此外 getting started 还提到 Observable 笔记本内置 D3、返回单元格生成的 DOM 元素即可展示图表,可作为在线调试环境。
第一步:构建层级并计算节点数值
用d3.hierarchy(data)从上面的data构造根节点:
const root = d3.hierarchy(data);不指定 children 访问函数时,默认取d.children,正好匹配上面的数据结构。构造完成后,每个节点带有node.data、node.depth、node.height、node.parent、node.children等属性,其中node.value是需要你显式计算汇总的"可选属性"。
treemap 文档写明:必须在把层级交给 treemap 布局之前调用root.sum(或root.count),否则节点没有value可用。sum按后序遍历把每个节点的值设为"自身访问函数返回值 + 所有子节点值之和",访问函数收到的是节点的 data:
root.sum((d) => d.value ?? 0);?? 0只用于本示例:内部节点没有value字段,返回 0 表示"数值全部来自叶子"。这正是文档给出的做法——如果你只想让叶子节点贡献数值,对任何有 children 的节点返回 0(例如root.sum((d) => d.value ? 1 : 0))。若数据每个节点都自带汇总值,则写成root.sum((d) => d.value)。
如果只想按叶子数量布局,可以直接用root.count(),它把每个节点值设为其下属叶子数。
第二步:配置并调用 treemap 布局
treemap 文档 的默认值:size默认为[1, 1],padding默认为 0,round默认false,平铺方法默认为黄金比例的treemapSquarify。因此要得到实际尺寸的布局,必须先设置size。hierarchy文档给出的官方示例写法是:
// Construct the treemap layout. const treemap = d3.treemap(); treemap.size([width, height]); treemap.padding(2);其中width和height是画布尺寸,取你自己的值;padding(2)同时设置内边距和外边距(本文取 2 像素,数值可自定)。
文档同时建议:在计算布局前调用root.sort对层级排序,treemap 推荐的比较方式是"高度降序、值降序":
root.sort((a, b) => b.height - a.height || b.value - a.value);注意 sort 的比较函数收到的是两个节点而不是两个 data,这与Array.prototype.sort比较 data 的习惯不同。排序必须在调用布局前完成,否则不影响本次布局结果。
最后调用布局计算:
treemap(root);调用后,root及其每个后代节点都被写入了四个属性,这就是渲染的全部输入:
node.x0—— 矩形左边缘node.y0—— 矩形上边缘node.x1—— 矩形右边缘node.y1—— 矩形下边缘
第三步:把节点渲染成 SVG 矩形
布局只负责坐标,画矩形需要 d3-selection 的数据绑定。按 joining 文档 的selectAll().join()模式,先创建 SVG 容器(getting-started 的空白图表示例即d3.create("svg")),再为每个后代节点生成一个rect:
// 创建 SVG 容器(尺寸与上一步 size 一致) const svg = d3.create("svg") .attr("width", width) .attr("height", height); // 为每个节点生成一个矩形 svg.append("g") .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); // 展示:在独立 HTML 页面里把 SVG 挂到容器上 container.append(svg.node());root.descendants()按拓扑序返回"根节点 + 所有后代",每个节点恰好对应一个rect;join("rect")完成数据到元素的绑定。在 Observable 里则不需要container.append,直接把svg.node()从单元格返回即可。
验证布局结果
文档没有给出"成功日志",但给出了可判断的坐标契约:每个节点必须有x0, y0, x1, y1,矩形宽高即x1 - x0与y1 - y0。运行后在浏览器控制台检查:
console.log(root.x0, root.y0, root.x1, root.y1); // 根节点四个坐标 const leaf = root.leaves()[0]; console.log(leaf.data.name, leaf.x0, leaf.y0, leaf.x1, leaf.y1);可核对的点:
- 根节点与每个叶子的
x1 > x0且y1 > y0,宽高为正,画出的矩形不翻转、不为零宽; leaf.data.name应等于你在data里写的名字(如示例中的 "Cain"),说明node.data关联正确;- 视觉上:所有矩形铺满
width × height区域,相邻子块之间有 2 像素缝隙(对应padding(2)),没有任何矩形越出 SVG 边界。
若某个区域缺失或错位,先检查两处最常见的文档明确要求:是否漏了root.sum(...)(treemap 文档将其列为布局前的强制步骤),以及size([width, height])是否设置成了你实际的画布尺寸而不是默认[1, 1]。
调整平铺方式与可选配置
treemap.tile 用于更换递归细分算法。默认的treemapSquarify实现 Bruls 等人的 squarified 算法,目标矩形宽高比默认为黄金比例 φ = (1 + √5) / 2。文档说明该比例只是给算法的提示,不保证每个矩形恰好等于该比例;squarify.ratio(ratio)可以改目标比例(要求 ≥ 1)。其余内置方法的行为:
| 方法 | 文档描述的行为 |
|---|---|
treemapDice | 按子节点数值水平划分,从左边缘x0起依次排布 |
treemapSlice | 按子节点数值垂直划分,从上边缘y0起依次排布 |
treemapSliceDice | 奇数深度委托给 slice,否则委托给 dice,即按深度交替横竖切分 |
treemapBinary | 递归划分成近似平衡的二叉树,宽矩形用水平划分、高矩形用垂直划分 |
treemapResquarify | 与 squarify 类似,但保留上一次treemapResquarify布局中节点相邻关系,适合做 treemap 的形变动画:只改尺寸不改相对位置,代价是后续布局不再是严格 squarify 结果 |
切换方式:
const treemap = d3.treemap() .tile(d3.treemapSliceDice) .size([width, height]) .padding(2);其余可选配置(均为 treemap 文档给出的默认值):
round(true)开启坐标取整,默认false;paddingInner(n)/paddingOuter(n)分别单独控制内边距(分隔相邻子节点)与外边距(分隔节点边缘与其子节点),都默认 0,也可传按节点取值的函数。
完整示例与文档限制
把上面各步拼起来,是一个可整页运行的独立 HTML(ESM CDN 加载,其余按你的数据替换):
<!DOCTYPE html> <div id="container"></div> <script type="module"> import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm"; const width = 640; const height = 400; const data = { name: "Eve", children: [ {name: "Cain", value: 45}, {name: "Seth", children: [{name: "Enos", value: 10}, {name: "Noam", value: 20}]}, {name: "Abel", value: 30}, {name: "Awan", children: [{name: "Enoch", value: 15}]}, {name: "Azura", value: 40} ] }; const root = d3.hierarchy(data); const treemap = d3.treemap() .size([width, height]) .padding(2); root.sum((d) => d.value ?? 0); root.sort((a, b) => b.height - a.height || b.value - a.value); treemap(root); const svg = d3.create("svg") .attr("width", width) .attr("height", height); svg.append("g") .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); container.append(svg.node()); </script>运行后按上一节的验证清单检查。文档明确给出的边界只有两条:布局前必须sum(或count),以及sum的访问函数必须返回非负数。若你的数据是表格而非层级,先走 stratify;若想让节点间留白更精细,再阅读 treemap 文档 中paddingTop/Right/Bottom/Left的单独控制接口。
【免费下载链接】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),仅供参考