浏览器中实现可视化的四种方式

简单来说,可视化就是将数据信息组织起来后,以图形的形式展示出来。
在web上,图形通常是通过浏览器来绘制的。其中负责绘制图形的部分是渲染引擎。渲染引擎绘制图形的方式,大体上有以下4种

HTML+CSS

使用html + css 可以实现常规的图标展示。

柱状图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* dataset = { current: [15, 11, 17, 25, 37], total: [25, 26, 40, 45, 68], } */
.bargraph {
display: grid;
width: 150px;
height: 100px;
padding: 10px;
transform: scaleY(3);
grid-template-columns: repeat(5, 20%);
}

.bargraph div {
margin: 0 2px;
}

.bargraph div:nth-child(1) {
background: linear-gradient(to bottom, transparent 75%, #37c 0, #37c 85%, #3c7 0);
}

.bargraph div:nth-child(2) {
background: linear-gradient(to bottom, transparent 74%, #37c 0, #37c 89%, #3c7 0);
}

.bargraph div:nth-child(3) {
background: linear-gradient(to bottom, transparent 60%, #37c 0, #37c 83%, #3c7 0);
}

.bargraph div:nth-child(4) {
background: linear-gradient(to bottom, transparent 55%, #37c 0, #37c 75%, #3c7 0);
}

.bargraph div:nth-child(5) {
background: linear-gradient(to bottom, transparent 32%, #37c 0, #37c 63%, #3c7 0);
}

效果如下图:
html-bar

饼图

1
2
3
4
5
6
7
.piegraph {
display: inline-block;
width: 250px;
height: 250px;
border-radius: 50%;
background-image: conic-gradient(#37c 30deg, #3c7 30deg, #3c7 65deg, orange 65deg, orange 110deg, #f73 110deg, #f73 200deg, #ccc 200deg); // 锥形渐变
}

效果如下图:
html-bar

优缺点

  • 简化开发,不需要引入额外的库,节省资源,提高网页打开的速度。
  • html + css主要还是为了用于网页布局,虽然可以绘制可视化图表,但绘制的方法并不简洁。从css中很难看出图形与数据之间的关系,并且换算也需要developer自己来做,数据一旦发生变化,就需要重新计算生成。维护成本较高。
  • 其次,开销较大。html+css 是浏览器渲染引擎的一部分,浏览器的渲染引擎在工作时,要先解析html、css绘制dom树,cssom树,render树等等,当用html绘图时,一旦图形发生变化,就要引发浏览器的重绘。

Canvas 2D

Canvas2D 是浏览器提供的简便快捷的指令式图形系统,它通过一些简单的指令就能快速绘制出复杂的图形。

MDN 使用教程链接:canvas 教程与指导

canvas元素和2d上下文

canvas元素本身的width和height,不等同于canvas元素css样式的宽高属性。

css宽高决定canvas页面呈现的大小,而canvas元素宽高决定了canvas的坐标系,决定可视区域的坐标范围。为了区分它们,我们称canvas元素属性宽高为画布宽高,css样式宽高为样式宽高

在实际绘制的过程中,如果不设置样式宽高,只设置了画布宽高,那么canvas的样式宽高就会等同于画布宽高。
如果不设置画布宽高,只设置了样式宽高,那么画布宽高将等同于样式宽高的二倍。

canvas操作步骤

此处不会赘述canvas的各个api,只是做于简单说明。

  1. 获取 Canvas 对象,通过 getContext(‘2d’) 得到 2D 上下文;
  2. 设置绘图状态,比如填充颜色 fillStyle,平移变换 translate 等等;
  3. 调用 beginPath 指令开始绘制图形;
  4. 调用绘图指令,比如 rect,表示绘制矩形;
  5. 调用 fill 指令,将绘制内容真正输出到画布上。

使用canvas绘制层次关系图

层次结构数据

用来表示能够体现层次结构的信息,例如城市与省与国家。一般来说,层次结构数据用层次关系图表来呈现。

城市层级示例图

json数据格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"name": "中国",
"children":
[
{
"name": "浙江",
"children":
[
{ "name": "杭州" },
{ "name": "宁波" },
{ "name": "温州" },
{ "name": "绍兴" }
]
},
{
"name": "河南",
"children":
[
{ "name": "濮阳" },
{ "name": "洛阳" },
{ "name": "南阳" },
{ "name": "安阳" },
{ "name": "信阳" },
]
},
{
"name": "广西",
"children":
[
{ "name": "桂林" },
{ "name": "南宁" },
]
}
]
}

假设我们想要实现的层级关系图效果如下:

城市层级关系图

数据中只有”城市>省份>中国”这样的层级数据,我们需要把数据层级、位置和要绘制的半径、位置一一对应起来。

换句话说,就是需要数学计算。不过,我们可以直接使用 d3-hierarchy这个工具库转换数据。

1
2
3
4
5
6
const regions = d3.hierarchy(cityData)
.sum(d => 1) //
const pack = d3.pack()
.size([1000, 1000])
.padding(3);
const root = pack(regions);

使用d3.hierarchy进行数据转换。将数据映射到一个1000 * 1000的画布上,每个相邻圆之间间隔3px。拿到数据之后,只需要遍历数据并且根据数据内容绘制圆弧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
const TAU = 2 * Math.PI;

function 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, TAU);
ctx.fill();
if(children) {
for(let i = 0; i < children.length; i++) {
draw(context, children[i]);
}
} else {
const name = node.data.name;
ctx.fillStyle = textColor;
ctx.font = '1.5rem Arial';
ctx.textAlign = 'center';
ctx.fillText(name, x, y);
}
}

draw(context, root);

首先使用arc 指令(api)在当前节点绘制一个圆,arc 方法的五个参数分别是圆心的 x、y 坐标、半径 r、起始角度和结束角度,前三个参数就是数据中的 x、y 和 r。因为我们要绘制的是整圆,所以后面的两个参数中起始角是 0,结束角是 2π。

绘制成图后,如果当前数据有下一级的数据,则遍历它的下一级数据,递归的调用绘图过程。如果没有下一级,则说明当前数据为城市数据(最小单元数据),通过fillText指令直接给出当前城市的名字。

优缺点

  • canvas能够直接操作绘图上下文,不需要html,css解析、渲染、布局等一系列操作。
  • 不容易添加操作事件(如click事件)

SVG

svg,可缩放矢量图。是一种基于 XML 语法的图像格式,可以用图片(img 元素)的src属性加载。

svg MDN参考文档地址

实现柱状图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- dataset = { total: [25, 26, 40, 45, 68], current: [15, 11, 17, 25, 37], } -->
<svg xmlns="http://www.w3.org/2000/svg" width="120px" height="240px" viewBox="0 0 60 100">
<g transform="translate(0, 100) scale(1, -1)">
<g>
<rect x="1" y="0" width="10" height="25" fill="#37c" />
<rect x="13" y="0" width="10" height="26" fill="#37c" />
<rect x="25" y="0" width="10" height="40" fill="#37c" />
<rect x="37" y="0" width="10" height="45" fill="#37c" />
<rect x="49" y="0" width="10" height="68" fill="#37c" />
</g>
<g>
<rect x="1" y="0" width="10" height="15" fill="#3c7" />
<rect x="13" y="0" width="10" height="11" fill="#3c7" />
<rect x="25" y="0" width="10" height="17" fill="#3c7" />
<rect x="37" y="0" width="10" height="25" fill="#3c7" />
<rect x="49" y="0" width="10" height="37" fill="#3c7" />
</g>
</g>
</svg>
<!-- total 和current中的每一项分别对应svg g中的height-->

绘制层次关系图

以canvas绘制的城市分级为例。数据同样需要经过d3.hierarchy进行转换。

转换完成之后获取当前的svg节点。同样实现draw方法从root开始遍历数据。不同于canvas的调用绘图指令(api)来绘图,svg是通过创建svg元素,将元素添加到DOM中,来使得图像显现出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const svgroot = document.querySelector('svg');

function draw(parent, node, {fillStyle = 'rgba(0, 0, 0, 0.2)', textColor = 'white'} = {}) {
const children = node.children;
const {x, y, r} = node;
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', r);
circle.setAttribute('fill', fillStyle);
circle.setAttribute('data-name', node.data.name);
parent.appendChild(circle);

...
}

draw(svgroot, root);

使用Document.createElementNS来创建一个具有指定命名空间(arg1)和限定名称(arg2)的元素。

因为要绘制圆形,所以创建一个circle元素,指定x,y,r 分别为圆的cx(中心点x),cy(中心点y),cr(半径);fillStyle赋值给fill属性。然后将circle元素添加到他的parent里。

1
2
3
4
5
6
7
8
if(children) {
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
for(let i = 0; i < children.length; i++) {
draw(group, children[i], {fillStyle, textColor});
}
group.setAttribute('data-name', node.data.name);
parent.appendChild(group);
}

svg的g代表一个分组,可以考虑用它(g)来建立一个层级结构,且g元素的属性,其子元素也可继承。

如果有子节点,则接着遍历下一层数据,直到数据最小单元(没有下一级的数据了)

1
2
3
4
5
6
7
8
9
10
11
12
else {
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('fill', textColor);
text.setAttribute('font-family', 'Arial');
text.setAttribute('font-size', '1.5rem');
text.setAttribute('text-anchor', 'middle');
text.setAttribute('x', x);
text.setAttribute('y', y);
const name = node.data.name;
text.textContent = name;
parent.appendChild(text);
}

当没有下一级数据时,就需要为其添加text文字元素了,然后设置元素的属性,添加到父节点。

优缺点

  • 相较于html + css ,弥补了html绘制不规则图形的能力,使用svg实现不规则形状图形要简单的多。
  • 适用于元素较少的简单场景。同样需要经过浏览器渲染引擎的一系列操作。如果数据很复杂,同样会开销很多的内存空间。

svg与canvas

  • 写法不同
    svg是以创建图形元素绘图的”声明式”的绘图系统,Canvas 是执行绘图指令绘图的“指令式”绘图系统。
  • 交互实现不同
    svg的交互方式与dom操作大体相同。如(addEventListner)而canvas,则需要用到复杂的数学计算。
  • svg绘制大量几何图形会极大的增大浏览器的重绘和重排。

WebGL

webGL比上述三种方式要复杂一些,因为 WebGL 是基于 OpenGL ES 规范的浏览器实现的,API 相对更底层,使用起来不如前三种那么简单直接。要使用webGL绘图,我们必须要深入细节里。换句话说就是,我们必须要和内存、GPU 打交道,真正控制图形输出的每一个细节。

图形是如何绘制的

首先说一下计算机图形系统的主要组成部分,以及他们在绘图过程中的作用。