疫情視覺化(後續)

2022-11-07 06:00:49

前言

這是疫情視覺化最開始的文章,有需要了解的可前往檢視:https://www.cnblogs.com/xi12/p/16690119.html。 本來說有時間就把這個專案完結了的,結果後面一直有事拖著,直到現在十一月份了才搞完。老樣子,先看成果。

修改與新增

後面可能審美疲勞了,越看越醜,就乾脆直接用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修飾

結語

  • 最後我發現人的審美真的變化太快了。
  • 專案剛搞完:嗯不錯!真漂亮!
  • 過去一週後:什麼玩意!醜得一批!
  • 如果還有後續的話,我可能就要考慮新增自定義主題配色了。