HTML5 WebGL 实现 3D 地图助力新型冠状病毒疫情实时数据可视化

前言

2020年开始就黑天鹅不断,美伊搞翻,英国脱欧,俄罗斯政改。对我们国内来说最要命的是眼瞅要过年了,又来了个新型冠状病毒。疫情发展迅速,比非典有过之而无不及。看着医护人员一个个冲锋在前,我们却只能靠在家里躺着为国家做贡献。N天过后,却发现整天葛优躺也会把人憋坏。既不能上前线,又闲的难受。不如活动活动,索性做个疫情监测系统。也给大家提供一条不同的了解疫情的途径。

在这之前,笔者也参与过不少医院信息化相关的项目,比如去年参与的上海某知名妇产科医院的智慧医院 3D 可视化系统。现在在国内新医改政策的推动下,各大医疗机构对医疗信息化和智能化的需求增长迅速。而像智慧医疗、智慧临床、云上医疗、AI 辅助诊断等的不断应用,即让医疗机构能够拥有更高的服务效率,更优的资源配置,更低的运营成本。同时也可以为患者提供更便捷和人性化的服务。

下面是上海某知名妇产科医院的效果:

结合之前在医疗可视化方面的经验,对于这次疫情地图,为了有别于现有诸多页面的千篇一律,让大家更直观方便的了解实时疫情信息。我这里在网页前端同时结合了 2D 和 3D。

先来看页面加载效果:

预览地址:http://www.hightopo.cn/demo/coronavirus/

系统介绍:

该系统总共包括两部分,分别是 2D 数据面板和 3D 地图。

  • 2D数据面板包含:
    1. 左侧的每日统计数据,该数据显示最近一段时间每天的确诊人数,并根据疫情变化定时刷新。同时,该部分还与地图及右侧数据联动。切换不同的日期后,地图颜色及右侧详细信息会跟着显示历史数据。
    2. 表格详细信息,该表格用来显示各省及各市的疫情详细信息。包括疑似,确诊,治愈,死亡数据。该表格数据根地图及每日统计数据联动。
    3. 疫情增长柱状图,该柱状图由康复,确诊,死亡三部分组成。显示近7日的疫情数据。
  • 3D地图包含:
    1. 各省颜色随动变化,即各省区域颜色根据该省确诊人数变化,确诊人数越多,颜色越深。
    2. 武汉地区人口输出动画,即武汉地区输出人口到各省的比例。
    3. 指定省份突出显示,用户点击某个省份后,该省份在地图上高亮显示,同时,右侧表格会显示当前省份的详细数据。点击背景恢复显示全国数据。

系统开发:

1. 寻找数据源

既然各大网站都提供疫情实时地图,我们就没必要在这上面多花时间。官方的如疾控中心 (CDC), 大厂如 BBA 三家。还有最近异军突起的丁香园。

打开各家网站经过在 console 里面一顿查找,最终选中其中一家的数据作为来源。

这里的数据都是标准的 json 格式,具体怎么绑定到 2D 部分难度不大,不在此展开说明。下面主要介绍 3D 部分的几处关键代码。

2. 关键代码实现:

1)  根据每个地区疫情的不同情况,将该地区标记为不同颜色。

首先挑选合适的颜色,之后,由浅到深定义这几种颜色到对象 MyConst:

1 const MyConst = {
2   COLOR_0: 'rgb(235,235,235)',
3   COLOR_9: 'rgb(245,165,130)',
4   COLOR_99: 'rgb(190,30,10)',
5   COLOR_999: 'rgb(130,25,25)',
6   COLOR_9999: 'rgb(77,5,5)',
7 };

再根据每个省的确诊数量使用不同的颜色。this.areaDataObj 保存的是各个省的数据对象,其内容如下:

 最后,根据每个地区的确诊人数(total_confirm)为该地区设置不同的颜色。代码如下:

 1   refreshAreaColor() {
 2     for (const key in this.areaDataObj) {
 3       if (Object.prototype.hasOwnProperty.call(this.areaDataObj, key)) {
 4         const dataNode = this.areaDataObj[key].data; // 省区域 Node
 5         const total_confirm = this.areaDataObj[key].total_confirm; // 该省累计确诊数量
 6         switch (true) {
 7           case total_confirm == 0:
 8             dataNode.s('shape3d.blend', MyConst.COLOR_0);
 9             break;
10           case total_confirm <= 9:
11             dataNode.s('shape3d.blend', MyConst.COLOR_9);
12             break;
13           case total_confirm <= 99:
14             dataNode.s('shape3d.blend', MyConst.COLOR_99);
15             break;
16           case total_confirm <= 999:
17             dataNode.s('shape3d.blend', MyConst.COLOR_999);
18             break;
19           default:
20             dataNode.s('shape3d.blend', MyConst.COLOR_9999);
21             break;
22         }
23       }
24     }
25   }

 

2) 动画展示

 根据官方数据,武汉地区总人口超过1400万,春节前流出人口有500多万。这些人员的去向也是大家关心的问题。这里增加了武汉到国内各省的人口流动动画。另外,每个城市点也增加了动画来进行位置展示。

上图总共包括三部分动画:

  1. 每个城市点的转动;
  2. 每个城市点圆柱增长动画;
  3. 城市飞线(人口流动)的动画;

这三个动画可以通过一个函数实现,代码如下。要注意 data.setRotationMode() 方法不同的坐标轴顺序会导致不同的显示效果。

 1   start3DAnim() {
 2     // 设置城市点旋转模式
 3     this.cityPoints.forEach((data) => {
 4       data.setRotationMode('zxy');
 5     });
 6 
 7     let uv_offset = 0;
 8     let point_ang = 0;
 9     let c_h = 2, wh_c_h = 2; 
10     const wuhanCylinder = this.dm3d.getDataByTag('wuhanCylinder'); // 武汉地区单独处理
11     setInterval(()=> {
12         point_ang = point_ang + 0.01;
13         uv_offset = uv_offset + 0.01;
14         
15         c_h = c_h + 0.2; 
16         if(c_h > 20) {
17             c_h = 2;
18         }
19         wh_c_h = wh_c_h + 0.6;
20         if(wh_c_h > 60) {
21           wh_c_h = 2;
22         }
23         // 城市点旋转
24         this.cityPoints.forEach((val) => {
25             val.setRotationZ(point_ang);
26         });
27         // 城市点圆柱增长
28         this.cylinders.forEach((val) => {
29             val.setTall(c_h);
30         });
31         // 武汉地区单独处理
32         wuhanCylinder.setTall(wh_c_h);
33         // 城市间飞线
34         this.movingLines.forEach((val) => {
35             val.s('shape3d.uv.offset', [-uv_offset, 0]);
36         });
37         
38     }, 30);

3) 鼠标移动到省区域之上时悬浮高亮

要实现悬浮高亮需要利用 onHover 或者 onEnter 和 onLeave 事件。由于 onHover 需要悬停一段事件才能触发,为了提高反应速度,这里通过检测 onEnter 和 onLeave 事件修改区域的边界颜色来突出某个省。this.proviences 为各个省名称的集合。this.proviences.indexOf(selectedProvience) >= 0 用来确保只有指定的省区域上面才执行悬浮高亮。

代码如下:

 1   handleInteractive3d(e) {
 2     let {
 3       data, // 事件指向的对象
 4       kind // 事件类型
 5     } = e; // e为事件
 6     if (!data) {
 7       return;
 8     }
 9 
10     const selectedProvience = data.getDisplayName(); // 省名称
11 
12     // 悬浮高亮
13     const onEnterBorderColor = "rgb(250,250,87)";
14     const onLeaveBorderColor = "rgb(61,61,61)";
15     if (kind == 'onEnter' && this.proviences.indexOf(selectedProvience) >= 0) {
16       this.updateAreaBorder(data, onEnterBorderColor);
17       return;
18     }
19     if (kind == 'onLeave' && this.proviences.indexOf(selectedProvience) >= 0) {
20       this.updateAreaBorder(data, onLeaveBorderColor);
21       return;
22     }
23     ……    
24   }
25 
26   updateAreaBorder(data, color) {
27     data.s("wf.color", color); // 更新边框颜色
28   }

4) 点击某个省将其从地图拉高,同时弱化其他区域并更新表格内容。点击背景恢复原样。

思路是这样:拉高某个区域可通过改变其 y 方向的值。弱化其他区域可通过设置透明度来实现。代码如下:

 1   handleInteractive3d(e) {
 2     let {
 3       data,
 4       kind
 5     } = e;
 6     // 如果点击背景,则恢复区域颜色及y轴方向的值。
 7     if (kind == 'clickBackground') {
 8       this.updateAreaOpacity(1); //还原透明度
 9       this.updateTable(); // 恢复表格内容为全国数据
10       return;
11     }
12 
13     if (!data) {
14       return;
15     }
16 
17     const selectedProvience = data.getDisplayName();
18 
19     // 点击拉高
20     if (kind == 'clickData' && this.proviences.indexOf(selectedProvience) >= 0) {
21       this.showProvienceData(selectedProvience);
22     }
23   }
24 25 
26   updateAreaOpacity(opacity) {
27     for (const key in this.areaDataObj) {
28       if (Object.prototype.hasOwnProperty.call(this.areaDataObj, key)) {
29         const dataNode = this.areaDataObj[key].data; 
30         dataNode.s('shape3d.opacity', opacity); // 改变透明度
31         const [x, y, z] = dataNode.getPosition3d();
32         dataNode.setPosition3d(x, 0, z); // 恢复Y方向位置
33       }
34     }
35   }
36 
37   showProvienceData(selectedProvience) {
38     const data = this.provAreas[selectedProvience];
39     this.updateAreaOpacity(0.3); // 通过设置透明度为0.3来弱化周围区域
40     data.s('shape3d.opacity', 1); // 设置透明度为1来突出选中区域
41 
42     const [x, y, z] = data.getPosition3d();
43     if (y == 0) {
44       data.setPosition3d(x, 5, z); // 拉高该区域
45     } else {
46       data.setPosition3d(x, 0, z);
47     }
48     this.updateTable(selectedProvience); // 更新表格内容为该省数据
49   }

结语:

数据可视化越来越普及,在工业物联网、电信、智慧医疗、智能交通等行业都有广泛的应用。但依我看来,数据可视化不仅仅是将数据用某种图表展示出来。更重要的,是要给大家带来良好的用户体验。数据不但要展示,更要展示的优美、协调、重点突出。传统的 2D 普通页面已经成为过去式。WebGL 的出现给我们提供了更丰富的途径(3D)来展示原本呆板的数据。

正所谓,人们对美好事物的向往就是我们前端程序猿的奋斗目标。希望这次肺炎疫情能够尽快得到控制。武汉加油,中国加油!

 

 
posted @ 2020-02-24 08:47  HT学习笔记  阅读(3955)  评论(1编辑  收藏  举报