news 2026/9/13 16:07:25

如何用 d3.treemap 把层级数据渲染为矩形布局?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何用 d3.treemap 把层级数据渲染为矩形布局?

如何用 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 根节点,因此数据必须满足两点:

  1. 数据本身是层级结构。文档给出的标准形态是一个根对象,其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

  1. 每个参与面积计算的节点要有非负数值。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.datanode.depthnode.heightnode.parentnode.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。因此要得到实际尺寸的布局,必须先设置sizehierarchy文档给出的官方示例写法是:

// Construct the treemap layout. const treemap = d3.treemap(); treemap.size([width, height]); treemap.padding(2);

其中widthheight是画布尺寸,取你自己的值;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()按拓扑序返回"根节点 + 所有后代",每个节点恰好对应一个rectjoin("rect")完成数据到元素的绑定。在 Observable 里则不需要container.append,直接把svg.node()从单元格返回即可。

验证布局结果

文档没有给出"成功日志",但给出了可判断的坐标契约:每个节点必须有x0, y0, x1, y1,矩形宽高即x1 - x0y1 - 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 > x0y1 > 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 16:06:03

四件套搭建爬虫数据可视化大屏:Scrapy、MySQL、Flask与ECharts实战

简介&#xff1a;这是一套基于Echarts、Flask、Scrapy和MySQL的Python爬虫与数据可视化大屏设计源码&#xff0c;面向希望构建完整数据采集-存储-展示链路的数据分析开发者&#xff0c;适合用于学习爬虫框架、Web后端和前端图表的组合实践。压缩包共61个文件&#xff0c;约6.05…

作者头像 李华
网站建设 2026/9/13 16:05:26

手写LRU缓存:哈希表+双向链表核心实现与面试考点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 16:04:12

R绘制全球样点分布图:底图选型、投影转换与论文级美化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 16:02:55

如何用 Turso 吞吐基准测试测量并发写入事务的每秒事务数?

如何用 Turso 吞吐基准测试测量并发写入事务的每秒事务数&#xff1f; 【免费下载链接】turso A SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases. 项目地址: https://gitcode.com/GitHub_Trending/tu/turso …

作者头像 李华