文章来源:Python Django+SQL+Pandas+Pyecharts自建在线数据分析平台(一)
作者:ccpic
感谢:感谢作者 ccpic 分享的优质内容,本网页主要用于学习知识的存档备份,欢迎点击原网页支持作者。
本篇是系列文章的第5篇,之前的更新见:
(一)需求分析&技术实现
(二)初步搭建Django环境
(三)页面布局&Django模板
(四)SQL+Pandas初步处理数据
(五)前端表单交互
(六)Ajax异步传参与加载
(七)前端数据格式的处理
(八)DataTables接管前端表格
(九)Pyecharts实现交互图表
(十)静态图表的展示
(十一)“导出数据至Excel”功能
(十二)添加和配置缓存
(十三)用户登录系统
(十四)部署Django至生产环境
在上一章,我们已经成功从后端将分析结果传回前端Django模板并展示,但这个分析结果是静态的,缺乏交互性。本章我们希望在预留的filter.html模板内建立表单,从前端向后端提交数据筛选的参数。本章的内容比较容易理解,但对用户体验至关重要,是个细致活。
还是回到上一章对数据本身各字段的分析,这对表单设计也格外重要:

第4章的纸上谈兵本章依然有用
如上图,我们需要有一个必填单选代表一个分析目标字段(我还是习惯称之为breakout字段),它决定返回的数据结果里是品类份额,还是品牌份额,还是其他xx份额。它也是后端Pandas的pivot_table方法里column的动态参数。
我们还需要另外两个必填单选字段——UNIT和PERIOD,原因也请参考上图。
而我们所有的属性字段都是可为空的多选。
本例中我们表单不需要考虑AMOUNT和DATE字段。因为AMOUNT是唯一的指标字段,而我们的分析结果会取最新一个DATE做横断面结果,并计划把所有DATE的数据作为趋势分析,我们不需要对DATE动态选择。而在其他一些场景下,日期字段是经常作为表单的一员的,甚至有很多专门为其设计的calendar控件。
综上所述,我们的表单设计是下面这个样子,我们需要在filter.html文件中实现它。

TC为Therapy Class的简写,可理解为其他行业的不同层级的品类
实际前端模板代码编写前,可以后端先传一个预设的字段字典。这样操作一是分离前端方便以后修改,大部分情况下以后只修改后端就可以了;二是可以利用循环极大缩短代码长度,更加elegant。
我们再次修改views.py里index方法的代码,在context字典内增加表单的预设值传至前端:
D_MULTI_SELECT = { 'TC I': '[TC I]', 'TC II': '[TC II]', 'TC III': '[TC III]', 'TC IV': '[TC IV]', '通用名|MOLECULE': 'MOLECULE', '商品名|PRODUCT': 'PRODUCT', '包装|PACKAGE': 'PACKAGE', '生产企业|CORPORATION': 'CORPORATION', '企业类型': 'MANUF_TYPE', '剂型': 'FORMULATION', '剂量': 'STRENGTH' }
def index(request): ...
mselect_dict = {} for key, value in D_MULTI_SELECT.items(): mselect_dict[key] = {} mselect_dict[key]['select'] = value context = { ... 'mselect_dict': mselect_dict } return render(request, 'chpa_data/analysis.html', context)
|
前端html模板filter.html代码如下,为了用户体验,我们希望所有的下拉菜单都使用Semantic UI的search dropdown提供搜索响应功能,主要就是应用这个class:class=”ui fluid search dropdown”:
<div class="ui container"> <div class="ui form"> <form action="" method="post"> {% csrf_token %} <h3 class="ui header" id="analysis">分析维度</h3> <div class="field"> <div class="fields"> <div class="sixteen wide field"> <select name="DIMENSION_select" id="DIMENSION_select" class="ui fluid search dropdown"> {% for key, value in mselect_dict.items %} {% if value.select == 'PRODUCT' %} <option value="{{ value.select }}" selected>{{ key }}</option> {% else %} <option value="{{ value.select }}">{{ key }}</option> {% endif %} {% endfor %} </select> </div> </div> <div class="fields"> <div class="eight wide field"> <select name="UNIT_select" id="UNIT_select" class="ui fluid search dropdown"> <option value="Value" selected>金额</option> <option value="Volume">盒数</option> <option value="Volume (Counting Unit)">最小制剂单位数</option> </select> </div> <div class="eight wide field"> <select name="PERIOD_select" id="PERIOD_select" class="ui fluid search dropdown"> <option value="MAT" selected>滚动年</option> <option value="QTR">季度</option> </select> </div> </div> </div> <h3 class="ui header" id="data_filter">数据筛选</h3> <div class="field"> {% for key, value in mselect_dict.items %} <div class="field"> <select name="{{ value.select|add:"_select[]" }}" id="{{ value.select|add:"_select" }}" multiple="" class="ui fluid search dropdown"> <option value="">{{ key }}</option> {# {% for item in value.options %}#} {# <option value="{{ item }}">{{ item }}</option>#} {# {% endfor %}#} </select> </div> {% endfor %} </div> <br> <div class="ui buttons"> <input class="ui blue button" type='button' id='AJAX_get' value="查询"/> </div> </form> </div> </div>
<script> $('.ui.fluid.search.dropdown') .dropdown({ fullTextSearch: true }); </script>
|
这里首先我们第一次遇到了Django/Jinja2模板语法的集中应用,因为本文没有使用Django ORM,这种应用后续出场不多。我们只需要明白{% %}是功能标签,而{{ }}是变量标签,类似在模板层面的简单编程。而下方代码的意思是循环遍历后方传来的mselect_dict字典,字典的key是单选dimension_select下拉菜单选项的text,而value里嵌套的select键的值是菜单选项的value:
<select name="DIMENSION_select" id="DIMENSION_select" class="ui fluid search dropdown"> {% for key, value in mselect_dict.items %} {% if value.select == 'PRODUCT' %} <option value="{{ value.select }}" selected>{{ key }}</option> {% else %} <option value="{{ value.select }}">{{ key }}</option> {% endif %} {% endfor %} </select>
|
同理,后续又循环了一次mselect_dict,为根据字典内容生成若干个多选下拉菜单,注释掉的部分是后端动态生成备选项的一种解决方案,本文后半部分会涉及:
{% for key, value in mselect_dict.items %} <div class="field"> <select name="{{ value.select|add:"_select[]" }}" id="{{ value.select|add:"_select" }}" multiple="" class="ui fluid search dropdown"> <option value="">{{ key }}</option> {# {% for item in value.options %}#} {# <option value="{{ item }}">{{ item }}</option>#} {# {% endfor %}#} </select> </div> {% endfor %}
|
这里有一个大坑是下面这句,可能会让人觉得很奇怪(这里的|add是tag filter,下一章会解释,这并不是最奇怪的地方):
<select name="{{ value.select|add:"_select[]" }}" id="{{ value.select|add:"_select" }}" multiple="" class="ui fluid search dropdown">
|
为什么
此时再访问我们的主页http://127.0.0.1:8088/chpa/index,界面已经变成了下面这样:
筛选框已经在那了,但下方的多选框点开还没选项,我们还需要一个步骤,从后端动态传入所有多选下拉菜单的备选选项。

此时有两种常用方法:
- 在页面初始化时从后端提取所有字段的不重复值作为选项传入前端。
- 在控件搜索时根据键入关键字实时从后端返回前n个相关备选项。
第一种方法的优点是简单直接。在上方的代码块中,我们其实已经预留了注释掉的相应的代码,将views.py的index方法修改成类似下面这样,增加option_list部分传至前端:
def index(request): ...
mselect_dict = {} for key, value in D_FIELD.items(): mselect_dict[key] = {} mselect_dict[key]['select'] = value mselect_dict[key]['options'] = option_list ...
def get_distinct_list(column, db_table): sql = "Select DISTINCT " + column + " From " + db_table df = pd.read_sql_query(sql, ENGINE) l = df.values.flatten().tolist() return l
|
再在前端filter.html用下面的循环语句渲染部分:
<h3 class="ui header" id="data_filter">数据筛选</h3> <div class="field"> {% for key, value in mselect_dict.items %} <div class="field"> <select name="{{ value.select|add:"_select[]" }}" id="{{ value.select|add:"_select" }}" multiple="" class="ui fluid search dropdown"> <option value="">{{ key }}</option> {% for item in value.options %} <option value="{{ item }}">{{ item }}</option> {% endfor %} </select> </div> {% endfor %} </div>
|
很遗憾,功能是实现了,但用户体验很不好。因为我们部分字段的可选项过多,造成页面初始化加载很慢,并且点开选项较多的下拉菜单时反应也很慢。这也是初始化控件选项方法的最大缺点,不适应加载量太大的情况。

下拉菜单的Search Select功能实现了,但加载时间不可接受
但是我们必须使用search select功能,因为医药行业的专业术语太多了。于是考虑使用第二个方法,在控件搜索时根据键入关键字实时从后端返回前n个相关备选项,也就是我们说的on Server Response的方法。该方法适合表单可选项过多的场景。不使用Vue或React的情况下,Semantic UI的dropdown API就支持建设这种响应式搜索功能,并且官网提供了下方的例子:
本例中实现这种方法确实要相对复杂。我们需要先在views.py建立search方法,该方法除request外包含2个参数,要查询的字段名和查询的字符串,返回不重复的匹配结果作为前端表单选项,格式为符合Semantic UI要求格式的json。
import json
def search(request, column, kw): sql = "SELECT DISTINCT TOP 10 %s FROM %s WHERE %s like '%%%s%%'" % (column, DB_TABLE, column, kw) try: df = pd.read_sql_query(sql, ENGINE) l = df.values.flatten().tolist() results_list = [] for element in l: option_dict = {'name': element, 'value': element, } results_list.append(option_dict) res = { "success": True, "results": results_list, "code": 200, } except Exception as e: res = { "success": False, "errMsg": e, "code": 0, } return HttpResponse(json.dumps(res, ensure_ascii=False), content_type="application/json charset=utf-8")
|
上面只是个匹配关键字的最简单例子,未来还可以继续完善,例如处理多个关键字,模糊查询等。
同时,我们需要在url.py编辑对应search方法的URL pattern,并用<>括号预留column和kw两个对应的参数位置:
urlpatterns = [ ... path(r'search/<str:column>/<str:kw>', views.search, name='search') ]
|
此时可在浏览器输入上面的URL试试看效果,能看到已经正常返回预期的json了:

最后参考Semantic UI官网的例子在前端模板文件filter.html末尾加上下面这段JS代码,将后台search方法和多选框绑定。注意下方代码相对复杂有好几个坑,我都在注释一一标出了:
<script> var dict = {{ mselect_dict|safe }}; var url = "{% url 'chpa:search' 'COLUMNPLACEHOLDER' 'QUERYPLACEHOLDER' %}".replace( 'QUERYPLACEHOLDER', '{query}' ); $('.ui.fluid.search.dropdown.selection.multiple').each(function () { var text = $(this).dropdown('get default text'); var column = dict[text]['select']; $(this).dropdown( { apiSettings: { url: url.replace('COLUMNPLACEHOLDER', column) }, minCharacters : 2 }) ; }) </script>
|
在评论区有人回复下面语句会出现bug:
var text = $(this).dropdown('get default text');
|
虽然我个人没有碰到,但是如果有碰到的,可以考虑摒弃Semantic UI API,使用原生的JQuery语句:
var text = $(this).children('select').children('option:first').text();
|
至此,我们终于完成了大部分前端表单交互的表面工作。本章内容比较繁杂,又第一次在项目中引入了二手程序员的天敌JS,我们在此停笔告一段落。下一章再讨论传参和异步加载的话题。