最近在一家公司实习,入职第一个大一点的需求是将公司开发的两个winstore app的排名信息进行可视化。大概挑选了下,排除了Flask和Echarts。最终选择使用Django和它的插件django-echarts来实现。文末有项目的完整代码,不想看的可以直接去下载,拆箱可用。
本篇博客主要用于记录整体的实现步骤,以及在实现过程中遇到的各个问题。
本次搭建使用 Python 2.7.14,django 1.11.8,highcharts 4.0.1
直接命令行输入以下语句,即可安装django 1.11.8
。
pip install django==1.11.8
至于Highcharts
,可以去官网下载。我用的是之前前辈给的模板,js不是太懂,所以基本没改,只是为了方便进行拓展,对功能模块进行了注释。
手头已有爬取的winstore不同app,不同榜单,不同地区的多天rank数据。这些rank数据存放在MySQL服务器中,库名为winstore,表名为winstore_rank。
现在需要将这些rank数据用折线图的方式展示出来。同时在网页上需要可以根据选择的日期,地区,榜单来动态产生折线图。
根据开发需求,可以将这次任务分为三个部分。
根据上述的分析,前端肯定是js + jQuery + Echarts + jquery.multiselect
了,服务器端采用Django,数据库方面Django有对应的驱动模块,不用管。
新建一个文件rank.html
,内容如下:
<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' %}">
<link rel="stylesheet" href="{% static 'css/jquery.multiselect.css' %}">
<link rel="stylesheet" href="{% static 'css/screen1.css' %}">
<style type="text/css">
#set-content ul li #chart {
width: 60px;
font-size: 12px;
height: 22px;
}
style>
<script type="text/javascript">
// 设定开始日期和结束日期,默认为最近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);
});
// 动态获取数据库中region数据,填充入下拉列表
$(function() {
$.get("/getWinstoreRegions",
{"limit": "0"},
function(regionsDict) {
for (var id in regionsDict) {
regionOption = " + regionsDict[id] + "";
$("#region").append(regionOption);
}
},
"json"
)
});
// 动态获取数据库中chart数据,填充入下拉列表
$(function() {
$.get("/getWinstoreCharts",
{"limit": "0"},
function(chartsDict) {
for (var id in chartsDict) {
chartOption = " + chartsDict[id] + "";
$("#chart").append(chartOption);
}
},
"json"
)
});
// 动态获取数据库中category数据,填充入下拉列表
$(function() {
$.get("/getWinstoreCategories",
{"limit": "0"},
function(categoriesDict) {
for (var id in categoriesDict) {
categoryOption = " + categoriesDict[id] + "";
$("#category").append(categoryOption);
}
},
"json"
)
});
// 动态获取数据库中app名字,填充入下拉列表
$(function() {
$.get( "/getWinstoreApps",
{"limit":"0",},
function(dataDict) {
// 循环添加下拉列表的option
for (var id in dataDict) {
appOption = " + dataDict[id] + "";
$("#appName").append(appOption);
}
// 初始化多选
$("#appName").multiselect({header: false,});
// 选中所有下拉列表项
$("#appName").multiselect("checkAll");
// 动态设置多选框的宽度
var ulList = $(".ui-multiselect-checkboxes")[0];
// 必须先单击多选下拉列表,然后才可以获取对应元素的宽度值
$(".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;
}
}
// 设置对应标签的宽度
$($(".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;
// 获取绘图数据
function queryReport(region, beginDate, endDate, chart, category, appNames) {
// 清空原有绘图数据
$("#container")[0].innerHTML = "";
// 初始化折线图参数
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: '{point.key}
',
pointFormat: '\u25AC {series.name}: {point.y}
',
},
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: [],
});
// 构造url参数
parameters = {'region': region,
'beginDate': beginDate,
'endDate': endDate,
'chart': chart,
'category': category,
'appNames': appNames
};
// 请求绘图数据
$.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'>Querybutton>
li>
ul>
div>
<div id="container">div>
body>
这里稍微解释下,在实际使用中,使用highcharts生成折线图,根据不同的数据,只需要修改series参数即可。而series参数是个啥,可以在上面的HTML代码中搜索series即可。稍微观察下,就明白了。至于你想换个大饼图,柱状图,可以 点击这里 找现成的例子,稍作修改就可以使用了。当然也许你有更多个性化的需求,那可以 点击这里 找到对应的配置项进行修改。
1、首先命令行进入到你想放置项目代码的地方
django_admin startproject winstore
2、进入刚刚新建的项目文件夹
cd winstore
3、创建新的应用rank
。这里的应用可以理解成具有独立功能的一组网页的结合,当然在本篇博客里,只有一个网页了。
python manage.py startapp rank
4、在rank
文件夹中新建文件夹templates
和static
,将刚刚新建的rank.html
放入templates
文件夹,同时将引用的js库文件放入static
文件夹下,注意文件夹层级。
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数据库的控制信息。下面是我的数据库设置:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'winstore', # 数据库名
'HOST': '127.0.0.1', # IP
'PORT': '3306', # 端口号
'USER': 'root', # 用户名
'PASSWORD': '111111', # 密码
}
}
6、编辑rank
文件夹下的views.py
文件,在rank.html
中加入必要的网页动态功能的实现。由于app的排名数据是根据其所处的榜单chart
和应用类别category
,以及不同的地区region
来确定的,所以这里里的功能实现就需要包括5个部分。分别对appName
、chart
、category
、region
实现从数据库动态获取其取值集合以及获取排名数据。对应的实现分别如下:
appName
def getWinstoreApps(request):
"""
根据接收到的GET请求返回app的取值集合
"""
# 构造SQL语句
sql = 'SELECT DISTINCT appName FROM winstore_rank'
# 默认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):
"""
根据接收到的GET请求返回chart的取值集合
"""
# 构造SQL语句
sql = 'SELECT DISTINCT chart FROM winstore_rank'
# 默认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):
"""
根据接收到的GET请求返回category的取值集合
"""
# 构造SQL语句
sql = 'SELECT DISTINCT category FROM winstore_rank'
# 默认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):
"""
根据接收到的GET请求返回region的取值集合
"""
# 构造SQL语句
sql = 'SELECT DISTINCT region FROM winstore_rank'
# 默认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)
获取排名数据
def getWinstoreRank(request):
"""
根据接收到的GET请求返回对应app的排名数据
"""
# 从GET请求中获取参数
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("@")
# 构造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)
# 以每个appName作为key,对应的排名数据列表作为value
appRank = {}
for appName in appNames:
sql = sqlTemp + '"' + appName + '"'
try:
result = getDataFromSQL(sql)
# 根据数据库返回的结果将缺少rank数据的日期补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中缺少的日期补全,同时设定排名为0
Param:
beginDate: 开始日期字符串,“2018-01-23”
endDate: 结束日期字符串, “2018-02-02”
result: 形如[(date, 23L), (date, 12L), [date, 3L]......]
Return:
按照日期顺序排列的排名数据,缺省排名为0
"""
# 将日期字符串转变为date类型数据,方便日期加减
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中的日期,方便进行判断
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]
这里主要就是构造SQL语句,然后访问数据库获取对应的数据集合。其中getDataFromSQL()
是对访问MySQL数据库的简单封装,具体代码如下:
def getDataFromSQL(sql):
"""
根据sql语句获取数据库的返回数据
"""
cursor = connection.cursor()
cursor.execute(sql)
return list(cursor.fetchall())
一些涉及到的引用可以参考文末给出的项目代码。
最终绑定一下首页
def index(request):
"""
绑定网站首页
"""
return render(request, 'rank.html')
实际应用时,相关的rank数据是通过爬虫获取的。在这里,就直接填充一些随机的rank数据进去了,不影响最终的结果。
可以看到虽然前端页面很简陋,但是功能是实现了。不过有个问题 就是重新点击query按钮后,highcharts提供的右侧页面中间下载图片的那个三道杠会出现并排的两个。
本文的完整项目代码 点击这里 就可以获取了。