目录
本项目是基于flask+echarts搭建的全国疫情实时追踪的可视化大屏,主要涉及到的技术有爬虫,mysql数据库,flask框架,echarts图表。关于flask知识点,可学习另一篇文章Flask全套知识点从入门到精通,学完可直接做项目
最终效果如下:

从最终效果图可以看出,我们将屏幕分为4大板块(页面排布是左中右+上),第一板块是最上面的部分,包括大屏标题以及当前的实时时间;第二板块是最左边,上面的全国新增趋势折线图(新增确诊、治愈、死亡),下面是全国累计趋势折线图(累计确诊、治愈、死亡);第三板块是中间,上面是当天的一些数据,下面是全国累计确诊的疫情地图;第四板块是右边,上面是新增确诊人数Top前五的省份柱状图,下面是微博热搜话题的词云图。
开发工具:
vscode编辑器
python3.8
如果你在开发的过程中发现你编写的html或css文件在页面中没有更新,而是你上次编写的代码,也就是缓存的问题,这时候你需要在app.py中添加如下代码即可解决:
- @app.after_request
- def apply_caching(response):
- response.headers["Cache-Control"] = "no-cache"
- return response
项目的最终文件目录结构如下:

首先,我们得要有数据才能进行展示,这里我们选择用爬虫来进行数据采集并保存到mysql数据库中,考虑到平台限制,这里就不方便展示爬虫代码,需要的评论留言或私信。
这里我们先搭建一个基础的flask应用
- from flask import Flask,render_template
-
- app = Flask(__name__)
- app.config['JSON_AS_ASCII'] = False
-
- @app.route('/')
- def index():
- return render_template('main.html')
-
- if __name__ == '__main__':
- app.run(debug=True)
接着,需要编写main.html页面(这里我就直接放最终的代码)
- html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>全国疫情实时追踪title>
- <link rel="stylesheet" href="../static/css/main.css">
- <script src="../static/js/echarts.min.js">script>
- <script src="../static/js/china.js">script>
- <script src="../static/js/jquery-3.6.0.min.js">script>
- <script src="../static/js/echarts-wordcloud.min.js">script>
- head>
- <body>
- <div class="title">全国疫情实时追踪div>
- <div class="tim">div>
- <div class="l1" id="l1">div>
- <div class="l2" id="l2">div>
- <div class="c1">
- <div class="num"><h1>h1>div>
- <div class="num"><h1>h1>div>
- <div class="num"><h1>h1>div>
- <div class="num"><h1>h1>div>
- <div class="txt"><h2>累计确诊h2>div>
- <div class="txt"><h2>新增确诊h2>div>
- <div class="txt"><h2>累计治愈h2>div>
- <div class="txt"><h2>累计死亡h2>div>
- div>
- <div class="c2" id="main" >div>
- <div class="r1" id="r1">div>
- <div class="r2" id="r2">div>
- <script src="../static/js/get_data.js">script>
- <script src="../static/js/ec_center.js">script>
- <script src="../static/js/ec_left1.js">script>
- <script src="../static/js/ec_left2.js">script>
- <script src="../static/js/ec_right1.js">script>
- <script src="../static/js/ec_right2.js">script>
-
- body>
- html>
其次,我们还需要编写css来进行板块划分
main.css
- body{
- margin: 0;
- background-color: #333;
- }
- .title{
- position: absolute;
- width: 40%;
- height: 10%;
- top: 0;
- left: 30%;
- color: white;
- font-size: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .l1{
- position: absolute;
- width: 30%;
- height: 45%;
- top: 10%;
- left: 0;
- background-color: aquamarine;
- }
- .l2{
- position: absolute;
- width: 30%;
- height: 45%;
- top: 55%;
- left: 0;
- background-color: blue;
- }
- .c1{
- position: absolute;
- width: 40%;
- height: 25%;
- top: 10%;
- left: 30%;
- /* background-color: blue; */
- }
- .num{
- width: 25%;
- float: left;
- display: flex;
- align-items: center;
- justify-content: center;
- color: gold;
- font-size: 16px;
- }
- .txt{
- width: 25%;
- float: left;
- display: flex;
- align-items: center;
- justify-content: center;
- font-family: "幼圆";
- color: whitesmoke;
- font-size: 14px;
- }
- .c2{
- position: absolute;
- width: 40%;
- height: 65%;
- top: 35%;
- left: 30%;
- /* background-color: whitesmoke; */
- }
- .r1{
- position: absolute;
- width: 30%;
- height: 45%;
- top: 10%;
- right: 0;
- background-color: burlywood;
- }
- .r2{
- position: absolute;
- width: 30%;
- height: 45%;
- top: 55%;
- right: 0;
- background-color: brown;
- }
- .tim{
- position: absolute;
- /* width: 30%; */
- height: 10%;
- top: 5%;
- right: 2%;
- /* background-color: blueviolet; */
- font-size: 20px;
- color: whitesmoke;
- }
接下来我将按照4大板块进行介绍
那个大屏标题文字在上面的html页面中有,这里就不说了。还有一个就是右上角的时间显示,这里我们需要编写一个获取时间的接口,然后通过ajax来发送请求进行调用.
utils.py
- import time
- import pymysql
- import collections
- import jieba
- import re
-
- def get_time():
- time_str = time.strftime('%Y{}%m{}%d{} %X ')
- return time_str.format('年','月','日')
app.py
- import utils
- @app.route('/time')
- def time():
- return utils.get_time()
get_data.js
- function gettime(){
- $.ajax({
- url:'/time',
- timeout:10000,//超时时间
- success:function(data){
- $('.tim').html(data)
- },
- error:function(data){
-
- }
- });
- };
'运行
第二板块是左边的两个图
流程步骤就是先从数据库中获取数据,在flask应用中编写接口,最后在页面中通过ajax来发送进行调用,这是图表展示的通用步骤,下面我就不再叙述了。
这里因为我们要多次从数据库获取数据,所以 我们先封装一下方法,便于后面获取数据
utils.py
- def get_conn():
- """
- :return: 连接,游标
- """
- # 创建连接
- conn = pymysql.connect(host="127.0.0.1",
- user="xxx", # 这里写你的mysql用户名
- password="xxx", # 这里写你的mysql密码
- db="yiqing", # 这里写你的mysql中创建的数据库
- charset="utf8")
- # 创建游标
- cursor = conn.cursor()# 执行完毕返回的结果集默认以元组显示
- return conn, cursor
-
- def close_conn(conn, cursor):
- cursor.close()
- conn.close()
-
- def query(sql,*args):
- """
- 封装通用查询
- :param sql:
- :param args:
- :return: 返回查询到的结果,((),(),)的形式
- """
- conn, cursor = get_conn()
- cursor.execute(sql,args)
- res = cursor.fetchall()
- close_conn(conn, cursor)
- return res
左上的图:
utils.py
- def get_l1_data():
- # 因为会更新多次数据,取时间戳最新的那组数据
- sql = '''
- SELECT ds,confirm_add,heal_add,dead_add
- FROM history
- '''
- res = query(sql)
- return res
app.py
- from flask import jsonify
- @app.route('/l1')
- def get_l1_data():
- data = utils.get_l1_data()
- day,confirm_add,heal_add,dead_add = [],[],[],[]
- for item in data:
- day.append(item[0].strftime('%m-%d'))
- confirm_add.append(item[1])
- heal_add.append(item[2])
- dead_add.append(item[3])
- return jsonify({'day':day,'confirm_add':confirm_add,'heal_add':heal_add,'dead_add':dead_add})
get_data.js
- function get_l1_data(){
- $.ajax({
- url:'/l1',
- success:function(data){
- ec_left1_Option.xAxis[0].data=data.day
- ec_left1_Option.series[0].data=data.confirm_add
- ec_left1_Option.series[1].data=data.heal_add
- ec_left1_Option.series[2].data=data.dead_add
- ec_left1.setOption(ec_left1_Option)
- },
- error:function(data){
- }
- });
- }
'运行
ec_left1.js
-
- var ec_left1 = echarts.init(document.getElementById('l1'), "dark");
- var ec_left1_Option = {
- tooltip: {
- trigger: 'axis',
- //指示器
- axisPointer: {
- type: 'line',
- lineStyle: {
- color: '#7171C6'
- }
- },
- },
- legend: {
- data: ['新增确诊', '新增治愈','新增死亡'],
- left: "right"
- },
- //标题样式
- title: {
- text: "全国新增趋势",
- textStyle: {
- color: 'white',
- },
- left: 'left'
- },
- //图形位置
- grid: {
- left: '4%',
- right: '6%',
- bottom: '4%',
- top: 50,
- containLabel: true
- },
- xAxis: [{
- type: 'category',
- data: []
- }],
- yAxis: [{
- type: 'value',
- //y轴线设置显示
- axisLine: {
- show: true
- },
- position:'left',
- axisLabel: {
- show: true,
- color: 'white',
- fontSize: 12,
- formatter: function(value) {
- if (value >= 1000) {
- value = value / 1000 + 'k';
- }
- return value;
- }
- },
- //与x轴平行的线样式
- splitLine: {
- show: true,
- lineStyle: {
- width: 1,
- }
- }
- },
- {
- type: 'value',
- //y轴线设置显示
- axisLine: {
- show: true
- },
- position:'right',
- axisLabel: {
- show: true,
- color: 'white',
- fontSize: 12,
- formatter: function(value) {
- return value;
- }
- },
- //与x轴平行的线样式
- splitLine: {
- show: true,
- lineStyle: {
- width: 1,
- }
- }
- }
- ],
- series: [{
- name: "新增确诊",
- type: 'line',
- smooth: true,
- yAxisIndex:0,
- data: []
- }, {
- name: "新增治愈",
- type: 'line',
- smooth: true,
- yAxisIndex:1,
- data: []
- },{
- name: "新增死亡",
- type: 'line',
- smooth: true,
- yAxisIndex:1,
- data: []
- }
- ]
- };
-
- ec_left1.setOption(ec_left1_Option)
左下:
utils.py
- def get_l2_data():
- sql = '''
- SELECT ds,confirm,heal,dead
- FROM history;
- '''
- res = query(sql)
- return res
app.py
- @app.route('/l2')
- def get_l2_data():
- data = utils.get_l2_data()
- day,confirm,heal,dead = [],[],[],[]
- for item in data:
- day.append(item[0].strftime('%m-%d'))
- confirm.append(item[1])
- heal.append(item[2])
- dead.append(item[3])
- return jsonify({'day':day,'confirm':confirm,'heal':heal,'dead':dead})
get_data.js
- function get_l2_data(){
- $.ajax({
- url:'/l2',
- success:function(data){
- ec_left2_Option.xAxis[0].data=data.day
- ec_left2_Option.series[0].data=data.confirm
- ec_left2_Option.series[1].data=data.heal
- ec_left2_Option.series[2].data=data.dead
- ec_left2.setOption(ec_left2_Option)
- },
- error:function(data){
- }
- });
- }
'运行
ec_left2.js
- var ec_left2 = echarts.init(document.getElementById('l2'), "dark");
- var ec_left2_Option = {
- tooltip: {
- trigger: 'axis',
- //指示器
- axisPointer: {
- type: 'line',
- lineStyle: {
- color: '#7171C6'
- }
- },
- },
- legend: {
- data: ['累计确诊', '累计治愈','累计死亡'],
- left: "right"
- },
- //标题样式
- title: {
- text: "全国累计趋势",
- textStyle: {
- color: 'white',
- },
- left: 'left'
- },
- //图形位置
- grid: {
- left: '4%',
- right: '6%',
- bottom: '4%',
- top: 50,
- containLabel: true
- },
- xAxis: [{
- type: 'category',
-
- data: []
- }],
- yAxis: [{
- type: 'value',
- //y轴字体设置
-
- //y轴线设置显示
- axisLine: {
- show: true
- },
- axisLabel: {
- show: true,
- color: 'white',
- fontSize: 12,
- formatter: function(value) {
- if (value >= 1000) {
- value = value / 1000 + 'k';
- }
- return value;
- }
- },
- //与x轴平行的线样式
- splitLine: {
- show: true,
- lineStyle: {
- // color: '#FFF',
- width: 1,
- // type: 'solid',
- }
- }
- },
- {
- type: 'value',
- //y轴线设置显示
- axisLine: {
- show: true
- },
- position:'right',
- axisLabel: {
- show: true,
- color: 'white',
- fontSize: 12,
- formatter: function(value) {
- if (value >= 1000) {
- value = value / 1000 + 'k';
- }
- return value;
- }
- },
- //与x轴平行的线样式
- splitLine: {
- show: true,
- lineStyle: {
- width: 1,
- }
- }
- } ],
- series: [{
- name: "累计确诊",
- type: 'line',
- smooth: true,
- yAxisIndex:0,
- data: []
- }, {
- name: "累计治愈",
- type: 'line',
- smooth: true,
- yAxisIndex:1,
- data: []
- },{
- name: "累计死亡",
- type: 'line',
- smooth: true,
- yAxisIndex:1,
- data: []
- }
- ]
- };
-
- ec_left2.setOption(ec_left2_Option)
第三板块是中间部分
先完成上面的数值数据填充
utils.py
- def get_c1_data():
- """
- :return: 返回大屏div id=c1 的数据
- """
- # 因为会更新多次数据,取时间戳最新的那组数据
- sql = """
- SELECT confirm,confirm_add,heal,dead
- FROM history
- ORDER BY ds DESC LIMIT 1;
- """
- res = query(sql)
- return res[0]
app.py
- @app.route('/c1')
- def get_c1_data():
- data = utils.get_c1_data()
- return jsonify({'confirm':int(data[0]),'confirm_add':int(data[1]),'heal':int(data[2]),'dead':int(data[3])})
get_data.js
- function get_c1_data(){
- $.ajax({
- url:'/c1',
- success:function(data){
- $(".num h1").eq(0).text(data.confirm)
- $(".num h1").eq(1).text(data.confirm_add)
- $(".num h1").eq(2).text(data.heal)
- $(".num h1").eq(3).text(data.dead)
- },
- error:function(data){
- }
- });
- }
'运行
接着完成下面的地图
utils.py
- def get_c2_data():
- """
- :return: 返回各省数据
- """
- # 因为会更新多次数据,取时间戳最新的那组数据
- sql = '''
- SELECT province,sum(confirm_now)
- FROM details
- GROUP BY province;
- '''
- res = query(sql)
- return res
app.py
- @app.route('/c2')
- def get_c2_data():
- res = []
- for item in utils.get_c2_data():
- res.append({'name':item[0],'value':int(item[1])})
- return jsonify({'data':res})
get_data.py
- function get_c2_data(){
- $.ajax({
- url:'/c2',
- success:function(data){
- ec_center_option.series[0].data=data.data
- ec_center_option.series[0].data.push({
- name:"南海诸岛",value:0
- })
- ec_center.setOption(ec_center_option)
- },
- error:function(data){
- }
- });
- }
'运行
ec_center.js
- const ec_center_option = {
-
- tooltip: {
- trigger: 'item',
- formatter: '名称:{a}
省份:{b}
确诊人数:{c}' - },
- //左侧小导航图标
- visualMap: {
- show: true,
- x: 'left',
- y: 'bottom',
- textStyle: {
- fontSize: 8,
- color:['#FFFFFF']
- },
- splitList: [{ start: 0,end: 9 },
- {start: 10, end: 99 },
- { start: 100, end: 999 },
- { start: 1000, end: 9999 },
- { start: 10000 }],
- color: ['#8A3310', '#C64918', '#E55B25', '#F2AD92', '#F9DCD1']
- },
- series: [
- {
- name: '数据',
- type: 'map',
- mapType: 'china',
- roam: false,
- itemStyle: {
- normal: {
- borderWidth: .5, //区域边框宽度
- borderColor: '#62d3ff', //区域边框颜色
- areaColor: "#b7ffe6", //区域颜色
- label: { show: true }
- },
- emphasis: { //鼠标滑过地图高亮的相关设置
- borderWidth: .5,
- borderColor: '#fff',
- areaColor: "#fff",
- label: { show: true }
- }},
- data: [] // data_list
- }
- ]
- };
- ec_center = echarts.init(document.getElementById('main'));
- ec_center.setOption(ec_center_option)
到这里第三板块就完成了!
首先完成上面的柱状图
utils.py
- def get_r1_data():
- """
- :return: 返回新增确诊人数前5名的省份
- """
- sql = '''
- SELECT province,confirm FROM
- (select province ,sum(confirm_add) as confirm from details
- where update_time=(select update_time from details
- order by update_time desc limit 1)
- group by province) as a
- ORDER BY confirm DESC LIMIT 7;
- '''
- res = query(sql)
- return res
app.py
- @app.route('/r1')
- def get_r1_data():
- name,value = [],[]
- for n,v in utils.get_r1_data()[2:]:
- name.append(n)
- value.append(int(v))
- return jsonify({'name':name,'value':value})
get_data.js
- function get_r1_data(){
- $.ajax({
- url:'/r1',
- success:function(data){
- ec_right1_option.xAxis.data=data.name
- ec_right1_option.series[0].data=data.value
- ec_right1.setOption(ec_right1_option)
- }
- });
- }
'运行
ec_right1.js
- var ec_right1 = echarts.init(document.getElementById('r1'),"dark");
- var ec_right1_option = {
- //标题样式
- title : {
- text : "新增确诊人数TOP5",
- textStyle : {
- color : 'white',
- },
- left : 'left'
- },
- color: ['#3398DB'],
- tooltip: {
- trigger: 'axis',
- axisPointer: { // 坐标轴指示器,坐标轴触发有效
- type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
- }
- },
- xAxis: {
- type: 'category',
- color : 'white',
- data: []
- },
- yAxis: {
- type: 'value',
- color : 'white',
- },
- series: [{
- data: [],
- type: 'bar',
- barMaxWidth:"50%"
- }]
- };
- ec_right1.setOption(ec_right1_option)
接着完成下面的词云图
utils.py
- def get_r2_data():
- """
- :return: 返回最近的20条热搜
- """
- sql = 'select title from focu_news order by news_time desc limit 20'
- res = query(sql)
- all_word = ''
- for item in res:
- all_word += item[0]
- new_data = re.findall('[\u4e00-\u9fa5]+', all_word, re.S)
- new_data = "/".join(new_data)
- seg_list_exact = jieba.cut(new_data, cut_all=True)
- result_list = []
- with open('停用词库.txt', encoding='utf-8') as f:
- con = f.readlines()
- stop_words = set()
- for i in con:
- i = i.replace("\n", "") # 去掉读取每一行数据的\n
- stop_words.add(i)
- for word in seg_list_exact:
- if word not in stop_words and len(word) > 1:
- result_list.append(word)
- word_counts = collections.Counter(result_list)
- # 词频统计:获取前80最高频的词
- word_counts_top = word_counts.most_common(80)
- return word_counts_top
app.py
- @app.route('/r2')
- def get_r2_data():
- data = utils.get_r2_data()
- d = []
- for i in data:
- k = i[0]
- v = int(i[1])
- d.append({"name": k, "value": v})
- return jsonify({"kws": d})
get_data.py
- function get_r2_data() {
- $.ajax({
- url: "/r2",
- success: function (data) {
- ec_right2_option.series[0].data=data.kws;
- ec_right2.setOption(ec_right2_option);
- }
- })
- }
'运行
ec_right2.js
- var ec_right2 = echarts.init(document.getElementById('r2'), "dark");
-
- var ec_right2_option = {
- // backgroundColor: '#515151',
- title: {
- text: "微博热搜话题",
- textStyle: {
- color: 'white',
- },
- left: 'left'
- },
- tooltip: {
- show: false
- },
- series: [{
- type: 'wordCloud',
- // drawOutOfBound:true,
- gridSize: 1,
- sizeRange: [12, 55],
- rotationRange: [-45, 0, 45, 90],
- // maskImage: maskImage,
- textStyle: {
- normal: {
- color: function () {
- return 'rgb(' +
- Math.round(Math.random() * 255) +
- ', ' + Math.round(Math.random() * 255) +
- ', ' + Math.round(Math.random() * 255) + ')'
- }
- }
- },
- // left: 'center',
- // top: 'center',
- // // width: '96%',
- // // height: '100%',
- right: null,
- bottom: null,
- // width: 300,
- // height: 200,
- // top: 20,
- data: []
- }]
- }
-
- ec_right2.setOption(ec_right2_option);
到这里,全部的页面及渲染就编写完成!
这里忘记说了,前面每次在get_data.js中编写的函数,最后要调用才能使用
- get_c1_data()
- get_c2_data()
- get_l1_data()
- get_l2_data()
- get_r1_data()
- get_r2_data()
这里我们还需要给定义发送请求的ajax函数设置定时,也就是在get_data.js里面添加
- setInterval(gettime,1000) # 时间是1s获取一次
- setInterval(get_c1_data,1000*60*60) # 1小时发送一次请求
- setInterval(get_c2_data,1000*60*60*6)
- setInterval(get_l1_data,1000*60*60*12)
- setInterval(get_l2_data,1000*60*60*12)
- setInterval(get_r1_data,1000*60*60*6)
- setInterval(get_r2_data,1000*10)
本项目适合flask初学者来进行练手,当然前提也要会一些前端的知识,关于Echarts的使用可以去官网进行学习。关于页面的布局,可自由发挥来进行设计,或者在此基础上来进行创新,开发新的功能。