cover

本文是对在组里做的技术分享的脱敏版本,移除了公司内技术架构相关内容,保留了开源部分

Overview

数据报表在B端业务中,是一个非常基础常用的功能模块,对数据可视化进行进一步的学习可以帮助我们保障海量数据渲染性能、代码可维护性的同时,对数据报表的业务场景有更进一步的认识

  • 数据可视化的定义与特点
  • Web 前端数据可视化的实现方式
  • 图形语法
  • ECharts/HighCharts/Antv G2/D3 这些开源热门工具是做什么的,解决了什么问题
  • 了解一下开源工具的实现思路:ECharts 架构实现

数据可视化是什么

Data visualization wikipedia is an interdisciplinary field that deals with the graphic representation of data. It is a particularly efficient way of communicating when the data is numerous as for example a time series.

可视化是将数据组织成易于为人所理解和认知的结构,然后用图形的方式形象地呈现出来的理论、方法和技术。数据可视化是一个交叉领域,对于 Web 前端数据可视化而言,有三个关键要素,数据、图形与交互

只陈列数据,我们很难直观了解到数据中隐藏着趋势或者模式,可视化的图形和图像能够帮我们发现、分析这些隐藏信息,从另一个角度来看,数据可视化也是一种表达、传递信息的方式,选择恰当的展现形式可以帮助我们更好地传达信息

举例:陈列表达 VS 可视表达(南丁格尔玫瑰图)




优秀的可视化案例

哈佛大学:全球经济复杂度

更多优秀案例可参考:
https://www.visualcapitalist.com/

在数据可视化话题中,前端开发同学可以认为是关键玩家:

  • 如何处理大量、海量数据的 format、丝滑渲染与流畅交互
  • 如何处理可视化逻辑,维护可视化相关代码,而不是查一下 bar chart 的文档,搬来官网 demo 改改能用就行,项目里充斥着每个图表的一次性代码
  • 如何基于业务场景选择、实现合适的形式和图表,这是前端同学需要主动思考来助力业务的一个场景

可视化基础与图形语法

四种可视化实现方式

  • HTML/CSS
    HTML/CSS 当然也是一种实现方式,并且方便,不需要第三方依赖,如果我们仅需要绘制少量常见的图表其实是可以考虑由 HTML/CSS 来实现。缺点是 CSS 属性不能直观体现数据,绘制起来也比较麻烦,并且当图形发生变化时,可能要重新执行浏览器渲染的全过程,这样的性能开销非常大
  • SVG
    是对 HTML/CSS 的增强,弥补了 HTML 绘制不规则图形的能力。它通过属性设置图形,可以直观地体现数据,使用起来非常方便。但是 SVG 也有和 HTML/CSS 同样的问题,图形复杂时需要的 SVG 元素太多,也非常消耗性能
  • Canvas2D
    Canvas2D 是浏览器提供的简便快捷的指令式图形系统,它通过一些简单的指令就能快速绘制出复杂的图形。由于它直接操作绘图上下文,因此没有 HTML/CSS 和 SVG 绘图因为元素多导致消耗性能的问题,性能要比前两者快得多。但是如果要绘制的图形太多,或者处理大量的像素计算时,Canvas2D 依然会遇到性能瓶颈,且交互处理比较难,需要经过数学计算,定位的方式来获取局部图形的操作事件
  • WebGL
    使用复杂,但是功能强大,能够充分利用 GPU 并行计算的能力,来快速、精准地操作图像的像素,在同一时间完成数十万或数百万次计算。除此之外还内置了对 3D 物体的投影、深度检测等处理,这让它更适合绘制 3D 场景

举个例子:选择恰当的实现方式的重要性
在2020 美国总统大选结果披露过程中,各家媒体都做了类似的各州选票披露展示:

除了纽约时报 Nytimes 选择了用 Canvas 绘制之外,各家都选择了用 SVG 来实现,对比体验差异也是非常明显的,在这种固定时间内用户频繁刷新查看数据且图形无变化,只是进行实时的选票数字与颜色披露,SVG 无疑是非常适合的场景,简单方便。而 Nytimes 的选择则有些笨重,用户反映「卡,电脑都烫」

那些业界热门可视化工具都是干什么的

  • 图表库
    • ECharts
    • Chart.js
  • GIS 地图库
  • 渲染库
    • Three.js
  • 数据驱动框架
    • D3 Data Driven Document D3 is not a Data Visualization Library 使用绘图指令对数据进行转换,在源数据的基础上创建新的可绘制数据,生成 SVG 路径以及通过数据和方法在 DOM 中创建数据可视化元素
      D3 是数据可视化基础库,ECharts 是图表库,G2 是图形语法库,如果说 D3 是面粉,ECharts 是面条,那么 G2 就是面团,它介于面粉和面条之间,比 ECharts 更加灵活,比 D3 效率更高

图形语法

在设计一个图表库时,很难设计出足够多的图表类型来满足用户的各类需求。一个基于图表分类的图表库会面临随着图表量增大,而整个系统的结构变得复杂而难以维护的问题。图表与图表之间的一些相似的部分也难以得到高效的复用

因此,在工程实现方面,有一个问题越来越突出:如何能以比较小的代价穷举(使工程适应)尽可能多的图形?归根到底,就是如何表示一个可视化图表的问题

Leland Wilkinson 在上世纪 80 年代开始开发 SYSTAT 的统计图形软件包时,也遇到了这个问题。最初的版本是枚举每一个能收集到的统计图形,最终代码量非常大,约 1.5M 左右。90 年代初,他基于面向对象重构了这个项目,以一种树形结构管理图形元素,得到了更易扩展和动态的结果。这时软件包的大小下降到了 1M 以内。到了 90 年代末期,他和几个统计学家、计算机学家合作基于之前的工作开发了统计图形绘图工具 GPL 。这个 Java 版本的软件代码量下降到了 0.5M 以下,并且沉淀出了一套稳定可靠的架构

《The Grammar of Graphics》(简称 GOG)就是 Wilkinson 在开发这套可视化软件的时候编写的,既有他对无数统计图表分析研究后的理论总结,也不乏实现图形语法的软件架构细节。至此,用一套语法描述任意图形的方法诞生了,编写基于图形语法的软件包有了理论依据和设计实践指导

GOG 把创建图形的步骤分为 3 步:

  1. Specification 定义、描述
  2. Assembly 装配
  3. Display 展现

Specification 定义、描述

一个图形系统需要通过描述来处理数据,定义图形,显示图形。描述的另一种定义是说它是图形的底层语法。一个图形系统的描述总结为以下六条:

  1. DATA:从数据集创建变量的一系列数据操作
  2. TRANS:对数据的一些转化操作,例如聚合、排序、筛选
  3. SCALE: 缩放变换
  4. COORD:坐标系统。例如极坐标系
  5. ELEMENT:图形的视觉属性和属性。例如颜色、大小、形状
  6. GUIDE:一个或多个辅助项。例如刻度、图例

Assembly 装配

将上面示例图表的描述信息转换为下图所示的树形结构

这就是面向对象思想的应用,将场景内包含的元素进行分类和组合,利用继承实现分类和代码复用,利用集合(包含关系)对元素进行分组划分。有了一个完整的树形结构之后,在展现阶段,就可以遍历这棵树进行渲染了

Display 展现

上面的树形结构结合渲染工具(符号、折线,曲线,多边形等),布局规则,我们就可以渲染最终结果了。图形语法使得我们可以单独对其中的某个元素进行添加、删除、修改属性,而不需要重新定义整个对象结构

从数据生成图形的步骤


Antv G2 其中的 两个 G 就是来源于 The Grammar of Graphics 中的两个 G,是目前 JavaScript 社区对《The Grammar of Graphics》还原度最高的实现,也受到了 Wilkinson 本人的肯定

Antv G2 实现饼图的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const data = [
{ k: ‘一’, v: 40 },
{ k: ‘二’, v: 21 },
{ k: ‘三’, v: 17 },
{ k: ‘四’, v: 13 },
{ k: ‘五’, v: 9 }
]
const dv = new DataSet.View().source(data) // 载入数据
.transform({ // 数据处理:统计每一个 key 对应数值 value 占总和的比例
type: ‘percent’,
field: ‘v’,
dimension: ‘k’,
as: ‘percent’
})
const chart = new G2.Chart({
container: ‘id’ // 指定图表容器,可以是 DomNode,也可以是 id
})
chart.source(dv) // 载入数据
chart.axis(false) // 不显示坐标轴
chart.coordinate(‘polar’).transpose() // 坐标转换
chart.intervalStack() // interval 类型的 element,做堆叠处理
.position(‘1*percent’) // 位置映射
.color(‘k’) // 颜色映射
chart.render() // 渲染图表

如果通过传统的枚举型图表来实现饼图的话,我们需要写一个饼图的类,然后提供数据的接口和配置,数据需要满足要求的格式,配置只能支持固定的选项,然后实现绘制

而在图形语法的设计下,生成每一个图形的过程就是组合不同的基础图形语法的过程。上述饼图的生成过程就经历了从原始数据 data 到数据转换、坐标轴转换、指定辅助元素、指定基础图形对象、度量转换、数据映射等过程

图形语法的灵活和强大之处就在于,我们只需要改动其中某一步的处理过程,就能得到完全不同的、全新的图表。通过一连串链式的变换调用,就实现了饼图,而其中任何一部都可以修改和复用。所以通过这种抽象机制,代码本身就表示了配置

开源库拆解:ECharts 架构

主要参考论文 Li, Deqing, et al. “ECharts: a declarative framework for rapid construction of web-based visualization.” Visual Informatics June 2018, Pages 136-146
https://www.sciencedirect.com/science/article/pii/S2468502X18300068

数据流架构

作为一个非常复杂的图表库,ECharts 采用了数据驱动架构来实现,把步骤切分成数据处理,视觉编码和渲染阶段,其最终产生图形元素。数据流是单向的,任何用户交互只会会修改原始选项或数据,并从头开始运行 pipeline。此外,每个阶段都可以作为切面暴露给开发者

渐进式的可视化渲染


在进行数据量较大的图表绘制时,数据处理和渲染都会耗费许多时间,当用户进行交互操作时,问题会更加明显,会触发图表从头更新,会导致主线程阻塞,无法处理动画和交互响应

ECharts 采用 chunk data 的方式来解决这个问题。将数据源划分为多个小的 chunk。每一个区块的数据独立进行布局渲染。这样做是为了保证每个 chunk 都能尽可能在 16ms 内完成计算,每个 chunk 计算任务完成后,会调用 requestAnimationFrame,同时暂停接下来的任务,直到下一帧。如果在这个过程中发生了交互行为,那么已创建的旧任务都会被废弃,重新创建新的任务。这样,就可以保证浏览器的主线程不被阻塞,及时响应用户交互

2D 绘图引擎 ZRender

官网: https://ecomfe.github.io/zrender-doc/public/
GitHub: https://github.com/ecomfe/zrender

ZRender 是 ECharts 底层依赖的 2D 绘图引擎,用于图形元素管理、渲染器管理和事件系统,提供 Canvas、SVG、VML 多种渲染方式

ZRender 中有三种绘制类型,image / text / path,image 和 text 的绘制都可以借助 canvas API 来实现,对于 path, 会通过一个叫 PathProxy 的类,它会负责记录底层的绘制指令,把路径命令数据以 Float32Array 存储起来,通过这个数据实现了对多种渲染方式的支持,path 数据也会被用来在事件系统中做事件命中检查

为了让不同的渲染器可以有相同的事件处理机制,ECharts 对整个图表容器绑定了事件。SVG 也并不是其对 DOM 树的每个元素绑定事件,SVG/Canvas 都是使用一套统一的事件处理机制。ECharts 会检测事件的位置坐标 (x, y) 是否在图形边框里,对于 image/text 来说,这一步就可以判断事件是否命中、命中了哪个元素。对于 path 来说,会先检查事件位置是否在实际渲染区域里,然后再去做精确判断

参考