使用 Django Highcharts 實(shí)現(xiàn)數(shù)據(jù)可視化過程解析
概述
最近在一家公司實(shí)習(xí),入職第一個(gè)大一點(diǎn)的需求是將公司開發(fā)的兩個(gè)winstore app的排名信息進(jìn)行可視化。大概挑選了下,排除了Flask和Echarts。最終選擇使用Django和它的插件django-echarts來實(shí)現(xiàn)。文末有項(xiàng)目的完整代碼,不想看的可以直接去下載,拆箱可用。
本篇博客主要用于記錄整體的實(shí)現(xiàn)步驟,以及在實(shí)現(xiàn)過程中遇到的各個(gè)問題。
開發(fā)環(huán)境
本次搭建使用 Python 2.7.14,django 1.11.8,highcharts 4.0.1
直接命令行輸入以下語句,即可安裝django 1.11.8。
pip install django==1.11.8
至于Highcharts,可以去官網(wǎng)下載。我用的是之前前輩給的模板,js不是太懂,所以基本沒改,只是為了方便進(jìn)行拓展,對功能模塊進(jìn)行了注釋。
開發(fā)需求
手頭已有爬取的winstore不同app,不同榜單,不同地區(qū)的多天rank數(shù)據(jù)。這些rank數(shù)據(jù)存放在MySQL服務(wù)器中,庫名為winstore,表名為winstore_rank。
現(xiàn)在需要將這些rank數(shù)據(jù)用折線圖的方式展示出來。同時(shí)在網(wǎng)頁上需要可以根據(jù)選擇的日期,地區(qū),榜單來動(dòng)態(tài)產(chǎn)生折線圖。
問題解析
根據(jù)開發(fā)需求,可以將這次任務(wù)分為三個(gè)部分。
前端頁面
a. ajax動(dòng)態(tài)獲取地區(qū)列表、榜單列表,生成對應(yīng)的下拉列表,必要時(shí)需將傳統(tǒng)下拉列表轉(zhuǎn)換成多選下拉列表
b. 根據(jù)搜索結(jié)果,將符合條件的app的rank添加到折線圖中
服務(wù)器端
a. 接受前端的請求,與數(shù)據(jù)庫通信,返回所請求數(shù)據(jù)
MySQL數(shù)據(jù)庫
a. 根據(jù)服務(wù)器端傳輸?shù)膕ql語句進(jìn)行對應(yīng)的查詢
根據(jù)上述的分析,前端肯定是js + jQuery + Echarts + jquery.multiselect了,服務(wù)器端采用Django,數(shù)據(jù)庫方面Django有對應(yīng)的驅(qū)動(dòng)模塊,不用管。
1. 前端頁面
新建一個(gè)文件rank.html,內(nèi)容如下:
<head>
{% load static %}
<script type="text/javascript" src="{% static 'js/jquery.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/highcharts.js' %}"></script>
<script type="text/javascript" src="{% static 'js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/exporting.js' %}"></script>
<script type="text/javascript" src="{% static 'js/jquery.multiselect.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/jquery-ui.min.css' %}" rel="external nofollow" >
<link rel="stylesheet" href="{% static 'css/jquery.multiselect.css' %}" rel="external nofollow" >
<link rel="stylesheet" href="{% static 'css/screen1.css' %}" rel="external nofollow" >
<style type="text/css">
#set-content ul li #chart {
width: 60px;
font-size: 12px;
height: 22px;
}
</style>
<script type="text/javascript">
// 設(shè)定開始日期和結(jié)束日期,默認(rèn)為最近10天
$(function() {
$("#beginDate").datepicker({dateFormat: "yy-mm-dd"});
$("#endDate").datepicker({dateFormat: "yy-mm-dd"});
var dateNow = new Date();
var str_dateNow = dateNow.getFullYear() + "-" + (dateNow.getMonth() + 1) + "-" + dateNow.getDate();
var dateBegin = new Date(dateNow - 10 * 1000 * 3600 * 24);
var str_dateBegin = dateBegin.getFullYear() + "-" + (dateBegin.getMonth() + 1) + "-" + dateBegin.getDate();
$("#beginDate").datepicker("setDate", str_dateBegin);
$("#endDate").datepicker("setDate", str_dateNow);
});
// 動(dòng)態(tài)獲取數(shù)據(jù)庫中region數(shù)據(jù),填充入下拉列表
$(function() {
$.get("/getWinstoreRegions",
{"limit": "0"},
function(regionsDict) {
for (var id in regionsDict) {
regionOption = "<option value='" + id + "'>" + regionsDict[id] + "</option>";
$("#region").append(regionOption);
}
},
"json"
)
});
// 動(dòng)態(tài)獲取數(shù)據(jù)庫中chart數(shù)據(jù),填充入下拉列表
$(function() {
$.get("/getWinstoreCharts",
{"limit": "0"},
function(chartsDict) {
for (var id in chartsDict) {
chartOption = "<option value='" + id + "'>" + chartsDict[id] + "</option>";
$("#chart").append(chartOption);
}
},
"json"
)
});
// 動(dòng)態(tài)獲取數(shù)據(jù)庫中category數(shù)據(jù),填充入下拉列表
$(function() {
$.get("/getWinstoreCategories",
{"limit": "0"},
function(categoriesDict) {
for (var id in categoriesDict) {
categoryOption = "<option value='" + id + "'>" + categoriesDict[id] + "</option>";
$("#category").append(categoryOption);
}
},
"json"
)
});
// 動(dòng)態(tài)獲取數(shù)據(jù)庫中app名字,填充入下拉列表
$(function() {
$.get( "/getWinstoreApps",
{"limit":"0",},
function(dataDict) {
// 循環(huán)添加下拉列表的option
for (var id in dataDict) {
appOption = "<option value='" + id + "'>" + dataDict[id] + "</option>";
$("#appName").append(appOption);
}
// 初始化多選
$("#appName").multiselect({header: false,});
// 選中所有下拉列表項(xiàng)
$("#appName").multiselect("checkAll");
// 動(dòng)態(tài)設(shè)置多選框的寬度
var ulList = $(".ui-multiselect-checkboxes")[0];
// 必須先單擊多選下拉列表,然后才可以獲取對應(yīng)元素的寬度值
$(".ui-multiselect")[0].click();
var maxWidth = 0;
for (var i = 0; i < ulList.childElementCount; i++) {
var currentInputWidth = $(ulList.childNodes[i]).find("input")[0].offsetWidth;
var currentSpanWidth = $(ulList.childNodes[i]).find("span")[0].offsetWidth;
var currentWidth = currentSpanWidth + currentInputWidth * 3;
if (currentWidth > maxWidth) {
maxWidth = currentWidth;
}
}
// 設(shè)置對應(yīng)標(biāo)簽的寬度
$($(".ui-multiselect")[0]).width(maxWidth);
$($(".ui-multiselect-menu")[0]).width(maxWidth + 6);
// 二次單擊
$(".ui-multiselect")[0].click();
},
"json");
});
// 綁定query按鈕的單擊操作
$(function() {
$("#query").click(function() {
var region = $("#region").val();
var beginDate = $("#beginDate").val();
var endDate = $("#endDate").val();
var chart = $("#chart").val();
var appNames = $("#appName").val();
var category = $("#category").val();
// 將appNames連接成字符串
queryReport(region, beginDate, endDate, chart, category, appNames.join("@"));
});
})
var lineChart;
// 獲取繪圖數(shù)據(jù)
function queryReport(region, beginDate, endDate, chart, category, appNames) {
// 清空原有繪圖數(shù)據(jù)
$("#container")[0].innerHTML = "";
// 初始化折線圖參數(shù)
var lineChart = new Highcharts.Chart({
chart: {
renderTo: 'container', type: 'line'
},
title: {
text: 'Daily Ranking',
style: {fontFamily: 'Helvetica', fontWeight: '200'}
},
subtitle: {
text: 'By Product',
style: {fontFamily: 'Helvetica', fontWeight: '200'}
},
xAxis: [{ // master axis
type: 'datetime',
gridLineWidth:1,
gridLineDashStyle: 'longdash',
tickInterval: 24 * 3600 * 1000,
}, { // slave axis
type: 'datetime',
linkedTo: 0,
opposite: true,
tickInterval: 24 * 3600 * 1000,
labels: {
formatter: function () {return Highcharts.dateFormat('%a', this.value);}
}
}], tooltip: {
headerFormat: '<span>{point.key}</span><br/>',
pointFormat: '<span style="color:{series.color}">\u25AC</span> {series.name}: <b>{point.y}</b><br/>',
},
yAxis: [{ // Primary yAxis
min:1,
reversed: true,
labels: {
format: 'No. {value}',
style: {
color: '#4572A7'
}
},
title: {
text: 'Ranking',
style: {
color: '#4572A7'
}
}
},
{ // Secondary yAxis
min:1,
reversed: true,
title: {
text: 'Ranking',
style: {
color: '#4572A7'
}
},
labels: {
format: 'No. {value}',
style: {
color: '#4572A7'
}
},
opposite: true,
}],
plotOptions: {
column: {
dataLabels: {
enabled: true
},
enableMouseTracking: true
},
line: {
dataLabels: {
enabled: true
},
enableMouseTracking: true
}
},
series: [],
});
// 構(gòu)造url參數(shù)
parameters = {'region': region,
'beginDate': beginDate,
'endDate': endDate,
'chart': chart,
'category': category,
'appNames': appNames
};
// 請求繪圖數(shù)據(jù)
$.get("/getWinstoreRank",
parameters,
function(rankDict) {
var ranksOfApp = new Array();
for (var app in rankDict) {
lineChart.addSeries({
name: app,
data: rankDict[app]
});
}
},
"json"
);
}
</script>
</head>
<body>
<div id="set-content">
<ul>
<li>
<label for="region">Country/Region: </label>
<select id="region"></select>
</li>
<li>
<label for="beginDate">Begin Date: </label>
<input type="text" id="beginDate">
</li>
<li>
<label for="endDate">End Date: </label>
<input type="text" id="endDate">
</li>
<li>
<label for="chart">Chart: </label>
<select id="chart"></select>
</li>
<li>
<label for="category">Category: </label>
<select id="category"></select>
</li>
<li>
<label for="appName">App:</label>
<select id="appName" multiple="multiple" size="4"></select>
</li>
<li>
<button id='query'>Query</button>
</li>
</ul>
</div>
<div id="container"></div>
</body>
這里稍微解釋下,在實(shí)際使用中,使用highcharts生成折線圖,根據(jù)不同的數(shù)據(jù),只需要修改series參數(shù)即可。而series參數(shù)是個(gè)啥,可以在上面的HTML代碼中搜索series即可。稍微觀察下,就明白了。
至于你想換個(gè)大餅圖,柱狀圖,可以 點(diǎn)擊這里 找現(xiàn)成的例子,稍作修改就可以使用了。當(dāng)然也許你有更多個(gè)性化的需求,那可以 點(diǎn)擊這里 找到對應(yīng)的配置項(xiàng)進(jìn)行修改。
2. 服務(wù)器端
1、首先命令行進(jìn)入到你想放置項(xiàng)目代碼的地方
django_admin startproject winstore
2、進(jìn)入剛剛新建的項(xiàng)目文件夾
cd winstore
3、創(chuàng)建新的應(yīng)用rank。這里的應(yīng)用可以理解成具有獨(dú)立功能的一組網(wǎng)頁的結(jié)合,當(dāng)然在本篇博客里,只有一個(gè)網(wǎng)頁了。
python manage.py startapp rank
4、在rank文件夾中新建文件夾templates和static,將剛剛新建的rank.html放入templates文件夾,同時(shí)將引用的js庫文件放入static文件夾下,注意文件夾層級(jí)。
5.、打開winstore文件夾下的settings.py ,在INSTALL_APPS 下添加rank,添加之后如下:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rank' # 添加的部分 ]
將DATABASES修改成你自己的MySQL數(shù)據(jù)庫的控制信息。下面是我的數(shù)據(jù)庫設(shè)置:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'winstore', # 數(shù)據(jù)庫名
'HOST': '127.0.0.1', # IP
'PORT': '3306', # 端口號(hào)
'USER': 'root', # 用戶名
'PASSWORD': '111111', # 密碼
}
}
6、編輯rank文件夾下的views.py文件,在rank.html中加入必要的網(wǎng)頁動(dòng)態(tài)功能的實(shí)現(xiàn)。由于app的排名數(shù)據(jù)是根據(jù)其所處的榜單chart和應(yīng)用類別category,以及不同的地區(qū)region來確定的,所以這里里的功能實(shí)現(xiàn)就需要包括5個(gè)部分。分別對appName、chart、category、region 實(shí)現(xiàn)從數(shù)據(jù)庫動(dòng)態(tài)獲取其取值集合以及獲取排名數(shù)據(jù)。對應(yīng)的實(shí)現(xiàn)分別如下:
appName
def getWinstoreApps(request):
"""
根據(jù)接收到的GET請求返回app的取值集合
"""
# 構(gòu)造SQL語句
sql = 'SELECT DISTINCT appName FROM winstore_rank'
# 默認(rèn)appNames的key和value相同
appNames = {}
try:
result = getDataFromSQL(sql)
result = [r[0] for r in result]
for key in result:
appNames[key] = key
except Exception as e:
print('getWinstoreApps ERROR: ' + str(e))
appNames['QQ'] = 'QQ'
return JsonResponse(appNames)
chart
def getWinstoreCharts(request):
"""
根據(jù)接收到的GET請求返回chart的取值集合
"""
# 構(gòu)造SQL語句
sql = 'SELECT DISTINCT chart FROM winstore_rank'
# 默認(rèn)charts的key和value相同
charts = {}
try:
result = getDataFromSQL(sql)
result = [r[0] for r in result]
for key in result:
charts[key] = key
except Exception as e:
print('getWinstoreCharts ERROR: ' + str(e))
charts['Free'] = 'Free'
return JsonResponse(charts)
category
def getWinstoreCategories(request):
"""
根據(jù)接收到的GET請求返回category的取值集合
"""
# 構(gòu)造SQL語句
sql = 'SELECT DISTINCT category FROM winstore_rank'
# 默認(rèn)categories的key和value相同
categories = {}
try:
result = getDataFromSQL(sql)
result = [r[0] for r in result]
for key in result:
categories[key] = key
except Exception as e:
print('getWinstoreCategories ERROR: ' + str(e))
categories['Education'] = 'Education'
return JsonResponse(categories)
region
def getWinstoreRegions(request):
"""
根據(jù)接收到的GET請求返回region的取值集合
"""
# 構(gòu)造SQL語句
sql = 'SELECT DISTINCT region FROM winstore_rank'
# 默認(rèn)regions的key和value相同
regions = {}
try:
result = getDataFromSQL(sql)
result = [r[0] for r in result]
for key in result:
regions[key] = key
except Exception as e:
print('getWinstoreRegions ERROR: ' + str(e))
regions['EN-US'] = 'EN-US'
return JsonResponse(regions)
獲取排名數(shù)據(jù)
def getWinstoreRank(request):
"""
根據(jù)接收到的GET請求返回對應(yīng)app的排名數(shù)據(jù)
"""
# 從GET請求中獲取參數(shù)
region = request.GET.get("region", "EN-US")
chart = request.GET.get("chart", "Free")
category = request.GET.get("category", "Education")
beginDate = request.GET.get("beginDate", "2018-01-22")
endDate = request.GET.get("endDate", "2018-02-02")
appNames = request.GET.get("appNames", "QQ").split("@")
# 構(gòu)造SQL語句
sqlTemp = 'SELECT the_date, rank FROM winstore_rank WHERE ' \
'region="%s" AND chart="%s" AND category="%s" AND ' \
'the_date BETWEEN "%s" AND "%s" AND ' \
'appName=' % (region, chart, category, beginDate, endDate)
# 以每個(gè)appName作為key,對應(yīng)的排名數(shù)據(jù)列表作為value
appRank = {}
for appName in appNames:
sql = sqlTemp + '"' + appName + '"'
try:
result = getDataFromSQL(sql)
# 根據(jù)數(shù)據(jù)庫返回的結(jié)果將缺少rank數(shù)據(jù)的日期補(bǔ)0
result = addZeroToRank(beginDate, endDate, result)
appRank[appName] = result
except Exception as e:
print('getWinstoreRank ERROR: ' + str(e))
return JsonResponse(appRank)
def addZeroToRank(beginDate, endDate, result):
"""
以beginDate和endDate為日期的起始,將result中缺少的日期補(bǔ)全,同時(shí)設(shè)定排名為0
Param:
beginDate: 開始日期字符串,“2018-01-23”
endDate: 結(jié)束日期字符串, “2018-02-02”
result: 形如[(date, 23L), (date, 12L), [date, 3L]......]
Return:
按照日期順序排列的排名數(shù)據(jù),缺省排名為0
"""
# 將日期字符串轉(zhuǎn)變?yōu)閐ate類型數(shù)據(jù),方便日期加減
y, m, d = [int(i) for i in beginDate.split("-")]
begin = datetime.date(y, m, d)
y, m, d = [int(i) for i in endDate.split("-")]
end = datetime.date(y, m, d)
current = begin
# 獲取result中的日期,方便進(jìn)行判斷
resultTemp = [r[0] for r in result]
while current <= end:
if not (current in resultTemp):
result.append((current, 0))
current += datetime.timedelta(days=1)
result.sort(key=lambda x: x[0])
return [int(r[1]) for r in result]
這里主要就是構(gòu)造SQL語句,然后訪問數(shù)據(jù)庫獲取對應(yīng)的數(shù)據(jù)集合。其中getDataFromSQL() 是對訪問MySQL數(shù)據(jù)庫的簡單封裝,具體代碼如下:
def getDataFromSQL(sql): """ 根據(jù)sql語句獲取數(shù)據(jù)庫的返回?cái)?shù)據(jù) """ cursor = connection.cursor() cursor.execute(sql) return list(cursor.fetchall())
一些涉及到的引用可以參考文末給出的項(xiàng)目代碼。
最終綁定一下首頁
def index(request): """ 綁定網(wǎng)站首頁 """ return render(request, 'rank.html')
3. MySQL數(shù)據(jù)庫
實(shí)際應(yīng)用時(shí),相關(guān)的rank數(shù)據(jù)是通過爬蟲獲取的。在這里,就直接填充一些隨機(jī)的rank數(shù)據(jù)進(jìn)去了,不影響最終的結(jié)果。
最終成果展示
首頁

rank數(shù)據(jù)展示

可以看到雖然前端頁面很簡陋,但是功能是實(shí)現(xiàn)了。不過有個(gè)問題 就是重新點(diǎn)擊query按鈕后,highcharts提供的右側(cè)頁面中間下載圖片的那個(gè)三道杠會(huì)出現(xiàn)并排的兩個(gè)。
本文的完整項(xiàng)目代碼 點(diǎn)擊這里 就可以獲取了。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Python實(shí)現(xiàn)爬取知乎神回復(fù)簡單爬蟲代碼分享
這篇文章主要介紹了Python實(shí)現(xiàn)爬取知乎神回復(fù)簡單爬蟲代碼分享,本文實(shí)現(xiàn)了爬取知乎的“如何正確地吐槽”收藏夾,是對個(gè)人的一個(gè)興趣實(shí)現(xiàn),需要的朋友可以參考下2015-01-01
解決tensorflow測試模型時(shí)NotFoundError錯(cuò)誤的問題
今天小編就為大家分享一篇解決tensorflow測試模型時(shí)NotFoundError錯(cuò)誤的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-07-07
Python程序打包exe報(bào)錯(cuò)的幾種解決方法
本文主要介紹了Python程序打包exe報(bào)錯(cuò)的幾種解決方法,文中通過幾種解決方法的介紹非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2023-08-08
pandas.DataFrame.drop_duplicates 用法介紹
這篇文章主要介紹了pandas.DataFrame.drop_duplicates 用法介紹,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07
Python安裝Imaging報(bào)錯(cuò):The _imaging C module is not installed問題解決
這篇文章主要介紹了Python安裝Imaging報(bào)錯(cuò):The _imaging C module is not installed問題解決方法,原來是PIL庫的庫文件沒有加到系統(tǒng)中導(dǎo)致老是提示這個(gè)錯(cuò)誤,需要的朋友可以參考下2014-08-08
python實(shí)現(xiàn)隨機(jī)梯度下降法
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)隨機(jī)梯度下降法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
python3實(shí)現(xiàn)語音轉(zhuǎn)文字(語音識(shí)別)和文字轉(zhuǎn)語音(語音合成)
這篇文章主要介紹了python3實(shí)現(xiàn)語音轉(zhuǎn)文字(語音識(shí)別)和文字轉(zhuǎn)語音(語音合成),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10

