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

简单来说,可视化就是将数据信息组织起来后,以图形的形式展示出来。
在 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 打交道,真正控制图形输出的每一个细节。

图形是如何绘制的

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

  • 光栅 指构成图像的像素阵列。
  • 像素 一个像素对应图像上的一个点,通常保存图像上的某个具体位置的颜色等信息。
  • 帧缓存 是一块内存地址。在绘图过程中,像素信息被存放于帧缓存中。
  • CPU 中央处理单元,负责逻辑计算。
  • GPU 图形处理单元,负责图形计算。

数据经过 CPU 处理,成为具有特定结构的几何信息。然后,这些信息会被送到 GPU 中进行处理,在 GPU 中经过两个步骤生成光栅信息,这些光栅信息会输出到帧缓存中,最后渲染到屏幕上。

一 创建 webGL 上下文

创建 webGL 上下文这一步和 Canvas2D 基本一样。

1
2
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('2d');
二 创建 WebGL 程序

要创建 WebGL 程序,需要先编写两个着色器。 在绘图的时候,WebGL 是以顶点图元来描述图形几何信息的。顶点就是几何图形的顶点。图元是 WebGL 可直接处理的图形单元,由 WebGL 的绘图模式决定。顶点着色器负责处理图形的顶点信息;片元着色器负责处理图形的像素信息。我们可以把顶点着色器理解为处理顶点的 GPU 程序代码。它可以改变顶点的信息(如顶点的坐标、法线方向、材质等等)。顶点处理完成后,WebGL 就会根据顶点和绘图模式指定的图元,计算出需要着色的像素点,然后对他们执行片元着色器程序(对指定图元中的像素点着色)。

WebGL 从顶点着色器和图元提取像素点给片元着色器执行代码的过程,就是生成光栅信息的过程,也叫做光栅化过程。所以片元着色器的作用就是处理光栅化后的像素信息