从地图看疫情
前段时间我们发布了一张可以动态展示全球新型冠状病毒疫情趋势的地图:
https://www.mapbox.cn/coronavirusmap/
这张图经过N个版本的迭代,结合了来自东西方灵感的碰撞,最终形成了目前大家看到的还算令我们满意的一个版本。今天和大家谈谈开发这张地图背后的故事:我们想通过地图表达什么以及我们通过哪些技术方案让疫情地图得以实现。
起源 - 最初的版本
故事开始于春节在家自我隔离期间,当时接到的老板C&F从大洋彼岸打来的电话,寒暄之后我们决定简单的做一张可以反映疫情发展趋势的地图。但此时的我们并不知道我们最终要做成什么样子,需要通过地图表达出什么样的故事,只能...硬着头皮干吧!
最初的热力图
谈到“趋势”、“发展”之类的关键字,第一个想到的自然是热力图了!于是吭哧吭哧的搞了一个晚上,有了我们最初的一个版本:
现在看来我们的第一版确实是有那么一点粗糙:几个“随意”放在一起的面板,加上“花了一点时间”做的一个时间控件(话说后来上线后居然有美国同事说很喜欢这个时间控件,问我是用的哪个库的时候,我欣喜若狂的在心里说“我才没用第三方库呢,纯手工打造!”),再叠上一个没有经过过多修改的热力图,就是我们的第一个版本了。本以为可以完工了,但是之后经过和联合出品方的“无数”轮讨论之后,突然茅塞顿开,大家也达成了一致“其实数据的表达不仅仅只有热力图可以做到,我们可以做的更多,可以让让数据更有力量!”。
数据可以更有力量
同一份数据按照不同的展现形式可以带给人们不同的信息,就像我们在工作中通常习惯用饼状图来表达比例,用折线图来表达变化趋势一样,选择正确的表达形式可以带给观众不同的感受,自然,地图也可以这么干!于是后来我们在这张专题图中引入了图层的概念,我们决定用不同的展示效果让访问者看到不同的东西:当你关注疫情是如何蔓延的时候那么请打开热力图,当你想看不同省份之间对比情况的时候请打开填色图,当你想看哪个城市的治愈力最高的时候请打开分类集合图...... 同时数据经过处理,我们还可以通过不同维度去分析,比如我们可以把数据分为累计人数、新增人数、现有人数等,每个维度都能表达不同的故事:累计人数可以告诉我们整个事件的严重程度,每日的新增人数可以让我们看到不同地方疫情发展情况,现有人数又可以告诉我们胜利就在不远的前方!
于是就有了下面的改进:
改进的曲线图
最早的时候,我们的曲线图和网络上看到的大多数曲线图一样,“暴力的”把确诊、治愈、疑似、死亡放在一张折线图上:
但是通过这张累计人数图我们只能看到各种线条在不断地增加,直到疫情结束后这些曲线保持在一定的数值不再增加。而我们更想让观众看到更多信息,不仅可以看出病毒扩展趋势还能看到治愈率等数据背后的东西,于是我们将折线改成了堆叠图,一张把现有确诊、疑似、治愈、死亡人数叠加在一起团的一张图。
通过这张图,我们可以清晰的看出各种状态的分类所占的比例,能看到随着有效措施不断地实施,表达治愈人数的绿色区域不断扩大,表达确诊人数的红色区域不断减少,能看到随着医疗科技的投入没来及判断的疑似人数变得越来越少。我们期待着不久的未来绿色完全覆盖红色!
相关链接:
这里我们是用来了 Apache ECharts 实现了折线图和堆叠图,官网链接如下:
- Apache ECharts https://www.echartsjs.com/
改进的热力图
由于第一个版本的热力图使用了默认的配置参数,我们发现这么几个问题:
- 疫情人数偏少的地区由于透明度过低,不易看出
- 色彩的搭配相对来说比较单调
- 阈值配置的不是特别合理导致人数高低的过渡不是特别的明显
于是我们对热力图进行了调整:首先我们提高了低数值地区的热力图透明度,这样即便比较小的增长也能通过热力图察觉出来。然后我们又调整了过渡颜色的阈值,从数据上可以看到整个列表中的数值并不是线性分布的,比如某天的排序第一位的值是六万多,而第二及其之后的却是几百到一千的范围,这就意味着我们在设计阈值的时候,要把焦点放在 0.01-0.0001 的数据范围内(其中0.01是1000相对于60000的位置,0.0001是大概6相对于60000的位置)着重调整好这部分的数值就可以让热力的过渡更加柔和清晰。于是我们有了这样的一张修改后的热力图:
在我们洋洋得意的时候,修改后的热力样式得到了一些设计专业人士的吐槽,在一个内部的备忘录中有同事甚至写了很长的篇幅来阐述热力图的配色问题。这可难为了我这种非专业设计人士,没办法那就硬着头皮一点一点来处理吧,好在文章中给了很多有用的链接,当做学习一篇一篇的看下来。看完之后总结下来主要分为下面几个问题:
1. 热力失真
数据的热力情况有失真,尤其是地图缩放级别小的情况下,感觉整个中国东边都被感染了。
Emmm... 这个可能是热力图中比较容易出现的一个问题,如果大家知道热力图的原理就很容易明白为什么会出现这种情况了:通常情况下,在绘制过程中我们使用一定半径的半透明色的圆形填色来代表每一个点,绘制过程中会把所有的点根据权重绘制在一起,绘制圆的多的地方颜色会比较深反之较浅,这样就可以得到一张深浅不一,但是可以反映出趋势的图。拿到这张图再重新根据每个像素的深浅程度重新上色就得到了热力图。如果我们保持在不同的缩放级别都用同样的半径来绘制就会出现热力被“放大”的情况(想象一下一个10px的圆圈,在地图18级别的时候可能只有一栋楼的大小,但是在缩放级别为2的情况下可能比一个省份还要大)。
不过,好在Mapbox官方的热力图可以精准的控制热力绘制的半径、权重、透明度等一些列细微参数,我们通过尝试重新配比不同缩放级别的绘制参数,提高了整体热力图的可读性。
相关链接:
Mapbox GL JS中提供了官方的热力图图层,并且开放了诸如color
,intensity
,opacity
,radius
,weight
等细节参数,结合Expression可以方便的实现在不同缩放级别顺滑的切换到不同的参数值,具体可以通过以下链接查看:
- Mapbox GL JS - Heatmap https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#heatmap
2. 配色问题
彩虹色
事实上彩虹色我们最常见到的热力颜色,这种颜色相对于单色的颜色变化来说确实很吸引人,但是这种颜色很容易让人产生误解,用作可视化的话可能不是一个好主意。主要的原因是:
- 人们会把自己觉得最亮的颜色作为最大值,而不同的人对此又有不同的认知:有人认为是橙色最亮,有人则认为黄色最亮,所以很容易造成误解。
- 颜色的变化并不是平滑的,如下面的图,对比橙色(数值1)和红色(数值3)之间的色差(3-1=2),以及蓝色(数值1)和绿色(数值0.8)之间的色差(1-0.8=0.2),很多人会觉得橙色到红色的变化(2)比较少,而蓝色到绿色(0.2)的变化会比较大,但是实际上恰恰相反。
相关链接:
下面的链接给出了热力图配色应该怎么做不应该怎么做的一些建议,可供参考 :
- Dos and don’ts for a heatmap color scale https://blog.bioturing.com/2018/09/24/heatmap-color-scale/
色觉异常人士与配色方案
What?! 色觉异常人士?讲真,作为程序员的我通常在开发的过程中真的很少会考虑到这一类人群的感受,但是既然同事很严肃认真的提出了这个问题,那么我们就好好调研一番吧。据说有8%的男性和0.5%的女性有看到的颜色和大部分人看到的不一样,比如下面的两张图,左边的饼图是全色图,右边的饼图是某种色觉异常人士看到的效果,色觉异常的人们眼中颜色比正常色觉少了不少。基于此在看左下角的两个条形的时候某些色觉异常的人可能就无法区分前面两格颜色的不同。如果我们在开发的过程中,留意到这一点,通过对颜色的重新调配(右下角)色觉异常的人也能区分出不同的色块。
色觉异常的问题解决了,剩下的就是配色的问题了,但如何能使用正确配色对于我这个”外行人“来说就略显困难了,研究了几篇文章之后,我们决定采用”有意义“的配色方案。所谓有意义就是整体配色要符合主题,比如如果你想做一张表示极寒天气的热力图的话就可以用冷色的渐变(比如浅蓝到深蓝)来表示,如果你想表示植物的覆盖率的话就可以借鉴大部分人认知的植物和绿色的关系采用不同明度的绿色作为你的配色方案。基于这个结论,对于疫情来说,我最终选取了以表达预警的黄色渐变为主,辅以表示危险的红色作为最严重的表示,最终调整出下面的热力图:
相关链接:
关于如何设计出对色觉异常人士有好的热力图:
- Designing Color-Blind-Friendly Heatmaps https://wistia.com/learn/culture/heatmaps-for-colorblindness
在数据可视化中如何选择出正确的配色方案:
- Finding the Right Color Palettes for Data Visualizations https://blog.graphiq.com/finding-the-right-color-palettes-for-data-visualizations-fcd4e707a283
进化 - 图层
填色图
填色图是我们目前在网络上看到的最常见的一种表现形式,这里就不多解释这张图表达的含义了。 我们在做填色的时候,根据数据逇精度实现了全球国家、主要国家省份(洲)、城市的填色。
世界级别疫情图
省份/州级别疫情图
城市级别疫情图
地图发布之后,有很多朋友问我,我们是如何实现不同国家的不同填色的,因为在一些地图的SDK中,虽然可以支持自定义配色,但是只能针对所有的同一元素(比如说所有的国家、所有的省份)统一进行配色,不能单独的调整每个国家或者城市的配色。其实在Mapbox的地图中,我们可以在Studio中上传国家、省份、洲、城市等多边形数据,然后在Style中引用这个数据图层,也可以使用我们自带的商业边界数据。同时我们的GL JS SDK 支持根据瓦片中的数据进行配色的的功能,在Mapbox中我们把它叫做Expression。Expression极其强大,我们只需要修改Style JSON文件就可以可以轻松的控制不同国家地区的颜色,比如如果在Style中有个表示城市的layer,我们只需要定义他的 fill 入下:
[
"match",
["get", "name"],
"武汉", "red",
"上海", "rgba(255,255, 255, 0.0)",
"美国", "rgba(255,255, 0, 0.0)"
]
在渲染的时候,就会按照上述规则进行渲染。
你甚至可以直接使用瓦片数据中的字段,比如在瓦片中我们关于国家的属性如下:
{
"osm_id": 424314830,
"name_en": "Russia",
"type": "country",
"abbr": "RU",
"name": "俄罗斯",
}
我需要把国家名称的英文显示出来,只要将Style中国家名称的text-field
字段从默认的中文{name}
为{name_en}
即可,甚至Express还支持各种计算、条件语句等。打开脑洞,假设你拿到一个变态的需求,需要把中国以外的国家的国家名称显示成他们的国家的缩写加上osm_id
的最后4位(即对于如上数据需要显示Russia_4830
)。这个需求实际上已经很复杂了,我们需要判断国家是不是中国,并且要去取其他国家的osm_id
进行数学计算,然后再转换成字符串和国家的缩写abbr
结合在一起。但是我要告诉你的是,通过Mapbox的Expression,你可以一句话搞定这一切,是不是很腻害!!具体的你可以参考后面提供的链接。
参考链接:
- Mapbox Expression https://docs.mapbox.com/help/glossary/expression/
聚合图
聚合图也是一个不错的数据展示方案,它很简单,通过不同大小颜色的圈就能清楚的表达出一个区域的具体数值,简单却很有效果。
从图上可以看到,当我们把地图缩小到一定级别,比如说国家级别的时候,我们会把国家区域的数值聚合在一起并显示。读者朋友可以花两分钟想象一下,如果手动的实现这些功能需要做哪些事情?大部分情况下我们需要在不同的缩放级别去手动的计算聚合后的数据,然后再展示。但是在Mapbox中我们的数据有一个很神奇的cluster
参数,把它打开即可通过一行代码实现数据的自动聚合,你也可以调整聚合的大小,比如我想把20px范围内的所有的点都聚合在一起,然后显示,只要指定clusterRadius
为50即可。
另外图上的不同大小颜色的圆圈也可以通过Mapbox的官方circle Layer直接实现,具体的可以参考后续的参考链接。
参考链接:
以下两个是文章中提到的Cluster和圆圈图层的链接:
-
Mapbox Sources Cluster https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#geojson-cluster
-
Mapbox circle Layer https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#circle
以下是Mapbox官方示例中和该效果图类似的示例代码:
- Create and style clusters Demo https://docs.mapbox.com/mapbox-gl-js/example/cluster/
分类图
有了热力图、填色图、聚合图,我们可以很好地看出疫情蔓延的趋势,以及局部地区的严重程度,但是我们有没有办法看到局部疫情的治愈进程呢?我们引入了分类图的概念,分类图也是我们最喜欢的数据表现形式之一,看起来像一个彩色的圆环,由一个不定半径的圆以及外部不同颜色的环来表示。
圆的半径根据中心数值的大小而变化,外圈的红色、绿色、灰色部分分别代表确诊、治愈、死亡人数,中心数值是这几类数值的总和。通过这种分类图,我们可以一眼就看出当前地方的疫情治愈率、死亡率等状态。我们会观察到随着时间的发展绿色部分会变得越来越多,相应的红色区域也会逐渐减少,说明我们采取了强有力的措施。同时,如果我们把不同省份国家等地区的分类图放在一起,也能够很明显的看出不同地方的疫情治愈难易程度。
分类图制作起来相对麻烦,和聚合图一样我们要根据不同的缩放级别进行对数据聚合,然后针对每一个圆环,我们要计算出不同类型的数值相对总数的百分比,然后再转换成角度进行绘制。该例中我们使用了Mapbox的自定义图标功能结合SVG去绘制四周的环,在Mapbox官网上也有一个类似的示例可供查看。另外由于每个圆环都是由SVG组成的,对于海量的数据可能存在性能问题,我们建议如果你的数据过于庞大可以把SVG换成基于WebGL的实现来完成。
相关链接:
- Display HTML clusters with custom properties https://docs.mapbox.com/mapbox-gl-js/example/cluster-html/
蜕变 - 多维攻击
数据动起来之后确实可以让我们真实的感受到了病毒的扩散之快形势之严峻,但是拿到每日统计的数据我们还能做出更有意义的分析么?当然可以!篇幅有限我们拿众多维度中的两个维度的数据为例。
维度一:每日新增人数
既然我们有了每日的确诊人数,如果我们尝试把每日的人数减去前一天的人数那么我们岂不是就有了每日新增人数?
安排!
在我们的几个主要图层中我们都加入了每日新增图,感受一下每日新增的热力图:
这是一份被加快了的数据,红色的区域表示每日新增超过100人的地方,从中你看到了什么?是不是能感受到疫情前期的快速爆发以及到后期随着强有力的措施的实施疫情被很好的控制了?如果你想的话,你可以看到爆发区域的特点,你也能找到异常爆发的区域等等,这些信息在某些场合可能会有一定的价值。
举个实际的例子,在观察每日新增热力图的时候,我们突然发现山东济宁附件突然有一个爆发的红色区域, 打开我们的日增聚合图发现当日该地区新增了201例, 后经过查询发现和当地权威机构发布的疫情情况一致。
维度二:现有确诊人数
现有确诊人数也可以通过数学公式很容易的计算出来 现有确诊人数 = 累计确诊人数 - 累计治愈人数 - 累计死亡人数。与传统的累计图不同,用这种图可以让人们对疫情的爆发和退去有一个更宏观的认识,仔细观察如下热力图的最后几帧可以看到疫情有明显的消退趋势。
总结
地图和数据可视化是一个很有意思的话题,通过可视化的辅助我们可以更直观的观察到数据的特点,一张好的可视化图在某些时候甚至可以给我们的重要决策提供支持。最后需要声明的是,以上案例中的所有数据均收集自权威机构、媒体、网络,对数据的分析和解读不代表官方意见,如有冲突以官方公布消息为准。致敬抗疫一线的英雄们,希望疫情早日结束,此时此刻我们”静“待疫情消散春暖花开时,摘下口罩与亲朋好友走上街头,繁华与共!