本项目是基于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
项目实施
项目的最终文件目录结构如下:
1.数据采集
首先,我们得要有数据才能进行展示,这里我们选择用爬虫来进行数据采集并保存到mysql数据库中,考虑到平台限制,这里就不方便展示爬虫代码,需要的评论留言或私信。
2.搭建flask应用
这里我们先搭建一个基础的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页面(这里我就直接放最终的代码)
<!DOCTYPE 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来进行板块划分
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; }
3.可视化展示
接下来我将按照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}<br/>省份:{b}<br/>确诊人数:{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);
到这里,全部的页面及渲染就编写完成!
4.添加定时任务
这里忘记说了,前面每次在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的使用可以去官网进行学习。关于页面的布局,可自由发挥来进行设计,或者在此基础上来进行创新,开发新的功能。