使用 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的装饰器解决Bottle框架中用户验证问题
Apr 24 Python
Python连接mysql数据库的正确姿势
Feb 03 Python
spyder常用快捷键(分享)
Jul 19 Python
获取Django项目的全部url方法详解
Oct 26 Python
pyqt5 lineEdit设置密码隐藏,删除lineEdit已输入的内容等属性方法
Jun 24 Python
python读取并定位excel数据坐标系详解
Jun 26 Python
Python 调用 Outlook 发送邮件过程解析
Aug 08 Python
PYQT5 vscode联合操作qtdesigner的方法
Mar 24 Python
Python图片处理模块PIL操作方法(pillow)
Apr 07 Python
Python常用模块函数代码汇总解析
Aug 31 Python
python selenium xpath定位操作
Sep 01 Python
基于python实现百度语音识别和图灵对话
Nov 02 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
关于手调机和数调机的选择
2021/03/02 无线电
PHP5常用函数列表(分享)
2013/06/07 PHP
使用PHP获取当前url路径的函数以及服务器变量
2013/06/29 PHP
Laravel 5框架学习之Blade 简介
2015/04/08 PHP
php实现CSV文件导入和导出
2015/10/24 PHP
4种PHP异步执行的常用方式
2015/12/24 PHP
php实现计算百度地图坐标之间距离的方法
2016/05/05 PHP
CodeIgniter框架数据库基本操作示例
2018/05/24 PHP
js trim函数 去空格函数与正则集锦
2009/11/20 Javascript
用js实现的自定义的对话框的实现代码
2010/03/21 Javascript
Textarea与懒惰渲染实现代码
2012/01/04 Javascript
javaScript中两个等于号和三个等于号之间的区别介绍
2014/06/27 Javascript
JavaScript多图片上传案例
2015/09/28 Javascript
jQuery实现可关闭固定于底(顶)部的工具条菜单效果
2015/11/06 Javascript
nodejs接入阿里大鱼短信验证码的方法
2017/07/10 NodeJs
js canvas实现适用于移动端的百分比仪表盘dashboard
2017/07/18 Javascript
JS实现下拉菜单列表与登录注册弹窗效果
2017/08/10 Javascript
JS SetInterval 代码实现页面轮询
2017/08/11 Javascript
Vue.set()实现数据动态响应的方法
2018/02/07 Javascript
vue项目在安卓低版本机显示空白的原因分析(两种)
2018/09/04 Javascript
一篇文章,教你学会Vue CLI 插件开发
2019/04/17 Javascript
详解vue-cli中使用rem,vue自适应
2019/05/06 Javascript
js实现文字头像的生成代码
2020/03/07 Javascript
Python线程中对join方法的运用的教程
2015/04/09 Python
Python数据类型详解(四)字典:dict
2016/05/12 Python
python中的json总结
2018/10/11 Python
利用Python正则表达式过滤敏感词的方法
2019/01/21 Python
Python decorator拦截器代码实例解析
2020/04/04 Python
全方位了解CSS3的Regions扩展
2015/08/07 HTML / CSS
骆驼官方商城:CAMEL
2016/11/22 全球购物
儿科护士自我鉴定
2013/10/14 职场文书
热情服务标语
2014/10/07 职场文书
优秀党员推荐材料
2014/12/18 职场文书
幼儿园中班教育随笔
2015/08/14 职场文书
mysql字段为NULL索引是否会失效实例详解
2022/05/30 MySQL
类和原型的设计模式之复制与委托差异
2022/07/07 Javascript