简单来说,可视化就是将数据信息组织起来后,以图形的形式展示出来。
在 web 上,图形通常是通过浏览器来绘制的。其中负责绘制图形的部分是渲染引擎。渲染引擎绘制图形的方式,大体上有以下4种
。
HTML+CSS
使用 html + css 可以实现常规的图标展示。
柱状图
1 | /* dataset = { current: [15, 11, 17, 25, 37], total: [25, 26, 40, 45, 68], } */ |
效果如下图:
饼图
1 | .piegraph { |
效果如下图:
优缺点
- 简化开发,不需要引入额外的库,节省资源,提高网页打开的速度。
- 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,只是做于简单说明。
- 获取 Canvas 对象,通过 getContext(‘2d’) 得到 2D 上下文;
- 设置绘图状态,比如填充颜色 fillStyle,平移变换 translate 等等;
- 调用 beginPath 指令开始绘制图形;
- 调用绘图指令,比如 rect,表示绘制矩形;
- 调用 fill 指令,将绘制内容真正输出到画布上。
使用 canvas 绘制层次关系图
层次结构数据
用来表示能够体现层次结构的信息,例如城市与省与国家。一般来说,层次结构数据用层次关系图表来呈现。
城市层级示例图
json 数据格式如下:
1 | { |
假设我们想要实现的层级关系图效果如下:
数据中只有”城市>省份>中国”这样的层级数据,我们需要把数据层级、位置和要绘制的半径、位置一一对应起来。
换句话说,就是需要数学计算。不过,我们可以直接使用 d3-hierarchy这个工具库转换数据。
1 | const regions = d3.hierarchy(cityData) |
使用 d3.hierarchy 进行数据转换。将数据映射到一个 1000 * 1000 的画布上,每个相邻圆之间间隔 3px。拿到数据之后,只需要遍历数据并且根据数据内容绘制圆弧。
1 | const canvas = document.querySelector('canvas'); |
首先使用 arc 指令(api)在当前节点绘制一个圆,arc 方法的五个参数分别是圆心的 x、y 坐标、半径 r、起始角度和结束角度,前三个参数就是数据中的 x、y 和 r。因为我们要绘制的是整圆,所以后面的两个参数中起始角是 0,结束角是 2π。
绘制成图后,如果当前数据有下一级的数据,则遍历它的下一级数据,递归的调用绘图过程。如果没有下一级,则说明当前数据为城市数据(最小单元数据),通过 fillText 指令直接给出当前城市的名字。
优缺点
- canvas 能够直接操作绘图上下文,不需要 html,css 解析、渲染、布局等一系列操作。
- 不容易添加操作事件(如 click 事件)
SVG
svg,可缩放矢量图。是一种基于 XML 语法的图像格式,可以用图片(img 元素)的 src 属性加载。
svg MDN 参考文档地址
实现柱状图
1 | <!-- dataset = { total: [25, 26, 40, 45, 68], current: [15, 11, 17, 25, 37], } --> |
绘制层次关系图
以 canvas 绘制的城市分级为例。数据同样需要经过d3.hierarchy
进行转换。
转换完成之后获取当前的 svg 节点。同样实现 draw 方法从 root 开始遍历数据。不同于 canvas 的调用绘图指令(api)来绘图,svg 是通过创建 svg 元素,将元素添加到 DOM 中,来使得图像显现出来。
1 | const svgroot = document.querySelector('svg'); |
使用Document.createElementNS来创建一个具有指定命名空间(arg1)和限定名称(arg2)的元素。
因为要绘制圆形,所以创建一个 circle 元素,指定 x,y,r 分别为圆的 cx(中心点 x),cy(中心点 y),cr(半径);fillStyle 赋值给 fill 属性。然后将 circle 元素添加到他的 parent 里。
1 | if(children) { |
svg 的 g 代表一个分组,可以考虑用它(g)来建立一个层级结构,且 g 元素的属性,其子元素也可继承。
如果有子节点,则接着遍历下一层数据,直到数据最小单元(没有下一级的数据了)
1 | else { |
当没有下一级数据时,就需要为其添加 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 | const canvas = document.querySelector('canvas'); |
二 创建 WebGL 程序
要创建 WebGL 程序,需要先编写两个着色器。 在绘图的时候,WebGL 是以顶点和图元来描述图形几何信息的。顶点就是几何图形的顶点。图元是 WebGL 可直接处理的图形单元,由 WebGL 的绘图模式决定。顶点着色器负责处理图形的顶点信息;片元着色器负责处理图形的像素信息。我们可以把顶点着色器理解为处理顶点的 GPU 程序代码。它可以改变顶点的信息(如顶点的坐标、法线方向、材质等等)。顶点处理完成后,WebGL 就会根据顶点和绘图模式指定的图元,计算出需要着色的像素点,然后对他们执行片元着色器程序(对指定图元中的像素点着色)。
WebGL 从顶点着色器和图元提取像素点给片元着色器执行代码的过程,就是生成光栅信息的过程,也叫做光栅化过程。所以片元着色器的作用就是处理光栅化后的像素信息。