HTML5 WebGL 实现 3D 地图助力新型冠状病毒疫情实时数据可视化
前言
2020年开始就黑天鹅不断,美伊搞翻,英国脱欧,俄罗斯政改。对我们国内来说最要命的是眼瞅要过年了,又来了个新型冠状病毒。疫情发展迅速,比非典有过之而无不及。看着医护人员一个个冲锋在前,我们却只能靠在家里躺着为国家做贡献。N天过后,却发现整天葛优躺也会把人憋坏。既不能上前线,又闲的难受。不如活动活动,索性做个疫情监测系统。也给大家提供一条不同的了解疫情的途径。
在这之前,笔者也参与过不少医院信息化相关的项目,比如去年参与的上海某知名妇产科医院的智慧医院 3D 可视化系统。现在在国内新医改政策的推动下,各大医疗机构对医疗信息化和智能化的需求增长迅速。而像智慧医疗、智慧临床、云上医疗、AI 辅助诊断等的不断应用,即让医疗机构能够拥有更高的服务效率,更优的资源配置,更低的运营成本。同时也可以为患者提供更便捷和人性化的服务。
下面是上海某知名妇产科医院的效果:
结合之前在医疗可视化方面的经验,对于这次疫情地图,为了有别于现有诸多页面的千篇一律,让大家更直观方便的了解实时疫情信息。我这里在网页前端同时结合了 2D 和 3D。
先来看页面加载效果:
预览地址:http://www.hightopo.cn/demo/coronavirus/
系统介绍:
该系统总共包括两部分,分别是 2D 数据面板和 3D 地图。
- 2D数据面板包含:
- 左侧的每日统计数据,该数据显示最近一段时间每天的确诊人数,并根据疫情变化定时刷新。同时,该部分还与地图及右侧数据联动。切换不同的日期后,地图颜色及右侧详细信息会跟着显示历史数据。
- 表格详细信息,该表格用来显示各省及各市的疫情详细信息。包括疑似,确诊,治愈,死亡数据。该表格数据根地图及每日统计数据联动。
- 疫情增长柱状图,该柱状图由康复,确诊,死亡三部分组成。显示近7日的疫情数据。
- 3D地图包含:
- 各省颜色随动变化,即各省区域颜色根据该省确诊人数变化,确诊人数越多,颜色越深。
- 武汉地区人口输出动画,即武汉地区输出人口到各省的比例。
- 指定省份突出显示,用户点击某个省份后,该省份在地图上高亮显示,同时,右侧表格会显示当前省份的详细数据。点击背景恢复显示全国数据。
系统开发:
1. 寻找数据源
既然各大网站都提供疫情实时地图,我们就没必要在这上面多花时间。官方的如疾控中心 (CDC), 大厂如 BBA 三家。还有最近异军突起的丁香园。
- CDC: http://2019ncov.chinacdc.cn/2019-ncov/
- 百度:https://voice.baidu.com/act/newpneumonia/newpneumonia
- 腾讯:https://news.qq.com/zt2020/page/feiyan.htm
- 丁香园:https://ncov.dxy.cn/ncovh5/view/pneumonia
打开各家网站经过在 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多万。这些人员的去向也是大家关心的问题。这里增加了武汉到国内各省的人口流动动画。另外,每个城市点也增加了动画来进行位置展示。
上图总共包括三部分动画:
- 每个城市点的转动;
- 每个城市点圆柱增长动画;
- 城市飞线(人口流动)的动画;
这三个动画可以通过一个函数实现,代码如下。要注意 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)来展示原本呆板的数据。
正所谓,人们对美好事物的向往就是我们前端程序猿的奋斗目标。希望这次肺炎疫情能够尽快得到控制。武汉加油,中国加油!