使用 Django Highcharts 实现数据可视化过程解析


Posted in Python onJuly 31, 2019

概述

最近在一家公司实习,入职第一个大一点的需求是将公司开发的两个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数据用折线图的方式展示出来。同时在网页上需要可以根据选择的日期,地区,榜单来动态产生折线图。

问题解析

根据开发需求,可以将这次任务分为三个部分。

前端页面

a. ajax动态获取地区列表、榜单列表,生成对应的下拉列表,必要时需将传统下拉列表转换成多选下拉列表

b. 根据搜索结果,将符合条件的app的rank添加到折线图中

服务器端

a. 接受前端的请求,与数据库通信,返回所请求数据

MySQL数据库

a. 根据服务器端传输的sql语句进行对应的查询

根据上述的分析,前端肯定是js + jQuery + Echarts + jquery.multiselect了,服务器端采用Django,数据库方面Django有对应的驱动模块,不用管。

1. 前端页面

新建一个文件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' %}" 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">

  // 设定开始日期和结束日期,默认为最近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 = "<option value='" + id + "'>" + regionsDict[id] + "</option>";
       $("#region").append(regionOption);
      }
     },
     "json"
     )
  });

  // 动态获取数据库中chart数据,填充入下拉列表
  $(function() {
   $.get("/getWinstoreCharts",
     {"limit": "0"},
     function(chartsDict) {
      for (var id in chartsDict) {
       chartOption = "<option value='" + id + "'>" + chartsDict[id] + "</option>";
       $("#chart").append(chartOption);
      }
     },
     "json"
     )
  });
  // 动态获取数据库中category数据,填充入下拉列表
  $(function() {
   $.get("/getWinstoreCategories",
     {"limit": "0"},
     function(categoriesDict) {
      for (var id in categoriesDict) {
       categoryOption = "<option value='" + id + "'>" + categoriesDict[id] + "</option>";
       $("#category").append(categoryOption);
      }
     },
     "json"
     )
  });
  // 动态获取数据库中app名字,填充入下拉列表
  $(function() {
   $.get( "/getWinstoreApps",
    {"limit":"0",},
    function(dataDict) {
     // 循环添加下拉列表的option
     for (var id in dataDict) {
      appOption = "<option value='" + id + "'>" + dataDict[id] + "</option>";
      $("#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: '<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: [],
           });
   // 构造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'>Query</button>
   </li>
  </ul>
 </div>
 <div id="container"></div>
</body>

这里稍微解释下,在实际使用中,使用highcharts生成折线图,根据不同的数据,只需要修改series参数即可。而series参数是个啥,可以在上面的HTML代码中搜索series即可。稍微观察下,就明白了。

至于你想换个大饼图,柱状图,可以 点击这里 找现成的例子,稍作修改就可以使用了。当然也许你有更多个性化的需求,那可以 点击这里 找到对应的配置项进行修改。

2. 服务器端

1、首先命令行进入到你想放置项目代码的地方

django_admin startproject winstore

2、进入刚刚新建的项目文件夹

cd winstore

3、创建新的应用rank。这里的应用可以理解成具有独立功能的一组网页的结合,当然在本篇博客里,只有一个网页了。

python manage.py startapp rank

4、在rank文件夹中新建文件夹templatesstatic,将刚刚新建的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个部分。分别对appNamechartcategoryregion 实现从数据库动态获取其取值集合以及获取排名数据。对应的实现分别如下:

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')

3. MySQL数据库

实际应用时,相关的rank数据是通过爬虫获取的。在这里,就直接填充一些随机的rank数据进去了,不影响最终的结果。

最终成果展示

首页

使用 Django Highcharts 实现数据可视化过程解析

rank数据展示

使用 Django Highcharts 实现数据可视化过程解析

可以看到虽然前端页面很简陋,但是功能是实现了。不过有个问题 就是重新点击query按钮后,highcharts提供的右侧页面中间下载图片的那个三道杠会出现并排的两个。

本文的完整项目代码 点击这里 就可以获取了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python调用机器喇叭发出蜂鸣声(Beep)的方法
Mar 23 Python
python实现挑选出来100以内的质数
Mar 24 Python
详解Python中for循环的使用方法
May 14 Python
Python常见加密模块用法分析【MD5,sha,crypt模块】
May 24 Python
python中闭包Closure函数作为返回值的方法示例
Dec 17 Python
Python面向对象编程之继承与多态详解
Jan 16 Python
Appium Python自动化测试之环境搭建的步骤
Jan 23 Python
PyTorch的深度学习入门教程之构建神经网络
Jun 27 Python
Keras模型转成tensorflow的.pb操作
Jul 06 Python
利用Opencv实现图片的油画特效实例
Feb 28 Python
详解python第三方库的安装、PyInstaller库、random库
Mar 03 Python
用Python监控你的朋友都在浏览哪些网站?
May 27 Python
利用Python检测URL状态
Jul 31 #Python
Python解析json时提示“string indices must be integers”问题解决方法
Jul 31 #Python
Python Web程序搭建简单的Web服务器
Jul 31 #Python
python字典的常用方法总结
Jul 31 #Python
python Django的web开发实例(入门)
Jul 31 #Python
Flask框架模板继承实现方法分析
Jul 31 #Python
Flask框架模板渲染操作简单示例
Jul 31 #Python
You might like
下拉列表多级联动dropDownList示例代码
2013/06/27 PHP
js中cookie的使用详细分析
2008/05/28 Javascript
初学JavaScript第二章
2008/09/30 Javascript
jQuery中html()方法用法实例
2014/12/25 Javascript
浅谈NodeJS中require路径问题
2015/05/07 NodeJs
javascript表格的渲染组件
2015/07/03 Javascript
Angularjs使用directive自定义指令实现attribute继承的方法详解
2016/08/05 Javascript
简单分析javascript中的函数
2016/09/10 Javascript
Bootstrap模态框禁用空白处点击关闭
2016/10/20 Javascript
JS常见简单正则表达式验证功能小结【手机,地址,企业税号,金额,身份证等】
2017/01/22 Javascript
jQuery弹出窗口简单实现代码
2017/03/09 Javascript
js事件on动态绑定数据,绑定多个事件的方法
2018/09/15 Javascript
微信小程序开发之点击按钮退出小程序的实现方法
2019/04/26 Javascript
Vue将页面导出为图片或者PDF
2020/08/17 Javascript
JointJS JavaScript流程图绘制框架解析
2019/08/15 Javascript
使用Layui搭建后台管理界面的操作方法
2019/09/20 Javascript
在Python中利用Pandas库处理大数据的简单介绍
2015/04/07 Python
MySQL适配器PyMySQL详解
2017/09/20 Python
python 简单备份文件脚本v1.0的实例
2017/11/06 Python
python的Tqdm模块的使用
2018/01/10 Python
python实现随机调用一个浏览器打开网页
2018/04/21 Python
Python os.rename() 重命名目录和文件的示例
2018/10/25 Python
对Django中static(静态)文件详解以及{% static %}标签的使用方法
2019/07/28 Python
Pytorch 实现focal_loss 多类别和二分类示例
2020/01/14 Python
python 实现全球IP归属地查询工具
2020/12/18 Python
高中政治教学反思
2014/01/18 职场文书
社区党员先进事迹
2014/01/22 职场文书
小学端午节活动方案
2014/03/13 职场文书
基层党建工作汇报材料
2014/08/15 职场文书
七夕相亲活动策划方案
2014/08/31 职场文书
2014年社团工作总结范文
2014/11/27 职场文书
销售2014年度工作总结
2014/12/08 职场文书
三年级学生期末评语
2014/12/26 职场文书
安全教育主题班会教案
2015/08/12 职场文书
同学聚会开幕词
2019/04/02 职场文书
2019中秋节祝福语大全,提前收藏啦
2019/09/10 职场文书