草庐IT

疫情可视化part2

曦12 2023-03-28 原文

前言

此系列已完结,共3部分:

本来说有时间就把这个项目完结了的,结果后面一直有事拖着,直到现在十一月份了才搞完。老样子,先看成果。

修改与添加

后面可能审美疲劳了,越看越丑,就干脆直接用dataV(这可不是阿里的那个dataV)修饰页面了。这是项目改动后的样子:





主要做了如下改动:

  1. 添加首页的3d粒子球体。
  2. 完成“省内分析”模块。
  3. 完成当地疫情报告生成与下载。
  4. 使用dataV修饰页面。

粒子球体

粒子球体也是用three.js完成的,放大仔细看会发现其实是有三层构成的:内层透明球体,中间点状球体,外层病毒球体。

具体实现过程是这样的:

  1. 先绘制一个内层透明球体,记得将球体材质的transparent设为true,然后设置不透明度opacity值。
//创建斑点球体
async function createSpotSphere() {
  let globeBufferGeometry = new THREE.SphereGeometry(earthSize - 1, 50, 50);//球体几何体
  let globeInnerMaterial = new THREE.MeshBasicMaterial({
    color: new THREE.Color(dvColor[0]),//颜色
    // blending: THREE.AdditiveBlending,//纹理融合的叠加方式
    // side: THREE.FrontSide,//前面显示
    transparent: true,//透明
    // depthWrite: false,//深度写入
    // depthTest: false,//黑洞效果
    opacity: .3,//不透明度
  });
  let globeInnerMesh = new THREE.Mesh(
    globeBufferGeometry,
    globeInnerMaterial
  );
  earthGroup.add(globeInnerMesh); //将网格放入地球组
  cre
  1. 创建点状球体。这里需要使用一张黑白的世界地图。
let img = new Image();
img.src = earthGrayscale; //黑白地图

将图片使用onload加载到项目中后,利用canvas绘制一遍该图,再使用getImageData获取到像素点数据canData。

let canvas = document.createElement("canvas");
    canvas.width = img.width; //使得canvas尺寸与图片尺寸相同
    canvas.height = img.height;
    (canvas.getContext("2d") as any).drawImage(img, 0, 0, img.width, img.height);//canvas绘制图片
    let canData = (canvas.getContext("2d") as any).getImageData(0, 0, canvas.width, canvas.height);//获取画布像素数据

利用canData .data中的rgba信息生成缓冲几何顶点数组globeCloudVerticesArray。

let globeCloudBufferGeometry = new THREE.BufferGeometry(); //设置缓冲几何体
    let globeCloudVerticesArray = []; //地球云缓冲几何体顶点
    let o = null; //数组处理时的计数
    for (o = 0; o < canData.data.length; o += 4) {
      let r = (o / 4) % canvas.width,
        i = (o / 4 - r) / canvas.width;
      if ((o / 4) % 2 == 1 && i % 2 == 1)
        if (0 === canData.data[o]) {
          let n = r,
            longitude = (i / (canvas.height / 180) - 90) / -1, //经度
            latitude = n / (canvas.width / 360) - 180; //维度
          let s = latLongToVector3(longitude, latitude, earthSize, .1); //经纬度变换
          globeCloudVerticesArray.push(s); //将变换后的顶点放入数组
        }
    }

然后再使用three中的BufferAttribute生成属性position与color。

let l = new Float32Array(3 * globeCloudVerticesArray.length); //创建顶点数组长度
    for (o = 0; o < globeCloudVerticesArray.length; o++) {
      l[3 * o] = globeCloudVerticesArray[o].x;//设置顶点数组数据
      l[3 * o + 1] = globeCloudVerticesArray[o].y;
      l[3 * o + 2] = globeCloudVerticesArray[o].z;
    }
    let positionVal = new THREE.BufferAttribute(l, 3); //设置缓冲区属性值
    globeCloudBufferGeometry.setAttribute("position", positionVal); //给缓冲几何体添加位置属性
    let globeCloudMaterial = new THREE.PointsMaterial({
      color: new THREE.Color(dvColor[1]),//颜色
      fog: true,
      size: 1,
    });//球面斑点材质
    let d = new Float32Array(3 * globeCloudVerticesArray.length), c = [];
    for (o = 0; o < globeCloudVerticesArray.length; o++) {
      c[o] = new THREE.Color(dvColor[1]);//球面斑点颜色
      d[3 * o] = c[o].r;//设置地球云数组rgb颜色
      d[3 * o + 1] = c[o].g;
      d[3 * o + 2] = c[o].b;
    }
    let color_val = new THREE.BufferAttribute(d, 3);
    globeCloudBufferGeometry.setAttribute("color", color_val);//给缓冲几何体添加颜色属性,修改颜色直接修改globeCloudBufferGeometry的setAttribute

最后再使用THREE.Points创建球面的点,将position与color属性添加到点的几何体BufferGeometry中。

let globeCloud = new THREE.Points(//球面的象素点
      globeCloudBufferGeometry,
      globeCloudMaterial
    );

这是需要用到的坐标转换方法:

//经纬度坐标变换(传入e:纬度、a经度、t球半径、o球额外距离)
function latLongToVector3(e: any, a: any, t: any, o: any) {
  var r = (e * Math.PI) / 180,
    i = ((a - 180) * Math.PI) / 180,
    n = -(t + o) * Math.cos(r) * Math.cos(i),
    s = (t + o) * Math.sin(r),
    l = (t + o) * Math.cos(r) * Math.sin(i);
  return new THREE.Vector3(n, s, l); //计算三维向量
};
  1. 创建最外层的病毒球体。每一个病毒都是一个THREE.Sprite,材质是THREE.SpriteMaterial,利用Sprite.position.set设置具体坐标位置,最后将Sprite添加到球体组中。病毒的颜色是由当前点确诊值决定的,颜色值在colors数组中取出。
//创建病毒
function createVirus(data: any, earthSize: any) {
  let colors = [
    new THREE.Color(0xf9b8b8),
    new THREE.Color(0xfe4242),
    new THREE.Color(0xff0000),
  ]; //病毒颜色列表
  let virSize = 4; //病毒大小
  let list = JSON.parse(JSON.stringify(data));
  list.forEach((e: { value: number; color: any; position: any[]; }) => {
    e.value >= 10000000 && (e.color = colors[2]); //根据病毒数赋予不同颜色
    e.value >= 500000 && e.value < 10000000 && (e.color = colors[1]);
    e.value < 500000 && (e.color = colors[0]);
    if (e.position) {
      let virusMaterial = new THREE.SpriteMaterial({
        color: e.color,
        map: new THREE.TextureLoader().load(virusImg),
        side: THREE.FrontSide, //只显示前面
      }); //病毒材质
      let Sprite = new THREE.Sprite(virusMaterial); //点精灵材质
      Sprite.scale.set(virSize, virSize, 1); //点大小
      let lat = e.position[1]; //纬度
      let lon = e.position[0]; //经度
      let s = latLongToVector3(lat, lon, earthSize, 1); //坐标转换
      Sprite.position.set(s.x, s.y, s.z); //设置点的位置
      Sprite.dotData = e; //将点的数据添加到dotData属性中
      Sprite.name = "病毒";
      earthGroup.add(Sprite); //将病毒添加进球体组中
    }
  });
};

省内分析

  1. 省内数据概况。

    该数值在加载时是有增加动画的,是利用vue响应式原理完成的,在上一篇文章中有提到。
  2. 省内各地疫情柱状图。

    这是具体的echart配置:
let option = {
        title: {
            text: provinceBaseData.value.province + "各地数据",
            left: "center",
            top: '5%',
            textStyle: {
                color: "#fff",
            },
        },
        tooltip: {
            trigger: 'axis',
            axisPointer: {
                type: 'shadow'
            }
        },
        dataZoom: [
            {
                type: 'inside',
            },
            {
                show: true,
                yAxisIndex: 0,
                filterMode: 'empty',
                width: 25,
                height: '70%',
                showDataShadow: false,
                left: '3%',
                top: "center"
            }
        ],
        legend: {
            data: ['累计数', '治愈数', '确诊数', '较昨日新增', '死亡数'],
            orient: "vertical",
            top: "15%",
            right: "2%",
            textStyle: {
                color: "#fff"
            },
        },
        grid: {
            left: '3%',
            right: '15%',
            bottom: '10%',
            containLabel: true
        },
        xAxis: {
            type: 'category',
            data: echartData.cityName,
            axisLabel: {
                interval: 0,
                rotate: 50,
                color: "#fff"
            },
        },
        yAxis: {
            type: 'value',
            axisLabel: {
                color: "#fff",
            },
        },
        series: [
            {
                name: '累计数',
                type: 'bar',
                emphasis: {
                    focus: 'series'
                },
                itemStyle: {
                    color: '#f59158'
                },
                data: echartData.conNum
            },
            {
                name: '治愈数',
                type: 'bar',
                emphasis: {
                    focus: 'series'
                },
                itemStyle: {
                    color: '#48c56b'
                },
                data: echartData.cureNum
            },
            {
                name: '确诊数',
                type: 'bar',
                stack: 'total',
                emphasis: {
                    focus: 'series'
                },
                itemStyle: {
                    color: '#ffd889'
                },
                data: echartData.econNum
            },
            {
                name: '较昨日新增',
                type: 'bar',
                stack: 'total',
                emphasis: {
                    focus: 'series'
                },
                itemStyle: {
                    color: '#794ebd'
                },
                data: echartData.asymptomNum
            },
            {
                name: '死亡数',
                type: 'bar',
                stack: 'total',
                emphasis: {
                    focus: 'series'
                },
                itemStyle: {
                    color: '#ff6a6a'
                },
                data: echartData.deathNum
            },
        ]
    };
  1. 省份历史疫情数据图表。

    这是该图表配置:
let option = {
        title: {
            text: provinceBaseData.value.province + "历史数据",
            left: "center",
            top: '5%',
            textStyle: {
                color: "#fff",
            },
        },
        tooltip: {
            trigger: 'axis',
            axisPointer: {
                type: 'cross',
                label: {
                    backgroundColor: '#6a7985'
                }
            }
        },
        legend: {
            data: ['累计数', '确诊数', '较昨日新增', '治愈数', '死亡数'],
            orient: "vertical",
            top: "15%",
            right: "2%",
            textStyle: {
                color: "#fff"
            },
        },
        grid: {
            left: '8%',
        },
        xAxis: [
            {
                type: 'category',
                boundaryGap: false,
                axisLabel: {
                    color: "#fff",
                },
                data: echatrData.time
            }
        ],
        yAxis: [
            {
                type: 'value',
                axisLabel: {
                    color: "#fff",
                },
            }

        ],
        dataZoom: [
            {
                startValue: ''
            },
            {
                type: 'inside'
            }
        ],
        series: [
            {
                name: '累计数',
                type: 'line',
                stack: 'Total',
                smooth: true,
                lineStyle: {
                    width: 0
                },
                showSymbol: false,
                areaStyle: {
                    opacity: 0.8,
                    color: "#f59158"
                },
                emphasis: {
                    focus: 'series'
                },
                itemStyle: {
                    color: '#f59158'
                },
                data: echatrData.conNum
            },
            {
                name: '确诊数',
                type: 'line',
                stack: 'Total',
                smooth: true,
                lineStyle: {
                    width: 0
                },
                showSymbol: false,
                areaStyle: {
                    opacity: 0.8,
                    color: "#ffd889"
                },
                itemStyle: {
                    color: '#ffd889'
                },
                emphasis: {
                    focus: 'series'
                },
                data: echatrData.econNum
            },
            {
                name: '较昨日新增',
                type: 'line',
                stack: 'Total',
                smooth: true,
                lineStyle: {
                    width: 0
                },
                showSymbol: false,
                label: {
                    show: true,
                    position: 'top'
                },
                areaStyle: {
                    opacity: 0.8,
                    color: "#794ebd"
                },
                itemStyle: {
                    color: '#794ebd'
                },
                emphasis: {
                    focus: 'series'
                },
                data: echatrData.asymptomNum
            },
            {
                name: '治愈数',
                type: 'line',
                stack: 'Total',
                smooth: true,
                lineStyle: {
                    width: 0
                },
                showSymbol: false,
                areaStyle: {
                    opacity: 0.8,
                    color: "#48c56b"
                },
                itemStyle: {
                    color: '#48c56b'
                },
                emphasis: {
                    focus: 'series'
                },
                data: echatrData.cureNum
            },
            {
                name: '死亡数',
                type: 'line',
                stack: 'Total',
                smooth: true,
                lineStyle: {
                    width: 0
                },
                showSymbol: false,
                areaStyle: {
                    opacity: 0.8,
                    color: "#ff6a6a"
                },
                itemStyle: {
                    color: '#ff6a6a'
                },
                emphasis: {
                    focus: 'series'
                },
                data: echatrData.deathNum
            },
        ]
    };

报告生成与下载

报告生成利用的是docxtemplater,这是前端生成word比较方便的一个插件,具体使用方法可以看这里:https://www.cnblogs.com/xi12/p/16863383.html。

  • 这是生成的word报告示例:

dataV修饰

结语

  • 最后我发现人的审美真的变化太快了。
  • 项目刚搞完:嗯不错!真漂亮!
  • 过去一周后:什么玩意!丑得一批!
  • 如果还有后续的话,我可能就要考虑添加自定义主题配色了。

有关疫情可视化part2的更多相关文章

  1. ruby - Ruby 中的波形可视化 - 2

    我即将开始一个将录制和编辑音频文件的项目,我正在寻找一个好的库(最好是Ruby,但会考虑Java或.NET以外的任何库)以进行实时可视化波形。有人知道我应该从哪里开始搜索吗? 最佳答案 要流入浏览器的数据量很大。Flash或Flex图表可能是唯一能提高内存效率的解决方案。Javascript图表往往会因大型数据集而崩溃。 关于ruby-Ruby中的波形可视化,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.c

  2. Unity数据可视化图表插件XCharts3.0发布 - 2

    Unity数据可视化图表插件XCharts3.0发布历时8个多月,业余时间,断断续续,XCharts3.0总算发布了。如果要打个满意度,我给3.0版本来个80分。对于代码框架结构设计的调整改动,基本符合预期,甚是满意。相比之前的1.0和2.0版本,我认为3.0才是一个拿得出手给广大开发者使用的版本。1.0发布的时候,很兴奋,从0.1到1.0,也磨了一年,真的等不及想给大家试用了,还特地写过一篇文章以示庆祝。那个时候,1.0虽然还还不够完善,功能也不够丰富,但它是XCharts的开始,没有1.0,也就没有后面的2.0和3.0。后面的2.0发布,做了很多改进和优化,随着版本迭代,慢慢的发现有不少硬

  3. micropython复现经典单片机项目(二)可视化音频 频谱解析(基本搞定) - 2

    本人是音乐爱好者,从小就特别喜欢那个随着音乐跳动的方框效果,就是这个:arduino上一大把对,我忍你很久了,我就想用mpy做,全网没有,行我自己研究。果然兴趣是最好的老师,我之前有篇博客专门讲音频,有兴趣的可以回顾一下。提到可视化频谱,必然绕不开fft,大学学过这玩意,当时一心玩,老师讲的一个字都么听进去,网上教程简略扫了一下,大该就是把时域转频域的工具,我大mpy居然没有fft函数,奶奶的,先放着。音频信息如何收集?第一种傻瓜式的ADC,模拟转数字,原始粗暴,第二种,I2S库,我之前博客有讲过,数据是PCM编码。然后又去学PCM编码,一学豁然开朗,舒服,以代码为例:audio_in=I2S

  4. ruby-on-rails - Ruby On Rails 4.2 的可视化日志查看器 - 2

    我以前在Laravel4上工作过,它有一个很棒的日志查看器工具laravellogviewer查看demo我正在寻找与Rubyonrails4.2非常相似的东西,如果你们知道Rails4.2的任何好的可视化日志记录GEM,请告诉我..从代码我需要记录不同的日志级别,这个工具应该直观地组织我的日志,谢谢.. 最佳答案 这应该可以帮助您入门https://github.com/shadabahmed/logstasher如其所说Thisgemisheavilyinspiredfromlograge,butit'sfocusedonone

  5. 通过可视化运维配置,实现故障秒级自愈 - 2

    急促的告警铃声响彻寂静的夜晚。对运维人来说,晚间值守耗费更大的精力,往往一个简单的磁盘使用率告警通知,就不得不爬起来进行处理,毕竟告警无小事,对于小问题,运维人也不能心存侥幸心理。虽然有着值班人员和团队的支撑,但频繁的告警还是让运维人员精疲力竭,如何让系统的稳定性提高,减轻一线人员的工作量,减轻一线人员的压力?通过智能运维,实现故障自愈将成为不可避免的选择。故障自愈是提升企业网络系统可用性和降低故障处理的人力投入,实现故障自愈从"人工处理"到"无人值守"的变革。通过实时发现告警,进行预诊断分析,判断告警类型和级别,如果是一般告警,平台进行自动恢复,如果是严重复杂告警则通过告警通知、运维工单等形

  6. day1-数组part01| 704. 二分查找、27. 移除元素 - 2

    数组理论基础数组是存放在连续内存空间上的相同类型数据的集合。数组下标从0开始数组内存空间的地址是连续的c++中vector和array的区别1、vector是顺序容器,其利用连续的内存空间来存储元素,但是其内存空间大小是能够改变的。2、array是顺序容器,其也是利用连续的内存空间来存储元素,但它的内存空间是固定大小的,申请之后就无法改变。3、vector的底层是array实现的二维数组二维数组在内存的空间地址是连续的704|二分查找思路1、把整个数组一分为二;2、判断目标值在左区间还是右区间,若在左区间,则修改右区间指针的位置;若在右区间,则修改新区间的左区间位置3、重复上述过程,直到lef

  7. ruby 心印 : explicit scoping on a class definition part 2 - 2

    我想澄清这个原始post的一些事情.答案建议Ruby按以下顺序搜索常量定义:封闭范围任何外部范围(重复直到达到顶层)包含的模块父类(superclass)对象内核所以澄清一下,在第(1-6)步是为legs_in_oyster找到的常量LEGS的值?它来自父类(superclass)Animal吗?类MyAnimals的范围是否因为不被视为封闭范围而被忽略?这是由于明确的MyAnimals::Oyster类定义吗?谢谢!只是想了解。这是代码:classAnimalLEGS=4deflegs_in_animalLEGSendclassNestedAnimaldeflegs_in_neste

  8. javascript - 谷歌可视化堆积面积图 : Add total to tooltip - 2

    我正在使用谷歌可视化API创建堆积面积图。当用户将鼠标悬停在图表内的一个点上时,我希望它显示该位置点的总和,以及这些点的值。第二点,我可以通过指定选项focusTarget:'category'轻松实现。我希望在类似的外观和感觉中,在total的工具提示中多一行。我尝试通过添加一个名为Total的额外列来实现此目的,该列的值为0,但工具提示等于总和。然而,这会向图例和图表本身添加一个空行,这在视觉上并不吸引人。我觉得这应该是开箱即用的东西,但我找不到解决这个问题的方法。如果有人知道解决这个问题的好方法,请回答。提前致谢。 最佳答案

  9. javascript - 用 Dart 可视化? - 2

    我的工作涉及大量的可视化。我一直在用D3.js和JavaScriptInfovistoolkit我最近了解到Dart如何成为开发Web应用程序的新方法。Q1。Dart是否提供任何用于可视化的库(某种级别的D3.js或JavaScriptInfovistoolkit)?Q2。如果我继续使用Dart,我可以使用D3.js吗?/JavascriptInfovistoolkit与Dart一起?编辑:我在互联网上发现wecanuseJavascriptalongwithDart.我经历了DartFAQ,但无法真正找到与可视化库或D3.js本身相关的任何内容。 最佳答案

  10. javascript - 谷歌可视化-点击事件 - 2

    我正在使用googlevisulaization绘制饼图。我面临的问题是,我无法捕获饼图上的点击事件。我正在这样做。functiondrawchartfromRe(){dashboard=newgoogle.visualization.Dashboard(document.getElementById('dashboard_div'));//alert("RefuelLength"+totrefuelList.length);//alert("Vehicleid:"+totrefuelList[0].vehicleId);//google.load("visualization","1

随机推荐