TL;DR
不管你是做技术、BI 或是科研,都或多或少接触过“数据可视化”,或者在狭义范围来说便是各类图表。如果总结归纳一下,各种图表库按照表达性和速度的差异主要分为三类:
- 第一类是可视化分析工具如 Tableau、Excel、google sheets 等,你只需要简单的交互操作,比如选择和拖拽,就能够从预设的图表库中快速创建出图表,但相应地图表的自定义程度会受到较多的限制。
- 第二类是 OpenGL、Canvas 等绘图引擎提供了一系列 API,可以完全按照你的需要来实现可视化效果,当然细粒度的控制也意味着需要非常精通可视化理论。
- 第三类则介于二者之间,在速度和表达性之前做了权衡,在不同领域都有较好的实现,例如 R 语言的 ggplot2、python 的 bokeh、Tableau 的 VizQL、前端中 canvas 的 ECharts 等等。
如果在 Github 上搜索 chart
关键词会发现有 8.2w 个结果,其中有 3.3w 是与前端相关的,当然主要原因是浏览器的可视化使用起来更方便,也更容易分享。但如果时间倒退会十年前,前端能用的图表库大概只有 Highcharts 和 Adobe Flex,但像 ggplot2 这些经典的图表工具倒是一直非常稳定,主要原因一方面得益于数据可视化这门学科的发展,另一方面则是 HTML5 的崛起。十年间大部分图表库的发展都是基于声明式可视化语法的形态,不同的库针对速度与表达性做了不同程度的抽象,以满足不同层次的用户需求。
那么问题也来了,虽然大量图表库的出现降低了使用的门槛和壁垒,但是对于一个小白用户而言,面对一堆图表库,内心大概就是“我是谁,我在哪,我该用哪个图表库”。如果是接手的项目可能已经有明确的图表库了,比如国内大部分公司都会选择 ECharts、BizCharts 或者 G2Plot,那么可能问题就变成了“这个图该怎么画呢”、“为什么这个配置不生效”、“版本要升级吗”等等一系列的问题。
当然,本文并不是来解决这些问题。可以预见,在前端这个后浪不断的领域,新图表库仍然会层出不穷的出现,又或者接手的项目使用哪个图表库也不是马上可以被替换的。那么,理解核心的图形语法,才能以不变应万变。即使上手新的图表库,对着文档马上就能一把梭(前提是文档更新及时)。
图形语法
起源
图表作为数据可视化的重要载体,各类统计图表的描述和绘制,有着非常坚实的理论基础,而且是数学描述的形式化系统,这就是《The Grammar of Graphics》的内容。这本书非常好地体现了“思维抽象”,我们都知道 Excel 的图表是如何选择的:
又或者是 ECharts 的图表选择:
显然,这种逐一列举的方式是一种低层次的抽象。如果按照这种方式来绘制图表库,每增加一种新的图表,就需要增加一份绘图逻辑。一旦需求发生了大的变化,这个维护工作的难度可想而知。
Leland Wilkinson 在上世纪 80 年代开发 SYSTAT 的统计图形软件包时,也遇到了这个问题。最初的版本是枚举每一个已知的统计图形,最终代码量非常大。后来他基于面向对象的思路重构了这个项目,以一种树形结构管理图形元素,得到了更易扩展和动态的结果。图形语法正是在这个过程中编写的。
基本结构
Wilkinson 认为图形构造分为三个步骤,规范定义(Specification)、组装(Assembly)和显示(Display)。其中规范定义描述了如何将不同的图形对象“翻译”为形式化语言,由六部分组成:
- Data:数据,从数据集中创建变量的数据操作
- Trans:转换,数据变量之间的转换,例如排序
- Scale:标度,标度转换,例如 log 转换
- Coord:坐标,定义坐标系统,例如极坐标
- Element:图形,及其视觉属性,例如点图和颜色
- Guide:参考,图形对象间的比较、分类和对齐等,例如图例
组装则是根据规范定义绘制出画面,需要处理几何、布局、美学属性等等问题。展示则是指具体的显示枚举,如屏幕、纸张、视频等等,虽然我们现在大部分场景下是在计算机相关的平台上展示。由此看来,规范定义是整个图形语法的基础。
视觉编码
Specification 中关于 Element 图形的描述,其实在 1967 年已经有学者研究过了。Bertin 在 《Semiology of Graphics》一书中定量地阐述了图形符号与信息之间的对应关系,奠定了可视化编码的理论基础。Bertin 认为视觉编码由两部组成,视觉标记(Visual Marks)和用于控制标记视觉特征的视觉通道。图形符号有点、线、面三种,视觉通道则分为位置变量和视觉变量(Visual Variables)。位置变量包括二维平面上 x 和 y 位置, 视觉变量则包括尺寸 size_、明度 _value_、纹理 _texture_、颜色 _color_、方向 _orientation 和形状 _shape_。
按照 Bertin 所定义的视觉变量有 7 种,映射到图形符号点、线、面之后就有 21 种视觉编码,后来其他人又补充了一些视觉通道:长度、面积、体积、透明度、动画等,所以可用的视觉通道非常多。但一般一个可视化视图用到视觉通道并不是越多越好,视觉通道的堆叠会造成视觉的混乱。因此一份好的可视化设计需要考虑到人类的感知力和认知力,以确保用户可以准确、有效地理解信息。不同视觉通道的优先级、适用的数据类型等也都是需要考虑的。(这个命题涉及到了可视化设计原则,在此就不展开了)。
优势
图形语法把图形抽象为可枚举的基本元素,并且可以互相组合生成更复杂的元素。下图是 Data Points 对不同视觉编码在不同坐标系下总结。比如无填充的雷达图,其实就是把普通的折线图画在了极坐标系里。
虽然这种正交特征组合思想会存在一些不严密的地方,但是其灵活性也是不言而喻的,生成每个图形的过程就是组合不同的基础图形元素的过程,只需要改动其中某一步的处理过程,就能得到不同的可视化图表。使用这种语法结构,在实现一个绘图系统时,可以大幅简化架构的设计,不仅可以描述常用的已知图表,也能描述没有被广泛使用(甚至还未发明)的图表。
举个例子
这个图被称为“子弹图”(Bullet Chart),子弹图无修饰地线性表达方式能够在狭小的空间中表达丰富的数据信息。上图反映的是销售情况。黑色柱子的长度反映实际利润,背景颜色则是不同利润的区间范围,红色线则为设定的目标,反映其是否完成目标。
如果按照传统的二维统计图表分类,子弹图并不是常见的柱状图或直方图,也很难归类。但是如果利用图形语法的思想,子弹图的形式化语言描述是这样的。元素 Element 有黑色柱形和红色标记,辅助 Guide 则是背景颜色灰度不同所代表的评价标准。
ELEMENT: rect(position(actual), color(black))
ELEMENT: tick(position(object), color(red))
GUIDE: rect(position(ranges), color.gray())
GUIDE: axis(dim(1), label(title))
形式化系统
如果从技术角度而言,这本书所说的“图形定义”看起来也不新奇,无非是基于面向对象的思想。但作为学术著作,它的厉害之处是使用了“形式化系统”来描述这套语法,说白了就是使用数学符号进行定义、演绎推理和验证。这本书中都是这么来定义的:
error bar 的定义
描述nest的时候书用的集合定义
因为 Wilkinson 是个统计学家,在数学家的世界里只有数学的语言足够精炼和严谨。从抽象层次的角度,这个就比枚举、或是白话描述就高了很多。一方面各种定义、描述都非常简短,并且足够严密,另一方面,图表的后续发展可以依靠这套形式化语言进行更多的符号演算完成。
实现
图形语法目前已经有了不少的实现,如开头提到的 ggplot2、vega、Bokeh、G2 等等。图形语法严格来说是一种思想,并不是一个真正意义上的实现标准,所以这些实现并不能互相兼容,有些实现为了兼顾实际的使用场景,也没有完全遵循图形语法的原始定义。
在这些实现的基础之上,便是各类图表库。图形语法的抽象程度这么高,小白用户没点可视化基础根本拿不下。但目前为止,即使图形语法已经把图表拆成了不同的基本元素组合,但是没有哪个构建工具会让用户自己决定视觉元素、绑定数据、选择坐标系等等。Wilkinson 本人虽然也在 Tableau 麾下,但 Tableau 也还是用“列举”的方式,提供一系列预设的“图表类型”来给用户选择。
G2
再如 G2,也是打着“图形语法”的旗号。G2 中看似诡异的写法 chart.interval().position('genre*sold')
就是来自图形语法,这里的 * 不是乘法,而是作者自己定义的一种操作符,表示合并。但因为 JavaScript 不支持操作符重载,导致这里只能用字符串,丢失了类型信息,很容易拼错了都不知道。
G2 相较于拿来直接用的图表库更低一层,其定位也不是对标 Echarts 的,对于想把它拿来当成 Echarts 来用的人就会觉得非常痛苦。所以 G2 后来又提供了更简化的 G2Plot,又或是封装为 React 版本的 BizCharts 都从侧面验证了这一点。
Vega
另外值得一提的是 Vega,Vega 是基于 D3 实现的声明式可视化语法框架, 和 D3 一样都出自华盛顿大学的 IDL 实验室。Vega 的目标是做数据可视化的配置语法,可以算是图表库的低代码。虽然 ECharts 等图表库也主要依靠于 options/ config 等配置项,但是仍依赖于 JavaScript 代码来加载数据,但是 Vega 可以做到只需要 JSON 就能完成图表相关的开发,包括数据加载、转换等等。
例如下面的例子实现了加载一个 CSV 文件,并新增计算字段 temp_range,在 G2 的 Dataset 中也可以做到类似操作,但都是通过 JS 代码实现的,而 Vega 的底层引擎可以做到:
{
"data": {"url": "data/seattle-weather.csv"},
"transform": [
{"calculate": "datum.temp_max - datum.temp_min", "as": "temp_range"}
]
}
Vega 的配置化做法的好处,是特别方便程序生成和分析,比如他们专门给 Python 开发的 Altair,原理就是动态生成 JSON 的 Vega 配置,因此很容易移植到其他语言。同时在交叉领域(如机器学习与可视化的结合),这种低代码的形式更容易处理数据集。
但是追求完全的可配置性,导致最基本的图表都要写很多,比如下面是实现一个最简单的柱状图,都需要指定 scale、axies 等配置。所以 IDL 自己又在此基础上做了一个更高级的语法 Vega-Lite,基本上只要指定可视化标记类型、图形属性和数据字段的映射关系就能够实现一个图表。
Vega 的背后是个学术组织,所以相比较来说更关注学术方面的发展,一些在业界重要但没什么研究价值的东西就不太关系,比如主题配色、动画、阴影等等的优先级很低。像动画在 2016 年就说要做了,而作者在前阵子又说有好多其他功能要做,动画这个先交给我的学生去研究了╮(╯▽╰)╭
总结
本文主要非常粗浅的介绍了图形语法(因为我也啃不动《The Grammar of Graphics》这本书),只是希望大家对这些概念有一些了解,再去看各类图表库的设计也都基本上围绕着这些概念展开的。同时这本书的出发点也是非常值得我们学习的,即抽象化的思考。