聊聊可视化
可视化
开发绝大多数 web 前端应用的时候,我们关心的是文字/多媒体、区块排版、布局、样式和交互,关心的是如何给用户『阅读』体验更好的『网页』。 而开发可视化应用,我们关心的则是数据计算、不止是区块的各种图形的绘图渲染,当然也少不了交互,希望通过一张图让用户看清数据背后隐藏的信息。可视化的本质也就是把数据组织起来(比如从非结构化变成结构化)之后绘图呈现。
可视化开发和渲染基本原理
可视化开发中我们可能会相对少的用到 html 和 css,更多用到 canvas、webgl、svg 之类的图形 api。因为可能要处理视觉细节,很多时候即时用了主流的可视化图或图表库,我们依然需要接触底层渲染层 api 精细控制图形渲染细节。
浏览器如何绘制图形
不管可视化和一般 web 前端开发有多少不同,最终都还是通过浏览器渲染呈现的。浏览器通过自身的渲染引擎绘制图形,主要有两类方式:
- html/svg + css
- canvas(canvas2d/webgl)
html/svg + css
可能提到可视化我们第一反应不会想到 html 和 css (实际中也极少用) ,但这个方式确实适合实现布局简单(区块式)的常规图表可视化。 这种方式主要靠 css 技巧,比如:柱状图可以通过 grid layout + linear-gradient 实现;饼图可以用 conic-gradient 圆锥渐变实现,折线区块图可以用 clip-path 拼多边形实现等等。
.pie-chart {
display: inline-block;
width: 100px;
height: 100px;
border-radius: 50%;
background-image: conic-gradient(
#000 60deg,
#333 60deg,
#555 120deg,
#999 120deg,
red 240deg,
blue 240deg,
#aaa 300deg,
#eee 300deg,
#ccc 200deg
);
}这里还提到了 svg,由于浏览器也支持直接内嵌 svg 标签,支持我们通过 dom api 操作 svg、css 处理 svg,同时提供了很多图形元素标签(html 中需要 css 实现)。所以 svg 本质上是一种有特殊属性的 dom ,也可以实现可视化,本质上跟用 html 实现没有区别。
由于 svg 基于 xml 语法,我们可以比较方便的做视图和数据的解耦:
<rect x="0" y="0" width="20" height="40" fill="#666"/>相比之下 css 至少需要理解一次比例转换:
/**
data = [4, 4, 8]
*/
.bar {
background: linear-gradient(to bottom, yellow 25%, blue 25%, blue 50%, red 50%);
}优点
- 开发方便
缺点
- css 语法难以和数据解耦,难以拓展维护。
- 一个 svg 元素只能表示一种图形,数据量大的时候回产生大量的 svg 元素,耗内存、影响性能。
- 浏览器解析 html/svg 和 css 需要走一遍 构建 dom tree -> renderObject tree -> renderLayer tree 的渲染引擎流程,重渲会带来较大的性能开销(可视化重点在数据计算和绘图渲染)。
canvas(2d/webgl/webgl2)
提到 canvas 我们一般是指代浏览器的 canvas2d 上下文,而准确的说浏览器的 canvas elements 提供了 2d、webgl 和 webgl2 三种我们常用的上下文 api (其实还有 bitmaprenderer, placeholder, 这里不展开,感兴趣的可以看 html spec canvas context mode)。
从 api 规范角度看,canvas api 规范有 canvas2d 规范和 webgl 规范两类,webgl 规范是 opengl es (opengl es 是 opengl api 的子集,针对手机等终端设计)规范在 web 端的实现,webgl2 和 webgl 分别对应 opengl es 3 和 opengl es 2。最新的还有 webgpu,它是最新的 3d 图形 api,相比 webgl 有很大升级和差异,不再基于 opengl 而是 vulkan/direct3d,这里不展开讲。
不同于 html/svg 的声明式语法,我们通过 canvas api 是以指令式的方式绘图。canvas 元素在浏览器上创建画布并提供上下文接口,我们调用 api 绘制点线面登记本图形、设置图形属性、颜色、形状变换,从而完成绘图。
另一点, canvas2d 和 webgl 的绘制最终也都是给到 gpu 渲染 (大部分现代浏览器都会 gpu 加速,dom
渲染最后也是给到 gpu 位图),他们相比 html/svg 方式最大的不同是不存在 dom renderObjectTree -> renderLayer 的流程。
不过 canvas2d 和 webgl 对 gpu 的控制有所不同,我们无法通过 canvas2d 直接操作 gpu,我们只能调用 canvas2d 简陋的 api,浏览器在底层控制 gpu。webgl 预渲染到帧缓存后,再执行 webgl api (webglprogram)操作 shader,因此我们可以在这里控制 gpu 渲染,在 shader 中做更精细化的操作或计算,性能远超 js。尤其是对于图形数量庞大的绘制,我们可以利用 webgl 实例化渲染并让 gpu 并行处理实现批量渲染,而 canvas2d 在浏览器底层并不会主动做这种优化。
canvas 优点
- 指令式语法,开发友好易于拓展。
- 直接操作绘图上下文,没有 dom 渲染带来的性能问题。
canvas 缺点
- canvas api 比较简陋
- canvas 只有一张画布,没有图形元素的概念( html/svg 的 dom 对应某个基本图形元素),导致 canvas 中很难做到局部渲染、局部控制、事件操作等,会有整个画布重绘的问题。
- 绘制图形数量大时也会有性能问题。
webgl 优点
- 绘制图形数量大时、处理像素点多的精细绘制时,可以操作 gpu 调优
- 3d 绘制,内置了 3d 投影和处理。
webgl 缺点
- 设备兼容性问题
- 开发成本

native 如何绘制图形
还有点题外话,我们这里说的是 canvas elements 都是浏览器实现提供的,浏览器提供的东西最重要的就是通用性、跨平台、标准化。浏览器实现 canvas 至少要编译适配几个通用图形库,同时还需要再封装出一套 js api 给开发者用。
所以浏览器上 canvas 的绘制调用流程是:canvas js api -> js引擎 -> 浏览器接口逻辑 -> 图形库 -> gpu 绘制。
比如 webkit 封装了 skia 图形库,chromium 会用 skia 的 software rasterizer 绘制成位图,作为纹理上传到 gpu,或由 skia 的 opengl 直接绘制纹理到 gpu (感兴趣可以看 GPU Accelerated Compositing in Chrome)。
但这一层层的封装避免不了性能消耗,尤其在 js 引擎这一换。而 native 可以直接调用 opengl 或其他底层接口。 所以 浏览器 canvas 的渲染表现会比 native 差很多。当然 native 也不具备浏览器的通用开放跨平台性。
可视化引擎/库需要做什么
可以看到在浏览器中,无论用哪种方式绘图都依然有缺点。首要缺点就是,浏览器是通用平台,通用平台都只会提供基础 api,不会提供跟应用场景强相关的高级 api(所以前面说 canvas api 简陋),而开发者实际中需要的是高级 api,比如 drawGraph、drawshape 之类的。这种高级 api 需要可视化引擎/库去实现。
因此最常见的是这种提供丰富绘制 api 的图表库,比如 echarts、g2;以及一些专业的图绘制,比如画地图地理的 mapbox、L7;还有一类无关应用场景、更灵活的绘制图形或物理模型,2d 的 spritejs、3d 的 threejs。让人不用通过 canvas2d/webgl 等底层 api 从基础图形画起。
前面提到可视化除了绘图之外的另一个关键是数据组织,所以还有一类专门用于数据处理、结构转换的框架,比如 d3.js。
事实上绝大多数可视化绘图(由于 2d 场景较多)都是基 canvas2d(3d 场景肯定是用 webgl)。
canvas2d 的两大缺点:
- 一绘制图形数量大时会有性能问题,我们前面提了可以用过
webgl并行批量渲染优化; - 另一大缺点难以局部渲染、局部控制、存在画布重绘,这些问题就需要引擎/库在内部重点处理了。
延伸阅读
我后来把这些 Canvas 与可视化思考用在了城市地图产品中,具体记录在《从 OpenStreetMap 到可编辑城市地图:我如何做 EveryCityMap》里。
CURRENTLY BUILDING
EveryCityMap
Style, share, and export custom maps for 500 cities. Choose a theme, edit the layers, and download a 1440px WebP without signing up.