用D3制作矩形式树状结构图(Treemapping)并设计动画效果

简介: 矩形式树状结构图一般可以简称为Treemapping。Treemapping的各种制作方法网络上已经流行了许久,但是鲜有人在此之上有创作新意的,我在此基础上制作了一些动画效果供大家参考

矩形式树状结构图一般可以简称为Treemapping,因为做这张图一般默认就是Rectangular图。Treemapping的各种制作方法网络上已经流行了许久,但是鲜有人在此之上有创作新意的,只是用于处理一些分组数据的展示,对于这种图像我有一些独特的动画效果设想,不知道是否有人会喜欢我的创意。

首先这种图像给我们的感受非常直观,相比于那些古板的条形图,折线图,饼图之类的图像,这种图制作不但很简单,标签也可以直接插入到图像之中,不像其他图像因为表达出的单个数据的图形比较窄,一般都要使用角标,放在图像之外,如下图所示:

以这张图为例,每一个数据块,我们都可以放入3个标签

  1. 国家/地区名
  2. 法定假期数量
  3. 带薪年假数量

不需要什么横竖坐标去找到轴线对应的点和数据,就能一目了然的看到每一个数据块完整的样貌,并能简单的理清其中的大小关系,下面先让我们实现上面这个简单的图像,然后再讲解我于此之上进一步的一些动画创意。

起步

本文所提到的是当前最新的版本d3@7x,不需要太复杂的构建,和许多包一样npm install d3安装下来再像下面这样导入

import * as d3 from "d3";

当然也可以像d3官网和他们在npm的README里开头写的那样,直接写进标签里在线引入

<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
...
</script>

这样的方式不要忘了在script上写type="module",毕竟esm不是默认行为

参数设定

//只做个demo直接写body上,用的时候应该把这个当参数传入
const body = d3.select("body");

//在atlashxm.cn和www.shijian.cc/jieri随便找的一些数据
const data = [
    {
    label: "中非共和国", paidVacation: 24, statutoryHoliday: 14 },
    {
    label: "柬埔寨", paidVacation: 18, statutoryHoliday: 14 },
    {
    label: "津巴韦布", paidVacation: 22, statutoryHoliday: 14 },
    {
    label: "巴基斯坦", paidVacation: 14, statutoryHoliday: 10 },
    {
    label: "刚果(布)", paidVacation: 26, statutoryHoliday: 10 },
    {
    label: "哥伦比亚", paidVacation: 15, statutoryHoliday: 18 },
    {
    label: "苏丹", paidVacation: 20, statutoryHoliday: 11 },
    {
    label: "哈萨克斯坦", paidVacation: 24, statutoryHoliday: 12 },
    {
    label: "蒙古", paidVacation: 15, statutoryHoliday: 13 },
    {
    label: "缅甸", paidVacation: 10, statutoryHoliday: 9 },
    {
    label: "印度尼西亚", paidVacation: 12, statutoryHoliday: 15 },
    {
    label: "中国香港", paidVacation: 7, statutoryHoliday: 12 },
    {
    label: "越南", paidVacation: 12, statutoryHoliday: 14 },
    {
    label: "朝鲜", paidVacation: 14, statutoryHoliday: 18 },
    {
    label: "中国", paidVacation: 5, statutoryHoliday: 11 }
];

//尺寸设定
const width = 1500;
const height = 920;

//根节点和数据组的设定
const root = d3.stratify().path(d => d.label)(data);
//这个sum决定最终图像数据块的值,现在把这个值设为法定假与年假的和
root.sum(d => d?.paidVacation + d?.statutoryHoliday);
//这个数据组将经过后面内置函数的运算填充上实际的定位与尺寸
const leaves = root.leaves();
root.sort((a, b) => d3.descending(a.value, b.value));//排序一下

//使用d3内置的treemap函数设定好一些基本的结构
d3.treemap()
    //tile可以切换为treemapBinary、treemapSquarify、treemapSliceDice、treemapSlice、treemapDice
    .tile(d3.treemapSquarify)
    .size([width, height])
    .paddingInner(1)
    .round(true)(root);

//预定义颜色盘,设定4个色彩阶段(可以自由调节),平均分布于一个与数据长度相同的颜色数组中
const color = d3
    .scaleLinear()  
    .domain([0, data.length / 3, data.length / 3, data.length])
    .range(["orange", "limegreen", "skyblue", "#DE2910"]);

最后一个色特意换上咱的中国红,根据schemecolor网站给出的我国国旗色彩值,红色部分即为#DE2910

参数设定好就可以进行下面的效果了,不止是基础的Treemapping,下面所有的方式都会用到这些参数,调整这些参数对后面的效果也都是有效的

无分组的Treemapping

首先就是画基本的svg标签,所有的东西都应该放到这里面去,通用的样式也应该设置在此处

const svg = d3
    .create("svg")
    .attr("width", width)
    .attr("height", height)
    .attr("font-size", 10);

选用10是因为越小的尺寸后面放大时算起来也好算一些1.1就是11,1.2就是12了,这个无所谓的。下一步就是画出数据组:

const node = svg
    .selectAll("g")
    .data(leaves)
    .join("g")
    .attr("transform", d => `translate(${d.x0},${d.y0})`);

每个组包含一个矩阵和里面的若干文字,数据组自然是使用g标签包裹住其中的内容,然后在g标签上用transform属性进行定位,leaves是刚才预定好的数据组,经过了treemap()函数的计算已经有了定位和大小值,现在只需要代入给transform属性即可。

下面便开始绘画矩块:

node.append("rect")
    .attr("fill", (d, i) => color(i))
    .attr("width", d => d.x1 - d.x0)
    .attr("height", d => d.y1 - d.y0);

这个fill值设定颜色很好理解,就是调用参数设定好的预定义颜色盘。高宽要减一下是因为d3没有直接给出大小值,只给出了右下角坐标,所以右下角坐标减去左上角坐标得出大小,设置为高宽即可。

再进行下一步,绘画文字:

const text = node.append("text").attr("x", 3);
text.append("tspan")
    .text(d => d.data.label)
    .attr("y", "1.2em")
    .attr("font-size", "3.5em")
    .attr("font-weight", "bold");
text.append("tspan")
    .text(d => d.data.statutoryHoliday + "法定假期")
    .attr("y", "3.8em")
    .attr("x", "5")
    .attr("font-size", "2.5em")
    .attr("fill-opacity", "0.9");
text.append("tspan")
    .text(d => d.data.paidVacation + "带薪年假")
    .attr("y", "5.2em")
    .attr("x", "5")
    .attr("font-size", "2.5em")
    .attr("fill-opacity", "0.9");

看着这部分内容似乎有点多,其实反而是最简单的部分,就是设置一些文字样式,在text中插入3个tspan,然后进行定位,用x设定边距,用y调节文本之间的间距,此外还有文本大小、加粗、透明等一些属性

最后只需要把整个svg加入到body中即可:

div.append(() => svg.node());

就这么简单,要不了几行代码,因为计算都交给了D3,没有什么复杂的部分,只要设定好位置和样式即可。到这里,你就可以看到最上面那张图的效果。

但是,通常使用一些其他数据的情况下,你很可能遇到一个问题:文本溢出于矩形之外。其实这是一个常见的问题,总会有比较小的块,首先在能调节图片大小的情况下,直接增加svg大小把文本包进去(很多情况下并不方便调这个),其次能缩小文本大小和间距的情况下就压缩这方面,如果再不行,并且感觉溢出的并不大,可以尝试切换tile调整排列效果,上面代码注释处也提到了。

如果连这些都不行,那只有在设定text时进行判断,设定一个临界值,低于这个值,就不显示值,只显示标签,如果标签都无法显示就直接显示为“...”或者空白。

下面就开始说一说我设计的这个动态变化的动画效果。

折叠后弹出

首先直接设定一个矩块的最小值,对于这个图像图,我就设定为300x200的大小,然后再用动画变大,也就是把rect绘图部分改成下面这样:

node.append("rect")
    .attr("fill", (d, i) => color(i))
    .attr("width", 300)
    .attr("height", 200)
    .transition()
    .attr("width", d => d.x1 - d.x0)
    .attr("height", d => d.y1 - d.y0)
    .duration(1800).ease(d3.easeCircle);

这样每一个块就都有了一个滑动的放大效果。然后,再把g标签都定位在左上角,因为要在g标签生成后插入recttext,所以不能一开始就设置动画,否则后面无法append()标签了,所以要分为两个步骤,首先去掉g标签的定位设置:

const node = svg
    .selectAll("g")
    .data(leaves)
    .join("g")

其实就是删掉transform的设置就好了,然后在recttext设置好之后再进行动画定位:

node.transition()
    .attr("transform", d => `translate(${d.x0},${d.y0})`)
    .delay(500)
    .duration(1500)
    .ease(d3.easeCircle);

设置好延时和动画时间、方式就可以看到如下效果了:

这就是折叠后弹出的效果,下面再看一下抽屉滑动效果。

抽屉滑动效果

这个部分其实和上面一样,就是只设g标签的x,让y动态变化就是了,不过如果初始状态如果想要一条直线的话,就要text也跟随伸缩,不然初始化y值太低或者是0的情况下,文字就会溢出来。这时候给text设定scaleY()来实现从压缩到拉升的效果就好了,初始化的时候文本是缩起来的。当然这是下拉,如果是从左往右拉,那就是只设定y,让x动态变化,然后设定textscaleX(),区别不大,这里示例还是下拉的效果,先修改g标签的初始化:

const node = svg
    .selectAll("g")
    .data(leaves)
    .join("g")
    .attr("transform", d => `translate(${
     d.x0},0)`);

由于text后面也需要append()那3行tspan标签,所以也和g标签一样要分开初始化,在子标签append()之后再进行动画,先修改text的初始化:

const text = node
    .append("text")
    .attr("x", 3)
    .style("transform", "scaleY(0.05)");

然后修改rect的初始化,每一块都把height值初始化为10,然后逐步拉升:

node.append("rect")
    .attr("fill", (d, i) => color(i))
    .attr("width", d => d.x1 - d.x0)
    .attr("height", 10)
    .transition()
    .attr("height", d => d.y1 - d.y0)
    .duration(1800)

注意,这时候我们不需要ease(d3.easeCircle)这个弧线缓动的节奏了,抽屉就正常均速平滑即可,不需要花哨。然后在所有的tspan设置完毕之后,插入text的动画,其实非常简单,也是一句话的事情:

text.transition().style("transform", "none").duration(2000);

文本的动画走慢一点,不用太着急,毕竟这部分效果在走到大约80%的时候,就已经可以看了,所以可以稍微拖慢些,也防止文本拉的太快超出了色块。最后就是节点的动画了:

node.transition()
    .attr("transform", d => `translate(${d.x0},${d.y0})`)
    .delay(500)
    .duration(1500)

也是简简单单,设定好最终的数据就行了,我为了录屏delay(500)了一下,这个延迟是否需要看自身的需求即可。

下面就是最终的效果:

可能有人会有疑问本文的treemapping为什么没有group,这当然不是不好实现或者是偷懒,而是我想以一种比较新的方式表现这种图,它不是非要有group才能做,普通的条形图,饼图折线图那样的数据,也可以依照这样的方式进行展示。没有必要有group才想到这个图,它确实比较好看,很直观,标签摆放也方便,后面交互起来也简明,大小也有很大的调整空间,能忍受各种各样的屏幕比例。希望读者在有制图需求时,考虑一下这种有趣的统计图。

本文写作于2023年4月24日发布于lyrieek的掘金,于2023年7月13日进行修订发布于lyrieek的阿里云开发者社区。

目录
相关文章
平面设计实验五 图层及图层混合模式
平面设计实验五 图层及图层混合模式
99 0
|
JSON 前端开发 数据可视化
【图形基础篇】02 # 指令式绘图系统:如何用Canvas绘制层次关系图?
【图形基础篇】02 # 指令式绘图系统:如何用Canvas绘制层次关系图?
204 0
【图形基础篇】02 # 指令式绘图系统:如何用Canvas绘制层次关系图?
|
定位技术
制作地图的布局、元素和设计介绍
制作地图的布局、元素和设计介绍
265 0
制作地图的布局、元素和设计介绍
【D3.js - v5.x】(6)绘制树状图 | 层级布局 | 附完整代码
【D3.js - v5.x】(6)绘制树状图 | 层级布局 | 附完整代码
982 0
【D3.js - v5.x】(6)绘制树状图 | 层级布局 | 附完整代码
|
图形学
Unity中UGUI、粒子系统、Mesh混合使用保证层级正确
把粒子、Mesh渲染到一张RenderTexture上,然后把这张RenderTexture贴到一张RawImage就可以解决这种类似的UI,Mesh,粒子穿插使用的问题。这种方法由于比较麻烦就没有使用。
【ThreeJs】(1)四大组件:场景、相机、物体、渲染器 | 创建一个矩形 | THREE脑图
【ThreeJs】(1)四大组件:场景、相机、物体、渲染器 | 创建一个矩形 | THREE脑图
379 0
【ThreeJs】(1)四大组件:场景、相机、物体、渲染器 | 创建一个矩形 | THREE脑图
|
图形学 容器
UGUI系列-实现层级菜单(Unity3D)
层级菜单在Unity中用到的并不多,主要是做分类的时候用的比较多,今天就给大家分享几个层级代码,扩充一下,写成插件也是不错的。
|
JavaScript 前端开发 容器