使用Vue+DataV+ECharts打造新冠肺炎疫情数据大屏(可动态刷新)

源码

本项目已上传至 Github:data-visualization-with-covid-19

效果图

演示

仅适配 1080P 屏幕,使用浏览器访问后按 F11 进入全屏可看最佳显示效果。

  1. 疫情真实数据演示地址:演示地址-真实数据

  2. 模拟数据演示地址:演示地址-模拟数据

效果图

前端框架和类库

后台数据

数据主要来自:实时更新:新冠肺炎疫情最新动态 - 腾讯新闻 ,我的后台服务器每天都是定时去抓取数据。

历史数据来自:BlankerL/DXY-COVID-19-Data

数据来源与分析

网上有很多公开的疫情数据,包含国内和全球的数据,由于时间关系,我仅针对国内的数据进行收集和汇总。我选择的数据来源有 实时更新:新冠肺炎疫情最新动态 - 腾讯新闻丁香园 ,这两个接口的数据都已经汇总了近期的疫情统计信息,包含现存确诊、累计确诊、境外输入等数据,还有每天的新增数据,直接拿来用就行。

通过分析,我确定了要展示以下内容:

展示内容

选择展示图表

确定要展示的数据和信息了,接下来就是选择对应的图表来展示。如何实现直观、漂亮而炫酷的展示效果,需要按数据类型来匹配适合的图表。常用的图表类型有:折线图、柱状图、饼图、地理坐标/地图、散点图、热力图等,不同类型的图表结合数据展示的效果也不一样,如果对这些图标不了解的,建议直接去 ECharts 示例页面 看看。

ECharts

结合新冠肺炎疫情数据,我根据不同数据选择了相应的图表:

数据图表类型
基本数据文本显示
累计确诊数省份排名(前10)横向柱状图
现有确诊、境外输入占比饼图
最近一周累计治愈和累计确诊对比柱状图
治愈率和死亡率饼图
确诊数和新增趋势折线图
各省份统计信息列表数据表格展示

页面布局

通过查看网上实现的数据大屏,参考别人的布局,我确定大概的布局方式:

布局

代码实现

创建项目

使用 Vue Cli 创建 Vue 项目,没有 Vue Cli 的使用以下命令安装:

1
npm install -g @vue/cli

创建项目:

1
vue create datav-covid-19

安装依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 安装 DataV
npm install @jiaminghi/data-view

# 安装 echarts
npm install echarts -S

# 安装 element-ui
npm i element-ui -S

# 安装 vue-router
npm install vue-router

# 安装 mockjs
npm install mockjs --save-dev

# 安装 axios
npm install axios

# 安装 echarts-liquidfill
npm i echarts-liquidfill

引入注册

在项目中引入,编辑 main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import Vue from 'vue'
import App from './App.vue'
import dataV from '@jiaminghi/data-view'
import * as echarts from 'echarts'
import 'element-ui/lib/theme-chalk/index.css';
import axios from 'axios'
// 引入 echarts 水球图
import 'echarts-liquidfill'
import VueRouter from 'vue-router'

import {
Icon, Row, Col, Table, TableColumn, Button, Dialog, Link
} from 'element-ui';

// 注册 echarts
Vue.prototype.$echarts = echarts
Vue.config.productionTip = false

// 注册 axios
Vue.prototype.axios = axios

// 注册 dataV
Vue.use(dataV)

// 注册路由
Vue.use(VueRouter)

// 按需注册其他 element-ui 组件
Vue.use(Icon)
Vue.use(Row)
Vue.use(Col)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Button)
Vue.use(Dialog)
Vue.use(Link)

new Vue({
render: h => h(App),
}).$mount('#app')

编写组件

因为篇幅有限,为了阅读体验,这里以 累计排名 组件为例,其他的组件请看 Github 上的代码。

累计排名组件

效果图

累计排名组件效果图

累计排名组件采用 ECharts 的柱状图来显示,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<template>
<div
ref="provinceRankingBarChart"
style="width: 100%; height: 100%"
/>
</template>
<script>
import * as echarts from 'echarts'
let chart = null
export default {
props: {
data: {
type: Object,
default () {
return {
provinceList: [],
valueList: []
}
}
}
},
methods: {
initChart () {
if (null != chart && undefined != chart) {
chart.dispose()
}
chart = this.$echarts.init(this.$refs.provinceRankingBarChart)
this.setOptions()
},
setOptions() {
var salvProValue = this.data.valueList;
var salvProMax = [];
for (let i = 0; i < salvProValue.length; i++) {
salvProMax.push(salvProValue[0])
}
let option = {
grid: {
left: '2%',
right: '2%',
bottom: '2%',
top: '2%',
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'none'
},
formatter: function (params) {
return params[0].name + ' : ' + params[0].value
}
},
xAxis: {
show: false,
type: 'value'
},
yAxis: [{
type: 'category',
inverse: true,
axisLabel: {
show: true,
textStyle: {
color: '#fff'
},
},
splitLine: {
show: false
},
axisTick: {
show: false
},
axisLine: {
show: false
},
data: this.data.provinceList
}, {
type: 'category',
inverse: true,
axisTick: 'none',
axisLine: 'none',
show: true,
axisLabel: {
textStyle: {
color: '#ffffff',
fontSize: '12'
},
},
data: salvProValue
}],
series: [{
name: '值',
type: 'bar',
zlevel: 1,
itemStyle: {
normal: {
barBorderRadius: 30,
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [{
offset: 0,
color: 'rgb(2,163,254,1)'
}, {
offset: 1,
color: 'rgb(125,64,255,1)'
}]),
},
},
barWidth: 20,
data: salvProValue
},
{
name: '背景',
type: 'bar',
barWidth: 20,
barGap: '-100%',
data: salvProMax,
itemStyle: {
normal: {
color: 'rgba(24,31,68,1)',
barBorderRadius: 30,
}
},
},
]
}
chart.setOption(option)
}
},
watch: {
data: {
handler(newList, oldList) {
if (oldList != newList) {
this.setOptions()
}
},
deep: true
}
}
}
</script>

在页面中引入使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<div class="demo">
<province-ranking-bar-chart
ref="rankChart"
:data="dataList"
style="width: 100%; height: 380px"
/>
</div>
</template>
<script>
// 引入组件
import ProvinceRankingBarChart from '../components/ProvinceRankingBarChart'
export default {
components: {
ProvinceRankingBarChart
},
data () {
return {
// 定义数据
dataList: {
provinceList: ['湖北', '台湾'],
valueList: [68188, 15379]
}
}
},
mounted() {
// 创建图表并初始化
this.$refs.rankChart.initChart()
}
}
</script>
<style>
.demo {
width: 500px;
height: 600px;
}
</style>

其他组件的代码就不在这里写了,完整代码已上传 Github ,需要的可以去查看。

完整的组件结构如下:

完整的组件结

准备模拟数据

项目中提供了两种数据提供方式,一是请求真实后台地址,返回的数据格式参考 data 目录下的 json 文件;二是在本地使用 Mock 生成模拟数据。这里仅介绍使用 Mock 生成模拟数据方式。

使用 Mock 生成模拟数据

在项目根目录下创建文件夹 mock,分别创建 covid19.jsindex.js

编写 mock 服务

covid19.js 代码如下,代码中使用到一些 Mock 的语法,具体使用方法请查看 Mock 的文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 从本地读取 json 数据
const provinceData = require('../data/covid19-province.json')
const dailyData = require('../data/covid19-daily-list.json')
// 引入 mockjs
const Mock = require('mockjs')
// 使用 mockjs 的 Random 生成随机数据
const Random = Mock.Random

module.exports = [
{
url: '/api/covid-19/overall',
type: 'get',
response: config => {
return {
success: true,
code: 200,
message: "操作成功",
data: {
confirmedCount: Random.integer(110000, 120000),
confirmedIncr: 72,
curedCount: Random.integer(100000, 110000),
curedIncr: 173,
currentConfirmedCount: Random.integer(3000, 4000),
currentConfirmedIncr: -110,
deadCount: Random.integer(4000, 6000),
deadIncr: 12,
importedCount: Random.integer(6000, 8000),
importedIncr: 23,
noInFectCount: Random.integer(400, 600),
noInFectIncr: 8,
suspectIncr: 0,
suspectCount: 2,
updateTime: "2021-07-15 20:39:11",
curedRate: Random.float(90, 95, 0, 9),
deadRate: Random.float(1, 5, 0, 9)
}
}
}
},
{
url: '/api/covid-19/area/latest/list',
type: 'get',
response: config => {
return provinceData
}
},
{
url: '/api/covid-19/list',
type: 'get',
response: config => {
return dailyData
}
}
]
注册 mock 服务

编辑 index.js,这里主要是注册 mock 服务,调用方法 initMockData() 完成注册;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const Mock = require('mockjs')
// 引入写好的 mock 服务
const covid19 = require('./covid19')

const mocks = [
...covid19
]

function param2Obj(url) {
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
if (!search) {
return {}
}
const obj = {}
const searchArr = search.split('&')
searchArr.forEach(v => {
const index = v.indexOf('=')
if (index !== -1) {
const name = v.substring(0, index)
const val = v.substring(index + 1, v.length)
obj[name] = val
}
})
return obj
}

const initMockData = () => {

Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
Mock.XHR.prototype.send = function() {
if (this.custom.xhr) {
this.custom.xhr.withCredentials = this.withCredentials || false

if (this.responseType) {
this.custom.xhr.responseType = this.responseType
}
}
this.proxy_send(...arguments)
}

function XHR2ExpressReqWrap(respond) {
return function(options) {
let result = null
if (respond instanceof Function) {
const { body, type, url } = options
result = respond({
method: type,
body: JSON.parse(body),
query: param2Obj(url)
})
} else {
result = respond
}
return Mock.mock(result)
}
}

for (const i of mocks) {
Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
}
}

module.exports = {
mocks,
initMockData
}
使用 mock 服务

main.js 中引入:

1
2
3
const { initMockData } = require('../mock')
// 完成注册
initMockData()

然后在页面中使用 request.get('/api/covid-19/list') 就能请求获取到数据,这里的 request.get() 是我用 axios 封装写的方法。

封装数据接口

封装 axios

项目中的数据请求都是使用 axios 为方便使用,我简单封装了一个工具类 request.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import axios from "axios"
import sysConst from '../libs/const'
const fetch = (method = 'GET', url, param = '') => {
// 处理 url
url = `${sysConst.baseUrl}${url}`
return new Promise((resolve, reject) => {
axios({
method: method,
url: url,
changeOrigin: true,
data: JSON.stringify(param)
}).then((res) => {
resolve(res.data)
}, error => {
reject(error)
}).catch((error) => {
reject(error)
})
})
}

const get = (url) => {
return fetch('GET', url)
}

const post = (url, data) => {
return fetch('POST', url, data)
}

const put = (url, data) => {
return fetch('PUT', url, data)
}

const remove = (url, data) => {
return fetch('DELETE', url, data)
}

export {
get,
post,
put,
remove
}

这里引入的 const.js 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
let baseUrl = ''
if (process.env.NODE_ENV === 'development') {
// 修改你的 API 地址
baseUrl = ''
} else {
// 你的 API 地址
baseUrl = ''
}

export default {
baseUrl
}

封装数据接口

在项目根目录下新建文件夹 api ,用于保存编写数据接口,在该目录下新增文件 covid19.js,用于封装请求获取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import * as request from '@/utils/request'

/**
* 接口封装
*/
export default {
getOverall() {
let url = `/api/covid-19/overall?_=${Math.random()}`
return request.get(url)
},
getProvinceDataList() {
let url = `/api/covid-19/area/latest/list?_=${Math.random()}`
return request.get(url)
},
getDailyList() {
let url = `/api/covid-19/list?t=${Math.random()}`
return request.get(url)
}
}

调用数据接口获取数据并更新图表展示

1
2
3
4
5
6
7
8
9
10
11
12
13
// 引入
import covid19Service from '../api/covid19'

// 使用
let self = this
covid19Service.getOverall().then((res) => {
if (!res.success) {
console.log('错误:' + res.info)
return
}
// 修改数据,图表组件检测到数据变化会触发 setOptions() 方法更新显示( setOptions() 在图表组件中已定义好)
self.basicData = res.data
})

项目结构

完整的项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
├─build
├─data # 本地模拟数据目录
├─mock # mock 配置
├─public
└─src
├─api # 接口封装目录
├─assets
├─components # 组件目录
│ ├─About # 关于
│ ├─BasicDataItemLabel # 基本数据显示标签
│ ├─BasicProportionChart # 占比图表
│ ├─BasicTrendChart # 趋势图表
│ ├─ChartCard # 图表面板
│ ├─CuredAndDeadRateChart # 治愈率和死亡率图表
│ ├─CurrentConfirmedCompareBarChart # 最近一周累计治愈图表
│ ├─DataMap # 数据地图
│ └─ProvinceRankingBarChart # 累计排名图表
├─libs # 一些常用的配置
├─router # 路由配置
├─utils # 工具类
└─views # 视图

详细结构:

完整的项目结构

总结

  1. 采用组件化封装各个展示图表,能更好的图表展示及复用;
  2. 使用 axios 请求后台服务或本地 mock 服务获取数据,然后重新赋值图表中指定的数据;

项目源码:data-visualization-with-covid-19

这个项目是个人学习作品,能力有限,难免会有 BUG 和错误,敬请谅解。如有更好的建议或想法,请指出,谢谢