使用 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启动办公软件进程(word、excel、ppt、以及wps的et、wps、wpp)
Apr 09 Python
Python实现抓取城市的PM2.5浓度和排名
Mar 19 Python
python使用cStringIO实现临时内存文件访问的方法
Mar 26 Python
Python语言描述KNN算法与Kd树
Dec 13 Python
PyQt5每天必学之事件与信号
Apr 20 Python
Python实现输入二叉树的先序和中序遍历,再输出后序遍历操作示例
Jul 27 Python
解决python通过cx_Oracle模块连接Oracle乱码的问题
Oct 18 Python
Django logging配置及使用详解
Jul 23 Python
Python编写打字训练小程序
Sep 26 Python
pytorch nn.Conv2d()中的padding以及输出大小方式
Jan 10 Python
Python编写memcached启动脚本代码实例
Aug 14 Python
python Selenium 库的使用技巧
Oct 16 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
php遍历目录输出目录及其下的所有文件示例
2014/01/27 PHP
PHP 函数call_user_func和call_user_func_array用法详解
2014/03/02 PHP
php实现字符串首字母大写和单词首字母大写的方法
2015/03/14 PHP
php使用simplexml_load_file加载XML文件并显示XML的方法
2015/03/19 PHP
PHP脚本自动识别验证码查询汽车违章
2016/12/20 PHP
JQuery Tips(2) 关于$()包装集你不知道的
2009/12/14 Javascript
jquery关于页面焦点的定位(文本框获取焦点时改变样式 )
2010/09/10 Javascript
使用jQuery操作Cookies的实现代码
2011/10/09 Javascript
JavaScript高级程序设计阅读笔记(五) ECMAScript中的运算符(一)
2012/02/27 Javascript
查找页面中所有类为test的结点的方法
2014/03/28 Javascript
Javascript 正则表达式实现为数字添加千位分隔符
2015/03/10 Javascript
yii form 表单提交之前JS在提交按钮的验证方法
2017/03/15 Javascript
Angular.js实现动态加载组件详解
2017/05/28 Javascript
bootstrap table服务端实现分页效果
2017/08/10 Javascript
分析JS单线程异步io回调的特性
2017/12/01 Javascript
js最简单的双向绑定实例讲解
2018/01/02 Javascript
python类型强制转换long to int的代码
2013/02/10 Python
Python编写的com组件发生R6034错误的原因与解决办法
2013/04/01 Python
一个计算身份证号码校验位的Python小程序
2014/08/15 Python
Windows系统下安装Python的SSH模块教程
2015/02/05 Python
使用PyCharm配合部署Python的Django框架的配置纪实
2015/11/19 Python
Pyqt实现无边框窗口拖动以及窗口大小改变
2018/04/19 Python
Python使用pyautogui模块实现自动化鼠标和键盘操作示例
2018/09/04 Python
Python pandas DataFrame操作的实现代码
2019/06/21 Python
python画图--输出指定像素点的颜色值方法
2019/07/03 Python
Python学习笔记之迭代器和生成器用法实例详解
2019/08/08 Python
Python实现企业微信机器人每天定时发消息实例
2020/02/25 Python
酒吧创业计划书
2014/01/18 职场文书
奥利奥广告词
2014/03/20 职场文书
教师党员批评与自我批评
2014/10/15 职场文书
打架检讨书
2015/01/27 职场文书
专项资金申请报告
2015/05/15 职场文书
传单、海报早OUT了,另类传单营销方案送给你!
2019/07/15 职场文书
Nginx配置并兼容HTTP实现代码解析
2021/03/31 Servers
七个非常实用的Python工具包总结
2021/06/15 Python
vue3获取当前路由地址
2022/02/18 Vue.js