聊聊可视化

可视化

开发绝大多数 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 拼多边形实现等等。

CSS
.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 语法,我们可以比较方便的做视图和数据的解耦:

HTML, XML
<rect x="0" y="0" width="20" height="40" fill="#666"/>

相比之下 css 至少需要理解一次比例转换:

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 esopengl esopengl api 的子集,针对手机等终端设计)规范在 web 端的实现,webgl2webgl 分别对应 opengl es 3opengl 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 缺点

    • 设备兼容性问题
    • 开发成本

visual-0

native 如何绘制图形

还有点题外话,我们这里说的是 canvas elements 都是浏览器实现提供的,浏览器提供的东西最重要的就是通用性、跨平台、标准化。浏览器实现 canvas 至少要编译适配几个通用图形库,同时还需要再封装出一套 js api 给开发者用。

所以浏览器上 canvas 的绘制调用流程是:canvas js api -> js引擎 -> 浏览器接口逻辑 -> 图形库 -> gpu 绘制

比如 webkit 封装了 skia 图形库,chromium 会用 skiasoftware rasterizer 绘制成位图,作为纹理上传到 gpu,或由 skia 的 opengl 直接绘制纹理到 gpu (感兴趣可以看 GPU Accelerated Compositing in Chrome)。

但这一层层的封装避免不了性能消耗,尤其在 js 引擎这一换。而 native 可以直接调用opengl 或其他底层接口。 所以 浏览器 canvas 的渲染表现会比 native 差很多。当然 native 也不具备浏览器的通用开放跨平台性。

可视化引擎/库需要做什么

可以看到在浏览器中,无论用哪种方式绘图都依然有缺点。首要缺点就是,浏览器是通用平台,通用平台都只会提供基础 api,不会提供跟应用场景强相关的高级 api(所以前面说 canvas api 简陋),而开发者实际中需要的是高级 api,比如 drawGraphdrawshape 之类的。这种高级 api 需要可视化引擎/库去实现。

因此最常见的是这种提供丰富绘制 api 的图表库,比如 echarts、g2;以及一些专业的图绘制,比如画地图地理的 mapbox、L7;还有一类无关应用场景、更灵活的绘制图形或物理模型,2d 的 spritejs、3d 的 threejs。让人不用通过 canvas2d/webgl 等底层 api 从基础图形画起。

前面提到可视化除了绘图之外的另一个关键是数据组织,所以还有一类专门用于数据处理、结构转换的框架,比如 d3.js。

事实上绝大多数可视化绘图(由于2d场景较多)都是基 canvas2d(3d场景肯定是用 webgl)。 canvas2d 的两大缺点:

  • 一绘制图形数量大时会有性能问题,我们前面提了可以用过 webgl 并行批量渲染优化;
  • 另一大缺点难以局部渲染、局部控制、存在画布重绘,这些问题就需要引擎/库在内部重点处理了。