如何选择合适的方法对数据进行可视化处理
# 如何选择合适的方法对数据进行可视化处理
学习可视化设计,一定要学会处理数据。因为在可视化项目中,我们关注的信息经常会隐藏在大量原始数据中,而原始数据又包含了太过丰富的信息。其中大部分信息不仅对我们来说根本没用,还会让我们陷入信息漩涡,忽略掉真正重要的信息。
因此,只有深入去理解数据,学会提炼、处理以及合理地使用数据,我们才能成为一名优秀的可视化工程师。
# 从原始数据中过滤出有用的信息
在可视化中,我们处理数据的目的就是,从数据中梳理信息,让这些信息反应出数据的特征或者规律。
一个最常用的技巧就是按照某些属性对数据进行过滤,再将符合条件的结果展现出来,最终让数据呈现出我们希望用户看到的信息。
假设有一批仿造的公园每天的访问人群数据 (opens new window),我们想通过这批数据提取有用的信息,从而利用这些信息来优化公园的娱乐设施。
此时,我们就可以把人群的分布数据绘制成合适的可视化图表,从中分析出人群分布的规律,最终实现的效果可以看这里 (opens new window),公园管理人员可以直观的看到不同时间段人群在公园中的分布情况,从而更好的优化公园的娱乐设施。
此外,我们还可以通过其他的可视化展示形式,来获取更多有用的信息,比如,公园每天不同时间段的人数变化情况 (opens new window)、公园里不同娱乐区域的人数男女比例情况(展示形式一 (opens new window)、展示形式二 (opens new window)、展示形式三 (opens new window))等等。
通过上面这个例子,我们能体会到数据可视化分析的一般过程,通常我们通过数据过滤和展示,从中提取出有用信息,以便于做出后续的决策,这就是数据可视化的价值所在。只要数据是客观的,分析过程是合理的,那数据表现出来的结果就是具有实际意义的。
# 强化展现形式让用户更好地感知
在前面的例子中,我们用散点图呈现游客信息并从中分析出有用的内容,这种形式直观有效,但是展现形式略显单调。
除了合理的数据分析以外,数据可视化有时候通过强化展现形式,让用户更好地感知数据表达的内容。这样能够帮助需要关注该数据的用户,更好地把握整体信息。
有一名叫亚赛 (opens new window)的可视化设计师,他做了一个北京空气质量(2015-2018)的可视化展现 (opens new window),利用每天北京空气的 AQI 值绘制了色条,同时还用心地让每一天对应了一个地标的当天实拍照片。这不仅增加了项目整体的趣味性,也强化了用户的直观认知。
这个项目的具体代码见这里 (opens new window)。它的具体实现是依赖一个叫做 p5.js (opens new window) 的图形库,这也是一个很棒也很有趣的图形库,用来学习可视化也非常合适,有空可以研究研究。
我们可以学习这个项目对数据的处理和展示思路。
# 将信息的特征具象化
我们一般会认为,可视化都是需要使用真实数据来呈现的,数据越真实、越详细,可视化效果呢就越好。如果有了这个想法,说明我们有一点陷入到思维定式中了。
实际上,有时候我们并不要求数据越真实越详细,甚至不要求绝对真实的数据,只需要把数据的特征抽象和提取出来,再把代表数据最鲜明的特征,用图形化、令人印象深刻的形式呈现出来,这就已经是成功的可视化了。
比如下面这张图,就用可视化的方式解释了数据、信息、知识、见解、智慧和影响力。这种可视化呈现的数据并不是真实、准确地,而是带有趣味性的,通过对信息特征进行抽取,让看的人形成了一种视觉认知。
类似的可视化例子还有很多,比如图解博士是什么 (opens new window),虽然它的数据不是基于海量数据提取的,但却是一组概念的具象化,所以它毫无疑问也是一个非常成功的可视化方案例。
再比如,Manu Cornet 的组织架构图 (opens new window),也用非常形象的方法绘制出了各个知名公司的组织架构差异。它的数据当然也不是各个公司详细的组织架构数据,而是根据每个公司组织架构特征直接图形化形成的。
实际上,信息特征具象化的前提,就是我们真正掌握了我们需要的信息特征,而这些特征的提取和掌握,正是通过前面两种方法迭代出来的!用一句话总结就是,数据可视化本身是一个不断迭代的过程。
具体过程是,我们先进行原始数据的信息收集和分类处理,再通过原始方法表达出有用的信息,接着通过强化展现形式,让信息的核心特征变得更加鲜明,经过这一轮或者几轮的迭代,我们就可能拿到最本质的信息了,最终我们再把这些信息具象化,就可以达到令人印象深刻的效果了。
因此,对原始数据进行不断迭代,就是数据可视化的基本方法论。要牢记这句话。
# 可视化数据处理的一般方法是什么
在数据处理的过程中,我们经常遇到两种情况:
一种是数据太少,我们没法找到有用的信息,也就无法进行可视化呈现。
另一种是数据太多,信息纷繁复杂,我们经常会迷失在信息海洋中,无法选择合适的可视化呈现方式,最终也表达不了多少有意义的内容。
事实上,前面介绍的三种数据处理方法是数据可视化的基本方法论,我们可以在可视化过程中借鉴它们的思路,但是它们并不系统。
下面介绍一个合理的数据可视化分析过程,来系统地讨论数据处理的一般方法。
# 数据可视化的一般过程
在数据可视化中,我们一般会围绕 4 个问题对可视化过程进行迭代,它们分别是:
你有什么数据?
你想从数据中了解什么信息?
你想用什么样的可视化方式呈现?
你看到了什么,它有意义吗?
结合这几个问题,可以画出数据可视化的一般过程的流程图。
# 实战演练:对公园中的游客进行数据可视化
首先我们来看我们有什么数据 (opens new window)。我们的原始数据的格式就像下面记录的一样,有时间、地点和性别。
[{
"x": 456,
"y": 581,
"time": 12,
"gender": "f"
}, {
"x": 293,
"y": 545,
"time": 12,
"gender": "m"
}, {
// ...
}]
2
3
4
5
6
7
8
9
10
11
12
13
接下来我们看一下我们想了解什么,假设我们想了解公园一天中的游客变化规律,那么我们可以用分类的思路处理数据。在前面,我们就对数据进行了简单的时间分类、地点分类和性别分类。这里可以用 d3 的数据变换 (opens new window)(Transformations)模块将原始数据处理成我们想要的模式。
下面用到了 d3.rollups (opens new window),它可以对数据进行分组,然后汇总。
这个接口设计得比较函数式(functional),它接受 3 个参数,第一个参数是要处理的数据,也就是上面的原始数据,后面两个参数是两个函数算子,第一个算子表示对数据分组进行汇总的方式,这里是使用 length 来汇总,也就是统计数据的条目数。第二个算子则表示对数据进行分组的属性,这里是用时间属性进行分组。最后,我们在分组之后,再对数据进行一次排序,因为我们要按照时间从小到大进行排序。
(async function() {
const data = await (await fetch('data.json')).json();
const dataset = d3.rollups(data, v => v.length, d => d.time).sort(([a],[b]) => a - b);
...
}());
2
3
4
5
经过分组和排序之后,我们从原始数据得到了如下的新数据:
[[8, 145], [12, 141], [18, 191], [20, 23]]
现在,我们只有 8 点、12 点、18 点、20 点这 4 个时间段的数据,我们还需要把游客为 0 的时间信息补全。假设公园是早晨 6 点开门,晚上 22 点关门,那么 6 点、22 点的游客数应该是 0,补充的数据如下:
dataset.unshift([6, 0]);
dataset.push([22, 0]);
2
我们补全的数据是一个二维数组,其中每个元素的第一个值是时间,第二个值是当前时间的公园内人数。
接着,就要确定用哪种可视化方式呈现数据。因为要呈现游客的变化规律,所以我们最终决定使用折线图来呈现,那我们就要把数据转换成要显示的折线上的点坐标。
const points = [];
dataset.forEach((d, i) => {
const x = 20 + 20 * d[0];
const y = 300 - d[1];
points.push(x, y);
});
2
3
4
5
6
然后,我们用 SpriteJS (opens new window) 创建 Polyline 元素,把这个折线点坐标传给它。最后,我们把这个元素给添加到 layer 上,就可以将它显示出来了。
const p = new Polyline();
p.attr({
points,
lineWidth: 4,
strokeColor: 'green',
smooth: true,
});
fglayer.append(p);
2
3
4
5
6
7
8
9
因为还要考虑到游客是随时间变化的,所以我们要给它增加一个坐标轴。
这里,我们是用 SpriteSvg 来绘制坐标轴的,SpriteSvg 是一个特殊元素,它能够创建一个 SVG 对象,然后把它以图像方式绘制到 Canvas 上。这就是利用了前面学过的 SVG 和 Canvas 的混合使用方式,它可以把 SVG 作为图像绘制,这样既能使用 SVG 来灵活地改变图形,又可以用 Canvas 来高性能地渲染。
在创建坐标轴的时候,我们需要给坐标轴设置一个 scale,d3 中间应用了很多函数式编程思想,这里的 scale 也是一个函数算子,它是由高阶函数 d3.scaleLinear 创建的,我们给它设置 domain 从 0 到 24,表示一天的 24 个小时,range 从 0 到 480,表示占据 480 像素宽度。
const scale = d3.scaleLinear()
.domain([0, 24])
.range([0, 480]);
const axis = d3.axisBottom(scale)
.tickValues(dataset.map(d => d[0]));
const axisNode = new SpriteSvg({
x: 20,
y: 300,
flexible: true,
});
d3.select(axisNode.svg)
.attr('width', 600)
.attr('height', 60)
.append('g')
.call(axis);
axisNode.svg.children[0].setAttribute('font-size', 20);
fglayer.append(axisNode);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
然后,我们再通过 d3.axisBottom 高阶函数,用创建的 scale 来生成一个具体的坐标轴算子 axis,然后对 SVG 对象应用这个算子,就可以绘制出坐标轴的图像了。最终效果可以看这里 (opens new window)。
这样,我们就得到了一天中游园人数的变化趋势,这对公园的管理策略是有一些参考价值的。不过这个数据还很粗糙,因为我们的原始数据的信息量并不大。不过如果我们继续收集不同天的数据,并进一步分析游客在不同日期、不同地点的人数情况,或者对游客的性别进行分析,我们就可能得到更多有趣的信息,但究竟能获得什么样的有用信息,还要看原始数据情况和我们实际着手进行迭代的方式。
总之,当我们把更多的信息集中在一起的时候,我们就能做更多的事情了,比如,我们可以分析游客数和当月平均气温的关系,或者分析交通趋势和公园游客趋势的关系,以及分析天气与游园游客性别的相关性等等,并且我们还能利用这些数据来帮助公园后续的建设和管理决策。
# 如何处理多元变量
# 从数据到图表展现
一般来说,我们拿到的原始数据通常可以组织成表格的形式,表格中会有很多列,每一列都代表一个变量。比如这份 2014 年北京的天气历史数据 (opens new window),这份数据中包含了许多变量,比如时间、最高气温、平均气温、最低气温、最高湿度、平均湿度、最低湿度、露点等等。一般的情况下,我们会将其中我们最关心的一个变量比如平均气温,用一个图表展现出来。
首先,这份数据是 csv 格式的,是一张表,我们先用 D3.js 将数据读取出来,然后结构化成 JSON 对象。
我们通过 fetch 读取 csv 的数据。CSV 文件格式是用逗号和回车分隔的文本,所以我们用 .text() 读取内容。然后我们使用 d3 的 csvParse 方法,将数据解析成 JSON 数组。最后,我们再通过数组的 filter 和 map,将我们感兴趣的数据取出来。这里,我们截取了 1 月到 3 月的平均气温数据。
const rawData = await (await fetch('beijing_2014.csv')).text();
const data = d3.csvParse(rawData);
const dataset = data.filter(d => new Date(d.Date).getMonth() < 3)
.map(d => {return {temperature: Number(d['Temperature(Celsius)(avg)']), date: d.Date, category: '平均气温'}});
console.log(dataset);
2
3
4
5
取到了想要的数据,接下来我们就可以将它展示出来了,这里直接采用一个图表库 QCharts (opens new window),它是一个基于 SpriteJS (opens new window) 设计的图表库。与数据驱动框架相比,图表库虽然减少了灵活性,但是使用上更加方便,通过一些简单的配置,我们就可以完成图表的渲染。
用来展示平均气温最常见的图表就是折线图,展示折线图的过程可以简单分为 4 步:
第一步是创建图表(Chart)并传入数据;
第二步是创建图形(Visual),这里我们创建的是折线图,所以使用 Line 对象;
第三步是创建横、纵两个坐标轴(Axis)、提示(ToolTip)和一个图例(Legend);
最后一步是将图形、坐标轴、提示和图例都添加到图表上。
const { Chart, Line, Legend, Tooltip, Axis } = qcharts;
const chart = new Chart({
container: '#app'
});
let clientRect={bottom:50};
chart.source(dataset, {
row: 'category',
value: 'temperature',
text: 'date'
});
const line = new Line({clientRect});
const axisBottom = new Axis({clientRect}).style('grid', false);
axisBottom.attr('formatter', d => '');
const toolTip = new Tooltip({
title: arr => {
return arr.category
}
});
const legend = new Legend();
const axisLeft = new Axis({ orient: 'left',clientRect }).style('axis', false).style('scale', false);
chart.append([line, axisBottom, axisLeft, toolTip, legend]);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这样,我们就将图表渲染到画布上了。最终效果可以看这里 (opens new window)。
# 处理多元变量
刚才我们已经成功将平均气温这个变量用折线图展示出来了,但在很多数据可视化场景里,我们不只会关心一个变量,还会关注多个变量,比如,我们需要同时关注温度和湿度数据。那怎么才能把多个变量绘制在同一张图表上呢?换句话说,同一张图表怎么展示多元变量呢?
# 在一张图表上绘制多元变量
最简单的方式是直接在图表上同时绘制多个变量,每个变量对应一个图形,这样一张图表上就同时显示多个图形。
比如在刚才代码基础上,直接添加平均湿度数据。
const rawData = await (await fetch('beijing_2014.csv')).text();
const data = d3.csvParse(rawData).filter(d => new Date(d.Date).getMonth() < 3);
const dataset1 = data
.map(d => {
return {
value: Number(d['Temperature(Celsius)(avg)']),
date: d.Date,
category: '平均气温'}
});
const dataset2 = data
.map(d => {
return {
value: Number(d['Humidity(%)(avg)']),
date: d.Date,
category: '平均湿度'}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
然后,我们修改图表的数据,将温度(dataset1)和湿度(dataset2)数据都传入图表。
chart.source([...dataset1, ...dataset2], {
row: 'category',
value: 'value',
text: 'date'
});
2
3
4
5
这样,我们就得到了同时显示温度和湿度数据的折线图。最终效果可以看这里 (opens new window)。
# 用散点图分析变量的相关性
虽然上面我们已经把温度和深度同时绘制到一张折线图中了,但是我们很难直观地看出温度与湿度的相关性。如果我们想了解 2014 年全年,北京市温度和湿度之间的关联性,我们还得用另外的方式。
一般来说,要分析两个变量的相关性,我们可以使用散点图,散点图有两个坐标轴,其中一个坐标轴表示变量 A,另一个坐标轴表示变量 B。这里,我们将平均温度、相对湿度数据获取出来,然后用 QCharts 的散点图(Scatter)来渲染。
const rawData = await (await fetch('beijing_2014.csv')).text();
const data = d3.csvParse(rawData);
console.log(data);
const dataset = data
.map(d => {
return {
temperature: Number(d['Temperature(Celsius)(avg)']),
humdity: Number(d['Humidity(%)(avg)']),
category: '平均气温与湿度'}
});
const { Chart, Scatter, Legend, Tooltip, Axis } = qcharts;
const chart = new Chart({
container: '#app'
});
let clientRect={bottom:50};
chart.source(dataset, {
row: 'category',
value: 'temperature',
text: 'humdity'
});
const scatter = new Scatter({
clientRect,
showGuideLine: true,
});
const toolTip = new Tooltip({
title: (data) => '温度与湿度:',
formatter: (data) => {
return `温度:${data.temperature}C 湿度:${data.humdity}% `
}
});
const legend = new Legend();
const axisLeft = new Axis({ orient: 'left',clientRect }).style('axis', false).style('scale', false);
const axisBottom = new Axis();
chart.append([scatter, axisBottom, axisLeft, toolTip, legend]);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
最终效果可以看这里 (opens new window)。从图表结果中我们可以发现,平均温度和相对湿度并没有相关性,所以点的空间分布比较随机。事实上也是如此,气温和绝对湿度有关,但相对湿度因为已经考虑过了温度因素,所以就和气温没有相关性了。
下面展示下由相关性的变量的散点图,比如露点和平均温度。
在空气中水汽含量不变, 并且气压一定的情况下, 空气能够冷却达到饱和时的温度就叫做露点温度, 简称露点, 它的单位与气温相同。从定义里可以知道,露点和温度与湿度都有相关性。
下面我们只要在前面代码基础上,把平均湿度换成平均露点温度就行了。
const dataset = data
.map(d => {
return {
temperature: Number(d['Temperature(Celsius)(avg)']),
tdp: Number(d['Dew Point(Celsius)(avg)']),
category: '平均气温与露点'}
});
2
3
4
5
6
7
最终呈现出来的散点图具有典型的数据正相关性,也就是说图形的点更集中在对角线附近的区域。最终效果可以看这里 (opens new window)。
我们还可以把湿度数据也加上。
const dataset = data
.map(d => {
return {
value: Number(d['Temperature(Celsius)(avg)']),
tdp: Number(d['Dew Point(Celsius)(avg)']),
category: '平均气温与露点'}
});
const dataset2 = data
.map(d => {
return {
value: Number(d['Humidity(%)(avg)']),
tdp: Number(d['Dew Point(Celsius)(avg)']),
category: '平均湿度与露点'}
});
// ...
chart.source([...dataset, ...dataset2], {
row: 'category',
value: 'value',
text: 'tdp'
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
可以看到,平均湿度和露点也是成正相关的,不过露点与温度的相关性更强,因为散点更集中一些。最终效果可以看这里 (opens new window)。
如果是平均温度和最低温度的散点图,它们的相关性会更强。我们会发现,图上的散点基本上就在对角线上。最终效果可以看这里 (opens new window)。
# 散点图和相关性之间的关系
总的来说,两个数据的散点越集中在对角线,说明这两个数据的相关性越强,下面是一张散点图和相关性之间关系的总结图。
# 散点图的扩展
用散点图可以分析出数据的二元变量之间的相关性,这对数据可视化场景的信息处理非常有用。
不过,散点图也有明显的局限性,那就是它的维度只有二维,所以它一般只能处理二元变量,超过二维的多元变量的相关性,它处理起来就有些力不从心了。
不过,我们还不想放弃散点图在相关性上的优异表现。所以在处理高维度数据时,我们可以对散点图进行扩展,比如引入颜色、透明度、大小等信息来表示额外的数据维度,这样就可以处理多维数据了。
散点图处理多维数据的方法可以参考这篇文章:《厉害了,散点图还能这么画!》 (opens new window)。
# 处理多维信息的其他图表形式
事实上,处理多维信息,我们还可以用其他的图表展现形式,比如用晴雨表来表示数据变化的趋势就比较合适。比如,北大可视化实验室在疫情期间就制作了一张疫情数据晴雨表 (opens new window),你能明显看出每个省份每天的疫情变化。
再比如,还有平行坐标图 (opens new window)。平行坐标图也有横纵两个坐标轴,并且把要进行对比的五个不同参数都放在了水平方向的坐标上。
此外,我们还可以用热力图、三维直方图、三维气泡图等等其他的可视化形式来展现多维度的信息。具体方法可以参考这篇文章:从 1 维到 6 维,一文读懂多维数据可视化策略 (opens new window)。
# 如何让可视化设计更加清晰
在实际的可视化项目中,我们经常会遇到一种情况:用户期望所有的可视化图表都是简单明了的。实际上,这是不现实的。
因为我们拿到原始数据之后,第一步是分析数据,也就是从各种不同的角度尝试去观察数据,确定我们希望用户了解的信息。这些信息如果是简单清晰的,那么可视化结果就是简单直观的。如果用户想要了解的数据规律本身就很复杂,那么可视化图表所能做的事情,也只能是尽可能清晰地展现用户关注的重要信息,屏蔽干扰信息,来降低用户理解数据的难度。
因此,我们要明白,在任何时候,制作可视化图表都是为了帮助人们理解抽象的数据,不管这些数据多复杂,都要尽可能让读者快速理解。
# 分清信息主次,建立视觉层次
下面以平均温度和露点的散点图为例,来看看具体是怎么操作的。
一个可视化图表应该包括图形、图例、提示信息、坐标轴等等元素。其中,图形是最重要的元素,所以它一般会在图表最核心的区域呈现。
在上图中,我们使用了比较鲜明的蓝色来突出图形。至于左侧和下方的坐标轴,我们用比较淡的灰黑色来显示。背景中的辅助线存在感最弱,因为它们是用来辅助用户更认真地阅读图表、理解数值的,不是主要元素,所以我们会用非常淡的颜色把它们显示在背景里。这些元素就构成了一个有鲜明视觉层次感的图表。
不过,就这个图表而言,我们还可以把它做得更好。因为,我们实际上希望表达给用户的信息还包含了平均气温与露点的正相关性,如果用户对这个数学关系比较敏感,完全可以通过散点分布了解到它们的正相关性,但是对其他不敏感的用户来说,我们可以添加曲线图来引导他们。
第一步,我们处理一下数值,将数据按照气温高低排序,然后对相同温度的数据进行分组,最后将相同温度下的平均露点温度计算出来。
// 露点排序
let dataset2 = [...dataset].sort((a, b) => a.tdp - b.tdp);
// 对相同露点的温度进行分组
dataset2 = dataset2.reduce((a, b) => {
let curr = a[a.length - 1]
if (curr && curr.tdp === b.tdp) {
curr.temperature.push(b.temperature)
} else {
a.push({
temperature: [b.temperature],
tdp: b.tdp
})
}
return a
}, []);
// 最后将露点平均温度计算出来
dataset2 = dataset2.map(d => {
d.category = '露点平均气温'
d.temperature = Math.round(d.temperature.reduce((a, b) => a + b) / d.temperature.length)
return d
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在计算好数据之后,我们将散点和曲线两个数组都传给图表对象。
const chart = new Chart({
container: '#app'
});
let clientRect={bottom:50};
chart.source([...dataset, ...dataset2], {
row: 'category',
value: 'temperature',
text: 'tdp'
});
2
3
4
5
6
7
8
9
最后,我们就能分别用散点和曲线图来呈现数据了。
const ds = chart.dataset;
const d1 = ds.selectRows("平均气温与露点");
const d2 = ds.selectRows('露点平均气温');
// 散点图
const scatter = new Scatter({
clientRect,
showGuideLine: true,
}).source(d1);
// 曲线图
const line = new Line().source(d2);
line.style('line', function(attrs, data, i) {
return { smooth: true, lineWidth: 3, strokeColor: '#0a0' };
});
line.style('point', function(attrs) {
return { display: 'none' };
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
最终效果如下,也可以看这里 (opens new window)。
这个图表分为三个层次:
第一层,我们用曲线描绘气温与平均露点的关系,随着气温升高,平均露点温度也会升高,二者的总体趋势保持一致。
第二层,就是我们保留的散点图,我们可以通过它看出具体某一次记录的温度和露点数据,还可以从分布规律看出相关性的强度,因此它能够表达的信息比曲线图还要多一些。
第三层,我们使用了坐标轴和辅助线作为背景。
总之,像这样层次分明的图表,非常有助于我们快速理解图表上的信息。
# 选择合适图表,直观表达信息
前面我们实现过一个公园游客散点图,我们将男游客和女游客分别标记之后,可以看到不同区域在某个时刻的游客性别分布,比如下图就是公园 12 点游客分布情况。
不过,散点图虽然能够一眼看出不同性别游客在四个区域的大致分布,但不够直观。如果我们要更精细地分析的话,还是应该对数据呈现方式进行改进。
# 普通饼图
在表达某个单组变量的分布状况的时候,使用饼图是一个比较直观的方式。
比如,我们可以用一组饼图来表示中午 12 点公园游客在四个区域的性别分布,让它对应上面那张散点图。最终效果如下,也可以看这里 (opens new window)。
虽然饼图表示的结果非常简单和直观,但它也有缺点,就是一张饼图实际上能表示的信息很少。一个饼图一次只能表示一组单维度数据,而性别又只有男和女两种,所以我们为了表示四个区域,不得不用四张饼图,这会非常麻烦。
# 嵌套饼图
我们可以尝试把前面的四张饼图合并成一张嵌套饼图,它由两个饼状图组成,中间小的饼状图是女性在四个区域的分布情况,大的饼图是男性在四个区域的分布情况。最终效果也可以看这里 (opens new window)。
这样我们就用一张图表直观地显示了,中午 12 点,公园内四个区域中男女游客的分布情况了。
不过实际上,这张图表和前面的四个饼图表达的信息不同,那四张饼图表达的是某个区域男女游客分布的情况,而这张图则是表达男女游客分别在四个区域的分布情况,它们正好是互补的。
# 南丁格尔玫瑰图
如果想将游客性别和游客区域的分布情况融合在一起表达也是可以的。我们可以用堆叠的直方图,或者更加美观的南丁格尔玫瑰图来表达。下面使用南丁格尔玫瑰图来表达。
南丁格尔玫瑰图是一种圆形的直方图,用半径来表示数量和数据之间的区别。在这一张图上我们可以看出,四个区域的总人数分布,以及每个区域男女游客数量分布。最终效果也可以看这里 (opens new window)。
使用南丁格尔玫瑰图,我们能把人群在公园区域的分布和性别分布规律显示在一张图上,让更多的信息呈现在一张图表上。这既能节省空间,也便于人们高效率地获取更多信息。但是,太多的信息聚集也会显著增加图表复杂度,减少图表的直观程度。就像这张南丁格尔图一样,它虽然简单,但直观性仍然不如之前用四个饼图和一个嵌套饼图的表达形式。
所以在我们实际可视化项目中,需要根据实际情况选择合适的解决方案,大部分情况下,我们需要在直观性和信息聚集程度上做一个取舍。
# 改变图形属性,强化数据差异
除了直观表达信息外,我们还可以采用一些其他的手段,比如,改变颜色、大小、形状等等,以此来强化数据之间的差异,这也是增强可视化图表中信息表达的一种手段。
比如说,北大可视化实验室设计的全国新冠病毒肺炎疫情晴雨表 (opens new window),就是使用颜色和方块大小,将增量数据很直观地表达出来,从而宏观地呈现出疫情的发展态势,这对于揭示疫情拐点来说非常有帮助。
颜色和面积、折线图的方向、直方图的高度差,这些方式都能比较鲜明地体现出数据之间差异。除此之外,我们在绘制图表的时候,还可以使用背景网格线,来辅助用户观察数据间的差异,发现数据之间的变化规律。
说到强调数据差异,有一种比较完美的图表,它就是股市中常用的蜡烛图,又叫做 K 线图 (opens new window)。股市里的 K 线图是一个非常成功的可视化案例,这个图表用颜色来强化数据的增减,还包含了许多其他有用的信息,让想要了解股市市场的人,能够从中分析出商品或者股票的价格走势,再做出相应的决策。
# 可视化设计原则
# 简单清晰原则
当我们刚开始进行可视化设计的时候总会认为,只有用尽可能多的数据,才能做出十分酷炫的效果。但实际上,可视化真正的价值并不是这些看上去酷炫的效果,而是准确地表达信息内容。一味地堆砌数据,只会让真正有用的信息淹没在干扰信息里,从而影响人们解读信息中真正有价值的内容。
# 视觉一致性原则
在可视化中,颜色对于强化信息有着非常大的帮助。配色良好的图表,不仅看起来赏心悦目,也能帮助我们快速定位到想要关注的信息。
一般来说,有两种比较常用的配色方案,分别是互补色方案和同色系方案。
互补色方案
当想要突出数据之间的差异时,我们可以用互补色来增强对比效果。所谓的互补色,就是指在饱和度和明度相同的情况下,色相值相差 180 度的一对颜色。因为互补色色相差距最大,所以它们并列时会产生强烈的视觉对比效果,这样能够起到强调差异的作用。
当然,我们实际进行数据对比的时候,并不会严格要求两个颜色是差异 180 度的互补色,而是会采用差异较大的,差值比较接近 180 度的两种颜色,这样也算是互补色。
同色系方案
同色系方案就是利用不同深浅的同色系颜色来表示不同的数据。同色系方案的对比没有这么强烈,它从视觉上给人的感觉更柔和,而且色彩的一致也能够减少我们看图表时的视觉疲劳,从而让人保持注意力集中,帮助我们理解图表信息。
在具体项目中,使用互补色还是同色系方案不是绝对的,我们要看具体的应用场景:
如果你想要突出数据项之间的差异,那么采用对比色方案;
如果你想要让人长时间关注,尤其是一些复杂的大型图表,那么采用同色系方案就是更好的选择。
# 信息聚焦原则
长度、高度、大小、形状、颜色、透明度等等都可以用来表示变量。这样,我们就能在一张图上表示多元变量,同时我们改变这些属性还可以让信息更加聚焦。
比如这张天气预报的图表 (opens new window)。
图表上同时显示了温度、天气、风向、风速、浪高这些变量,每个变量都采用了不同的形式来展示,区分度很好,内容非常清晰也很聚焦。另外,图表上的除了用不同的 y 轴高度来表示风速之外,还采用红色、黄色、绿色这三种颜色标记了不同的风速等级,这就是用两种方式表示同一个元素。
一般来说,我们用一个图属性来表示一个变量,比如箭头方向表示风向,高度表示风速,但如果我们要强调某个变量的时候,我们也可以用超过一个属性来表示,比如用颜色和高度同时表示风速,这就起到了强调的作用。
类似的例子还有这种 (opens new window)。
总之,我们可以将相关的多元变量聚合在一张图表上,用来更聚焦地表达多元信息。不过,这么做的时候,我们要确定我们需要的信息真的包括了这些多元变量,并且它们彼此是有相关性的,否则我们还是应该考虑将它们拆分或者过滤掉无用的信息,这样才不违背前面的简单清晰原则。
# 高可访问性原则
在可视化设计中,我们也同样需要考虑设计高可访问性。
可视化的无障碍设计,主要体现在色彩系统上。要知道,我们的用户可能包含视觉障碍人群,而且我们的图表可能呈现在不同的设备上,从高端的 4K 显示屏、普通液晶显示器到投影仪、彩色打印机或者黑白打印机。
因此,即使我们设计的颜色在我们看来已经足够有差异性了,也可能在一些低色彩分辨率的设备上表现得不那么友好,甚至会给视觉障碍人士带来困扰。
这就要求我们在设计上要尽量避免对视觉障碍人士不太友好的配色,比如,用黄色和黄绿色来区分内容。在使用同色系配色方案的时候,我们也要注意色彩在明亮度和饱和度上要有足够的差异,以便于在黑白打印机等设备上打印出来的图表也有足够的区分度。
此外,我们还可以用一些色彩检查工具来辅助配色。
在 Photoshop 中,选择视图 -> 校样设置 -> 红色盲型 / 绿色盲型之后,就能看到我们设计的图表颜色在视觉障碍人群眼中的效果了。
如果不怎么用 Photoshop,还可以访问 color-blindness.com (opens new window) 的在线服务,通过上传图片来检查配色。
除了颜色之外,文字提示信息也需要考虑可访问性。
首先,提示字体的大小要适中,并且足够清晰。其次,要对于老年人和视力不好的人群提供缩放字体的功能,这样能够在很大程度上改善可访问性。
虽然网页本身提供了文字内容缩放的功能,但是图表库可能没有考虑文字缩放后的布局呈现。比如 Echarts 的这个图表,如果我们放大浏览器上的文字,它们就会叠在一起,完全不可阅读,这就是一个可访问性不太好的反面案例。
总之,图表设计的目的是给人阅读的,我们只有在可访问性上下功夫,才能让更多的人读懂图表,更好地发挥出它的价值。











