利用Canvas绘制层次关系图
一、Canvas简介
Canvas API 提供了一个通过JavaScript 和 HTML的canvas元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。
二、Canvas元素与2D上下文
1、canvas元素
对于浏览器而言,canvas也是HTML元素,可以利用canvas标签将其插入HTML内容中,例如:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas width="512" height="512"></canvas>
</body>
</html>

其中关于canvas标签,只有两个属性:width与height,这决定了canvas的画布宽高 ,与CSS的属性中设置的样式宽高决定这canvas中画布的坐标系。
如果不设置canvas元素的样式宽高,样式宽高的像素值就是画布宽高值。
如果样式宽高设置小于画布宽高,canvas的坐标系会发生什么变化?
画布宽高决定了可视区域的坐标范围,所以canvas将画布宽高与样式宽高分开的原因是能方便适配不同的显示设备。
三、canvas坐标系
默认左上角为坐标原点,X轴水平向右,Y轴垂直向下。
四、canvas绘制几何图形
1、插入canvas标签
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas width="512" height="512"></canvas>
</body>
</html>
2、获取canvas上下文
获取canvas元素
js
const canvas =document.querySelector('canvas');
获取上下文
js
const ctx = canvas.getContext('2d');
3、绘制图形
例如中心绘制一个100*100的正方形,并填充为红色对应的指令代码是:
js
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const rectSize = [100, 100];
ctx.fillStyle = "red";
ctx.beginPath();
ctx.rect(
0.5 * (canvas.width - rectSize[0]),
0.5 * (canvas.height - rectSize[0]),
...rectSize
);
ctx.fill();

除了在计算的时候改变顶点坐标,我们还可以对图形进行平移变换。
js
ctx.fillStyle = "blue";
ctx.beginPath();
ctx.translate(-25, -25);
ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, 50, 50);
ctx.fill();
对比改变图形的顶点坐标与对图形进行平移变换,这两者各有优缺点。
-
更改顶点坐标:
-
优点:直接,便捷;
-
缺点:对于不规则多边形,顶点计算会很麻烦;
-
-
平移变换:
- 优点:整体平移画布,不需要改变顶点状态;
- 缺点:画布状态改变了,会影响后续图形绘制的画布的起始状态。
对于平移操作,如果需要进行后续绘制,那么需要将画布状态恢复,恢复的方式有两种:
-
canvas上下文提供的save和restore方法:
jsconst canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); const rectSize = [100, 100]; ctx.fillStyle = "red"; ctx.beginPath(); ctx.save();//保存状态 ctx.translate(-50, -50); ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, ...rectSize); ctx.restore();//恢复状态 ctx.fill(); ctx.fillStyle = "blue"; ctx.beginPath(); ctx.save(); ctx.translate(-25, -25); ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, 50, 50); ctx.fill(); ctx.restore();
-
反向平移回去
jsconst canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); const rectSize = [100, 100]; ctx.fillStyle = "red"; ctx.beginPath(); ctx.translate(-50, -50);//第一次平移 ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, ...rectSize); ctx.fill(); ctx.translate(50,50);//反向平移恢复画布初始状态 ctx.fillStyle = "blue"; ctx.beginPath(); ctx.translate(-25, -25);//在画布初始状态下再平移 ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, 50, 50); ctx.fill();

不进行任何操作,只在初始状态进行绘制:
js
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const rectSize = [100, 100];
ctx.fillStyle = "red";
ctx.beginPath();
ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, ...rectSize);
ctx.fill();
ctx.fillStyle = "blue";
ctx.beginPath();
ctx.rect(0.5 * canvas.width, 0.5 * canvas.height, 50, 50);
ctx.fill();

五、canvas绘制层级关系图

绘制的图形对应的数据如下:
json
export default {
"name": "中国",
"children": [
{
"name": "浙江",
"children": [
{
"name": "杭州"
},
{
"name": "宁波"
},
{
"name": "温州"
},
{
"name": "绍兴"
}
]
},
{
"name": "广西",
"children": [
{
"name": "桂林"
},
{
"name": "南宁"
},
{
"name": "柳州"
},
{
"name": "防城港"
}
]
},
{
"name": "黑龙江",
"children": [
{
"name": "哈尔滨"
},
{
"name": "齐齐哈尔"
},
{
"name": "牡丹江"
},
{
"name": "大庆"
}
]
},
{
"name": "新疆",
"children": [
{
"name": "乌鲁木齐"
},
{
"name": "克拉玛依"
},
{
"name": "吐鲁番"
},
{
"name": "哈密"
}
]
},
{
"name": "河北",
"children": [
{
"name": "石家庄"
},
{
"name": "唐山"
},
{
"name": "邯郸"
},
{
"name": "秦皇岛"
}
]
},
{
"name": "西藏",
"children": [
{
"name": "拉萨"
},
{
"name": "昌都"
},
{
"name": "林芝"
}
]
},
{
"name": "江苏",
"children": [
{
"name": "南京"
},
{
"name": "无锡"
},
{
"name": "徐州"
},
{
"name": "常州"
},
{
"name": "连云港"
},
{
"name": "淮安"
}
]
},
{
"name": "江苏",
"children": [
{
"name": "南京"
},
{
"name": "无锡"
},
{
"name": "徐州"
},
{
"name": "常州"
},
{
"name": "连云港"
},
{
"name": "淮安"
}
]
},
{
"name": "湖南",
"children": [
{
"name": "长沙"
},
{
"name": "株洲"
},
{
"name": "湘潭"
},
{
"name": "衡阳"
},
{
"name": "邵阳"
},
{
"name": "岳阳"
}
]
},
{
"name": "海南",
"children": [
{
"name": "海口"
},
{
"name": "三亚"
},
{
"name": "三沙"
}
]
},
{
"name": "陕西",
"children": [
{
"name": "西安"
},
{
"name": "咸阳"
},
{
"name": "汉中"
},
{
"name": "安康"
},
{
"name": "榆林"
},
{
"name": "延安"
}
]
},
{
"name": "甘肃",
"children": [
{
"name": "兰州"
},
{
"name": "酒泉"
},
{
"name": "金昌"
},
{
"name": "天水"
},
{
"name": "嘉峪关"
},
{
"name": "武威"
}
]
}
]
}
我们需要把数据的层级、位置和圆的半径、位置对应起来。
使用d3-hierarchy
工具将上述json转换为下面的效果:
js
{
data: {name: '中国', children: [...]},
children: [
{
data: {name: '江苏', children: [...]},
value: 7,
r: 186.00172579386546,
x: 586.5048250548921,
y: 748.2441892254667,
}
...
],
value: 69,
x: 800,
y: 800,
r: 800,
}
完整代码如下,实现逻辑注释标明:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<canvas width="1600" height="1600"></canvas>
<script src="https://d3js.org/d3-hierarchy.v1.min.js"></script>
</body>
<script type="module">
import dataSource from "./data.js";
// 获取节点数量
const regions = d3
.hierarchy(dataSource)
.sum((d) => 1)
.sort((a, b) => b.value - a.value);
// 设置节点大小
const pack = d3.pack().size([1600, 1600]).padding(3);
// 获取根节点
const root = pack(regions);
console.log(root);
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
// 绘制节点
const draw = (
ctx,
node,
{ fillStyle = "rgba(0, 0, 0, 0.2)", textColor = "white" } = {}
) => {
// 获取子节点
const children = node.children;
// 获取节点的位置和半径
const { x, y, r } = node;
// 设置填充颜色
ctx.fillStyle = fillStyle;
// 开始绘制圆
ctx.beginPath();
// 绘制圆弧
ctx.arc(x, y, r, 0, 2 * Math.PI);
// 填充圆弧
ctx.fill();
// 如果有子节点,则绘制子节点
if (children) {
for (let i = 0; i < children.length; i++) {
draw(ctx, children[i]);
}
// 如果没有子节点,则绘制文本
} else {
// 设置文本颜色
ctx.fillStyle = textColor;
// 设置字体
ctx.font = "12px Arial";
// 设置文本对齐方式
ctx.textAlign = "center";
// 绘制文本
ctx.fillText(node.data.name, x, y);
}
};
// 绘制根节点
draw(ctx, root);
</script>
</html>