徐庆义 發表於 2020-2-24 08:47:00

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

<h3><strong>前言</strong></h3>
<p>2020年开始就黑天鹅不断,美伊搞翻,英国脱欧,俄罗斯政改。对我们国内来说最要命的是眼瞅要过年了,又来了个新型冠状病毒。疫情发展迅速,比非典有过之而无不及。看着医护人员一个个冲锋在前,我们却只能靠在家里躺着为国家做贡献。N天过后,却发现整天葛优躺也会把人憋坏。既不能上前线,又闲的难受。不如活动活动,索性做个疫情监测系统。也给大家提供一条不同的了解疫情的途径。</p>
<p>在这之前,笔者也参与过不少医院信息化相关的项目,比如去年参与的上海某知名妇产科医院的智慧医院 3D 可视化系统。现在在国内新医改政策的推动下,各大医疗机构对医疗信息化和智能化的需求增长迅速。而像智慧医疗、智慧临床、云上医疗、AI 辅助诊断等的不断应用,即让医疗机构能够拥有更高的服务效率,更优的资源配置,更低的运营成本。同时也可以为患者提供更便捷和人性化的服务。</p>
<p>下面是上海某知名妇产科医院的效果:</p>
<p><img src="https://img2018.cnblogs.com/common/1496396/202002/1496396-20200207114328256-537104205.png"></p>
<p>结合之前在医疗可视化方面的经验,对于这次疫情地图,为了有别于现有诸多页面的千篇一律,让大家更直观方便的了解实时疫情信息。我这里在网页前端同时结合了 2D 和 3D。</p>
<p>先来看页面加载效果:</p>
<p><img src="https://img2018.cnblogs.com/common/1496396/202002/1496396-20200207114902216-1394844506.gif"></p>
<p>预览地址:http://www.hightopo.cn/demo/coronavirus/</p>
<h3><strong>系统介绍:</strong></h3>
<p>该系统总共包括两部分,分别是 2D 数据面板和 3D 地图。</p>
<ul>
<li>2D数据面板包含:</li>
</ul>
<ol>
<li style="list-style-type: none"><ol>
<li>左侧的每日统计数据,该数据显示最近一段时间每天的确诊人数,并根据疫情变化定时刷新。同时,该部分还与地图及右侧数据联动。切换不同的日期后,地图颜色及右侧详细信息会跟着显示历史数据。</li>
<li>表格详细信息,该表格用来显示各省及各市的疫情详细信息。包括疑似,确诊,治愈,死亡数据。该表格数据根地图及每日统计数据联动。</li>
<li>疫情增长柱状图,该柱状图由康复,确诊,死亡三部分组成。显示近7日的疫情数据。</li>
</ol></li>
</ol>
<ul>
<li>3D地图包含:</li>
</ul>
<ol>
<li style="list-style-type: none"><ol>
<li>各省颜色随动变化,即各省区域颜色根据该省确诊人数变化,确诊人数越多,颜色越深。</li>
<li>武汉地区人口输出动画,即武汉地区输出人口到各省的比例。</li>
<li>指定省份突出显示,用户点击某个省份后,该省份在地图上高亮显示,同时,右侧表格会显示当前省份的详细数据。点击背景恢复显示全国数据。</li>
</ol></li>
</ol>
<h3><strong>系统开发:</strong></h3>
<h3>1. 寻找数据源</h3>
<p style="margin-left: 30px">既然各大网站都提供疫情实时地图,我们就没必要在这上面多花时间。官方的如疾控中心 (CDC), 大厂如 BBA 三家。还有最近异军突起的丁香园。</p>
<ul>
<li>CDC: http://2019ncov.chinacdc.cn/2019-ncov/</li>
<li>百度:https://voice.baidu.com/act/newpneumonia/newpneumonia</li>
<li>腾讯:https://news.qq.com/zt2020/page/feiyan.htm</li>
<li>丁香园:https://ncov.dxy.cn/ncovh5/view/pneumonia</li>
</ul>
<p style="margin-left: 30px">打开各家网站经过在 console 里面一顿查找,最终选中其中一家的数据作为来源。</p>
<p style="margin-left: 30px">这里的数据都是标准的 json 格式,具体怎么绑定到 2D 部分难度不大,不在此展开说明。下面主要介绍 3D 部分的几处关键代码。</p>
<h3>2. 关键代码实现:</h3>
<h4>1)&nbsp; 根据每个地区疫情的不同情况,将该地区标记为不同颜色。</h4>
<p><img src="https://img2018.cnblogs.com/common/1496396/202002/1496396-20200207154728590-1334056725.png"></p>
<p>首先挑选合适的颜色,之后,由浅到深定义这几种颜色到对象 MyConst:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> const MyConst =<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">2</span>   COLOR_0: 'rgb(235,235,235)'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">3</span>   COLOR_9: 'rgb(245,165,130)'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">4</span>   COLOR_99: 'rgb(190,30,10)'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">5</span>   COLOR_999: 'rgb(130,25,25)'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">6</span>   COLOR_9999: 'rgb(77,5,5)'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)">7</span> };</pre>
</div>
<p>再根据每个省的确诊数量使用不同的颜色。this.areaDataObj 保存的是各个省的数据对象,其内容如下:</p>
<p><img src="https://img2018.cnblogs.com/common/1496396/202002/1496396-20200207160115212-1960879419.png"></p>
<p>&nbsp;最后,根据每个地区的确诊人数(total_confirm)为该地区设置不同的颜色。代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 0, 0, 1)">refreshAreaColor() {
</span><span style="color: rgba(0, 128, 128, 1)"> 2</span>   <span style="color: rgba(0, 0, 255, 1)">for</span> (const key <span style="color: rgba(0, 0, 255, 1)">in</span> <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.areaDataObj) {
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span>       <span style="color: rgba(0, 0, 255, 1)">if</span> (Object.prototype.hasOwnProperty.call(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.areaDataObj, key)) {
</span><span style="color: rgba(0, 128, 128, 1)"> 4</span>         const dataNode = <span style="color: rgba(0, 0, 255, 1)">this</span>.areaDataObj.data; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 省区域 Node</span>
<span style="color: rgba(0, 128, 128, 1)"> 5</span>         const total_confirm = <span style="color: rgba(0, 0, 255, 1)">this</span>.areaDataObj.total_confirm; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 该省累计确诊数量</span>
<span style="color: rgba(0, 128, 128, 1)"> 6</span>         <span style="color: rgba(0, 0, 255, 1)">switch</span> (<span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)"> 7</span>         <span style="color: rgba(0, 0, 255, 1)">case</span> total_confirm == 0<span style="color: rgba(0, 0, 0, 1)">:
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span>             dataNode.s('shape3d.blend'<span style="color: rgba(0, 0, 0, 1)">, MyConst.COLOR_0);
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span>             <span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">10</span>         <span style="color: rgba(0, 0, 255, 1)">case</span> total_confirm &lt;= 9<span style="color: rgba(0, 0, 0, 1)">:
</span><span style="color: rgba(0, 128, 128, 1)">11</span>             dataNode.s('shape3d.blend'<span style="color: rgba(0, 0, 0, 1)">, MyConst.COLOR_9);
</span><span style="color: rgba(0, 128, 128, 1)">12</span>             <span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">13</span>         <span style="color: rgba(0, 0, 255, 1)">case</span> total_confirm &lt;= 99<span style="color: rgba(0, 0, 0, 1)">:
</span><span style="color: rgba(0, 128, 128, 1)">14</span>             dataNode.s('shape3d.blend'<span style="color: rgba(0, 0, 0, 1)">, MyConst.COLOR_99);
</span><span style="color: rgba(0, 128, 128, 1)">15</span>             <span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">16</span>         <span style="color: rgba(0, 0, 255, 1)">case</span> total_confirm &lt;= 999<span style="color: rgba(0, 0, 0, 1)">:
</span><span style="color: rgba(0, 128, 128, 1)">17</span>             dataNode.s('shape3d.blend'<span style="color: rgba(0, 0, 0, 1)">, MyConst.COLOR_999);
</span><span style="color: rgba(0, 128, 128, 1)">18</span>             <span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">19</span>         <span style="color: rgba(0, 0, 255, 1)">default</span><span style="color: rgba(0, 0, 0, 1)">:
</span><span style="color: rgba(0, 128, 128, 1)">20</span>             dataNode.s('shape3d.blend'<span style="color: rgba(0, 0, 0, 1)">, MyConst.COLOR_9999);
</span><span style="color: rgba(0, 128, 128, 1)">21</span>             <span style="color: rgba(0, 0, 255, 1)">break</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">22</span> <span style="color: rgba(0, 0, 0, 1)">      }
</span><span style="color: rgba(0, 128, 128, 1)">23</span> <span style="color: rgba(0, 0, 0, 1)">      }
</span><span style="color: rgba(0, 128, 128, 1)">24</span> <span style="color: rgba(0, 0, 0, 1)">    }
</span><span style="color: rgba(0, 128, 128, 1)">25</span>   }</pre>
</div>
<p>&nbsp;</p>
<h4>2) 动画展示</h4>
<p>&nbsp;根据官方数据,武汉地区总人口超过1400万,春节前流出人口有500多万。这些人员的去向也是大家关心的问题。这里增加了武汉到国内各省的人口流动动画。另外,每个城市点也增加了动画来进行位置展示。</p>
<p><img src="https://img2018.cnblogs.com/common/1496396/202002/1496396-20200207160312037-1132030599.gif"></p>
<p>上图总共包括三部分动画:</p>
<ol>
<li>每个城市点的转动;</li>
<li>每个城市点圆柱增长动画;</li>
<li>城市飞线(人口流动)的动画;</li>
</ol>
<p>这三个动画可以通过一个函数实现,代码如下。要注意 data.setRotationMode() 方法不同的坐标轴顺序会导致不同的显示效果。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 0, 0, 1)">start3DAnim() {
</span><span style="color: rgba(0, 128, 128, 1)"> 2</span>   <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 设置城市点旋转模式</span>
<span style="color: rgba(0, 128, 128, 1)"> 3</span>   <span style="color: rgba(0, 0, 255, 1)">this</span>.cityPoints.forEach((data) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)"> 4</span>       data.setRotationMode('zxy'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 128, 1)"> 5</span> <span style="color: rgba(0, 0, 0, 1)">    });
</span><span style="color: rgba(0, 128, 128, 1)"> 6</span>
<span style="color: rgba(0, 128, 128, 1)"> 7</span>   let uv_offset = 0<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span>   let point_ang = 0<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span>   let c_h = 2, wh_c_h = 2<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">10</span>   const wuhanCylinder = <span style="color: rgba(0, 0, 255, 1)">this</span>.dm3d.getDataByTag('wuhanCylinder'); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 武汉地区单独处理</span>
<span style="color: rgba(0, 128, 128, 1)">11</span>   setInterval(()=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">12</span>         point_ang = point_ang + 0.01<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">13</span>         uv_offset = uv_offset + 0.01<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">14</span>         
<span style="color: rgba(0, 128, 128, 1)">15</span>         c_h = c_h + 0.2<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">16</span>         <span style="color: rgba(0, 0, 255, 1)">if</span>(c_h &gt; 20<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">17</span>             c_h = 2<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">18</span> <span style="color: rgba(0, 0, 0, 1)">      }
</span><span style="color: rgba(0, 128, 128, 1)">19</span>         wh_c_h = wh_c_h + 0.6<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">20</span>         <span style="color: rgba(0, 0, 255, 1)">if</span>(wh_c_h &gt; 60<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">21</span>         wh_c_h = 2<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">22</span> <span style="color: rgba(0, 0, 0, 1)">      }
</span><span style="color: rgba(0, 128, 128, 1)">23</span>         <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 城市点旋转</span>
<span style="color: rgba(0, 128, 128, 1)">24</span>         <span style="color: rgba(0, 0, 255, 1)">this</span>.cityPoints.forEach((val) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">25</span> <span style="color: rgba(0, 0, 0, 1)">            val.setRotationZ(point_ang);
</span><span style="color: rgba(0, 128, 128, 1)">26</span> <span style="color: rgba(0, 0, 0, 1)">      });
</span><span style="color: rgba(0, 128, 128, 1)">27</span>         <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 城市点圆柱增长</span>
<span style="color: rgba(0, 128, 128, 1)">28</span>         <span style="color: rgba(0, 0, 255, 1)">this</span>.cylinders.forEach((val) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">29</span> <span style="color: rgba(0, 0, 0, 1)">            val.setTall(c_h);
</span><span style="color: rgba(0, 128, 128, 1)">30</span> <span style="color: rgba(0, 0, 0, 1)">      });
</span><span style="color: rgba(0, 128, 128, 1)">31</span>         <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 武汉地区单独处理</span>
<span style="color: rgba(0, 128, 128, 1)">32</span> <span style="color: rgba(0, 0, 0, 1)">      wuhanCylinder.setTall(wh_c_h);
</span><span style="color: rgba(0, 128, 128, 1)">33</span>         <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 城市间飞线</span>
<span style="color: rgba(0, 128, 128, 1)">34</span>         <span style="color: rgba(0, 0, 255, 1)">this</span>.movingLines.forEach((val) =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">35</span>             val.s('shape3d.uv.offset', [-uv_offset, 0<span style="color: rgba(0, 0, 0, 1)">]);
</span><span style="color: rgba(0, 128, 128, 1)">36</span> <span style="color: rgba(0, 0, 0, 1)">      });
</span><span style="color: rgba(0, 128, 128, 1)">37</span>         
<span style="color: rgba(0, 128, 128, 1)">38</span>   }, 30);</pre>
</div>
<h4>3) 鼠标移动到省区域之上时悬浮高亮</h4>
<p><img src="https://img2018.cnblogs.com/common/1496396/202002/1496396-20200207160430706-1128670325.gif"></p>
<p>要实现悬浮高亮需要利用 onHover 或者 onEnter 和 onLeave 事件。由于 onHover 需要悬停一段事件才能触发,为了提高反应速度,这里通过检测 onEnter 和 onLeave 事件修改区域的边界颜色来突出某个省。this.proviences 为各个省名称的集合。this.proviences.indexOf(selectedProvience)&nbsp;&gt;=&nbsp;0 用来确保只有指定的省区域上面才执行悬浮高亮。</p>
<p>代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 0, 0, 1)">handleInteractive3d(e) {
</span><span style="color: rgba(0, 128, 128, 1)"> 2</span> <span style="color: rgba(0, 0, 0, 1)">    let {
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span>       data, <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 事件指向的对象</span>
<span style="color: rgba(0, 128, 128, 1)"> 4</span>       kind <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 事件类型</span>
<span style="color: rgba(0, 128, 128, 1)"> 5</span>   } = e; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> e为事件</span>
<span style="color: rgba(0, 128, 128, 1)"> 6</span>   <span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">data) {
</span><span style="color: rgba(0, 128, 128, 1)"> 7</span>       <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span> <span style="color: rgba(0, 0, 0, 1)">    }
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span>
<span style="color: rgba(0, 128, 128, 1)">10</span>   const selectedProvience = data.getDisplayName(); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 省名称</span>
<span style="color: rgba(0, 128, 128, 1)">11</span>
<span style="color: rgba(0, 128, 128, 1)">12</span>   <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 悬浮高亮</span>
<span style="color: rgba(0, 128, 128, 1)">13</span>   const onEnterBorderColor = "rgb(250,250,87)"<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">14</span>   const onLeaveBorderColor = "rgb(61,61,61)"<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">15</span>   <span style="color: rgba(0, 0, 255, 1)">if</span> (kind == 'onEnter' &amp;&amp; <span style="color: rgba(0, 0, 255, 1)">this</span>.proviences.indexOf(selectedProvience) &gt;= 0<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">16</span>       <span style="color: rgba(0, 0, 255, 1)">this</span>.updateAreaBorder(data, onEnterBorderColor<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 128, 1)">17</span>       <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">18</span> <span style="color: rgba(0, 0, 0, 1)">    }
</span><span style="color: rgba(0, 128, 128, 1)">19</span>   <span style="color: rgba(0, 0, 255, 1)">if</span> (kind == 'onLeave' &amp;&amp; <span style="color: rgba(0, 0, 255, 1)">this</span>.proviences.indexOf(selectedProvience) &gt;= 0<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">20</span>       <span style="color: rgba(0, 0, 255, 1)">this</span>.updateAreaBorder(data, onLeaveBorderColor<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 128, 1)">21</span>       <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">22</span> <span style="color: rgba(0, 0, 0, 1)">    }
</span><span style="color: rgba(0, 128, 128, 1)">23</span> <span style="color: rgba(0, 0, 0, 1)">    ……   
</span><span style="color: rgba(0, 128, 128, 1)">24</span> <span style="color: rgba(0, 0, 0, 1)">}
</span><span style="color: rgba(0, 128, 128, 1)">25</span>
<span style="color: rgba(0, 128, 128, 1)">26</span> <span style="color: rgba(0, 0, 0, 1)">updateAreaBorder(data, color) {
</span><span style="color: rgba(0, 128, 128, 1)">27</span>   data.s("wf.color", color); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 更新边框颜色</span>
<span style="color: rgba(0, 128, 128, 1)">28</span>   }</pre>
</div>
<h4>4) 点击某个省将其从地图拉高,同时弱化其他区域并更新表格内容。点击背景恢复原样。</h4>
<p><img src="https://img2018.cnblogs.com/common/1496396/202002/1496396-20200207160529057-1272991564.gif"></p>
<p>思路是这样:拉高某个区域可通过改变其 y 方向的值。弱化其他区域可通过设置透明度来实现。代码如下:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 0, 0, 1)">handleInteractive3d(e) {
</span><span style="color: rgba(0, 128, 128, 1)"> 2</span> <span style="color: rgba(0, 0, 0, 1)">    let {
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span> <span style="color: rgba(0, 0, 0, 1)">      data,
</span><span style="color: rgba(0, 128, 128, 1)"> 4</span> <span style="color: rgba(0, 0, 0, 1)">      kind
</span><span style="color: rgba(0, 128, 128, 1)"> 5</span>   } =<span style="color: rgba(0, 0, 0, 1)"> e;
</span><span style="color: rgba(0, 128, 128, 1)"> 6</span>   <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 如果点击背景,则恢复区域颜色及y轴方向的值。</span>
<span style="color: rgba(0, 128, 128, 1)"> 7</span>   <span style="color: rgba(0, 0, 255, 1)">if</span> (kind == 'clickBackground'<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span>       <span style="color: rgba(0, 0, 255, 1)">this</span>.updateAreaOpacity(1); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">还原透明度</span>
<span style="color: rgba(0, 128, 128, 1)"> 9</span>       <span style="color: rgba(0, 0, 255, 1)">this</span>.updateTable(); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 恢复表格内容为全国数据</span>
<span style="color: rgba(0, 128, 128, 1)">10</span>       <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">11</span> <span style="color: rgba(0, 0, 0, 1)">    }
</span><span style="color: rgba(0, 128, 128, 1)">12</span>
<span style="color: rgba(0, 128, 128, 1)">13</span>   <span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">data) {
</span><span style="color: rgba(0, 128, 128, 1)">14</span>       <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 128, 1)">15</span> <span style="color: rgba(0, 0, 0, 1)">    }
</span><span style="color: rgba(0, 128, 128, 1)">16</span>
<span style="color: rgba(0, 128, 128, 1)">17</span>   const selectedProvience =<span style="color: rgba(0, 0, 0, 1)"> data.getDisplayName();
</span><span style="color: rgba(0, 128, 128, 1)">18</span>
<span style="color: rgba(0, 128, 128, 1)">19</span>   <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 点击拉高</span>
<span style="color: rgba(0, 128, 128, 1)">20</span>   <span style="color: rgba(0, 0, 255, 1)">if</span> (kind == 'clickData' &amp;&amp; <span style="color: rgba(0, 0, 255, 1)">this</span>.proviences.indexOf(selectedProvience) &gt;= 0<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">21</span>       <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.showProvienceData(selectedProvience);
</span><span style="color: rgba(0, 128, 128, 1)">22</span> <span style="color: rgba(0, 0, 0, 1)">    }
</span><span style="color: rgba(0, 128, 128, 1)">23</span>   }
<span style="color: rgba(0, 128, 128, 1)">24</span> <span style="color: rgba(0, 128, 128, 1)">25</span>
<span style="color: rgba(0, 128, 128, 1)">26</span> <span style="color: rgba(0, 0, 0, 1)">updateAreaOpacity(opacity) {
</span><span style="color: rgba(0, 128, 128, 1)">27</span>   <span style="color: rgba(0, 0, 255, 1)">for</span> (const key <span style="color: rgba(0, 0, 255, 1)">in</span> <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.areaDataObj) {
</span><span style="color: rgba(0, 128, 128, 1)">28</span>       <span style="color: rgba(0, 0, 255, 1)">if</span> (Object.prototype.hasOwnProperty.call(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.areaDataObj, key)) {
</span><span style="color: rgba(0, 128, 128, 1)">29</span>         const dataNode = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.areaDataObj.data;
</span><span style="color: rgba(0, 128, 128, 1)">30</span>         dataNode.s('shape3d.opacity', opacity); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 改变透明度</span>
<span style="color: rgba(0, 128, 128, 1)">31</span>         const =<span style="color: rgba(0, 0, 0, 1)"> dataNode.getPosition3d();
</span><span style="color: rgba(0, 128, 128, 1)">32</span>         dataNode.setPosition3d(x, 0, z); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 恢复Y方向位置</span>
<span style="color: rgba(0, 128, 128, 1)">33</span> <span style="color: rgba(0, 0, 0, 1)">      }
</span><span style="color: rgba(0, 128, 128, 1)">34</span> <span style="color: rgba(0, 0, 0, 1)">    }
</span><span style="color: rgba(0, 128, 128, 1)">35</span> <span style="color: rgba(0, 0, 0, 1)">}
</span><span style="color: rgba(0, 128, 128, 1)">36</span>
<span style="color: rgba(0, 128, 128, 1)">37</span> <span style="color: rgba(0, 0, 0, 1)">showProvienceData(selectedProvience) {
</span><span style="color: rgba(0, 128, 128, 1)">38</span>   const data = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.provAreas;
</span><span style="color: rgba(0, 128, 128, 1)">39</span>   <span style="color: rgba(0, 0, 255, 1)">this</span>.updateAreaOpacity(0.3); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 通过设置透明度为0.3来弱化周围区域</span>
<span style="color: rgba(0, 128, 128, 1)">40</span>   data.s('shape3d.opacity', 1); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 设置透明度为1来突出选中区域</span>
<span style="color: rgba(0, 128, 128, 1)">41</span>
<span style="color: rgba(0, 128, 128, 1)">42</span>   const =<span style="color: rgba(0, 0, 0, 1)"> data.getPosition3d();
</span><span style="color: rgba(0, 128, 128, 1)">43</span>   <span style="color: rgba(0, 0, 255, 1)">if</span> (y == 0<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 128, 1)">44</span>       data.setPosition3d(x, 5, z); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 拉高该区域</span>
<span style="color: rgba(0, 128, 128, 1)">45</span>   } <span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 128, 1)">46</span>       data.setPosition3d(x, 0<span style="color: rgba(0, 0, 0, 1)">, z);
</span><span style="color: rgba(0, 128, 128, 1)">47</span> <span style="color: rgba(0, 0, 0, 1)">    }
</span><span style="color: rgba(0, 128, 128, 1)">48</span>   <span style="color: rgba(0, 0, 255, 1)">this</span>.updateTable(selectedProvience); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 更新表格内容为该省数据</span>
<span style="color: rgba(0, 128, 128, 1)">49</span>   }</pre>
</div>
<p><strong>结语:</strong></p>
<p>数据可视化越来越普及,在工业物联网、电信、智慧医疗、智能交通等行业都有广泛的应用。但依我看来,数据可视化不仅仅是将数据用某种图表展示出来。更重要的,是要给大家带来良好的用户体验。数据不但要展示,更要展示的优美、协调、重点突出。传统的 2D 普通页面已经成为过去式。WebGL 的出现给我们提供了更丰富的途径(3D)来展示原本呆板的数据。</p>
<p>正所谓,人们对美好事物的向往就是我们前端程序猿的奋斗目标。希望这次肺炎疫情能够尽快得到控制。武汉加油,中国加油!</p>
<p>&nbsp;</p>
<div id="gtx-trans" style="position: absolute; left: 63px; top: 1371.38px">&nbsp;</div><br><br>
来源:https://www.cnblogs.com/htdaydayup/p/12273402.html
頁: [1]
查看完整版本: HTML5 WebGL 实现 3D 地图助力新型冠状病毒疫情实时数据可视化