文章来源Python Django+SQL+Pandas+Pyecharts自建在线数据分析平台(一)
作者ccpic
感谢:感谢作者 ccpic 分享的优质内容,本网页主要用于学习知识的存档备份,欢迎点击原网页支持作者。

(一)需求分析&技术实现

(二)初步搭建Django环境

(三)页面布局&Django模板

(四)SQL+Pandas初步处理数据

(五)前端表单交互

(六)Ajax异步传参与加载

(七)前端数据格式的处理

(八)DataTables接管前端表格

(九)Pyecharts实现交互图表

(十)静态图表的展示

(十一)“导出数据至Excel”功能

(十二)添加和配置缓存

(十三)用户登录系统

(十四)部署Django至生产环境

我一直认为表格是数据分析可视化的最重要形式,是整个项目最终成果的重中之重,因此放到交互图表之前来说。分析人员应该把制造酷炫的dashboard的激情分一点到表格上来,而呈现一个好的表格实际是一个非常综合且细节的用户体验管理,既要呈现美观醒目的结果,又要包含丰富实用的功能。

一个优秀的表格解决方案绝对不是个人就能搞定的小工程。因此,本章尝试用jQuery的DataTables插件接管前端表格,DataTables可能不是最后呈现效果最好的方案,甚至有些过时。但是胜在高度灵活,功能完善,社区活跃,基本上你想到的都能实现。本章的内容也会主要聚焦在一些细节的雕琢上,争取用最简单的方式获取尽可能佳的用户体验。

DataTables恰巧有我们Semantic UI的主题,那我们就使用它使整体风格更统一。在DataTables官网下载时记得要勾选,下载下来的css才会是Semantic UI主题的:

在第3章的base.html模板里,下面的引用部分都是和DataTables相关的。我们多下了一个百分比条形图插件percentageBars,接下来会用到:

<!-- DataTables Semantic UI主题的CSS -->
<link rel="stylesheet" href="{&#37; static 'datatables/dataTables.semanticui.min.css' &#37;}">
<!-- DataTables 主JS -->
<script src="{&#37; static 'datatables/jquery.dataTables.min.js' &#37;}"></script>
<!-- DataTables Semantic UI主题的JS -->
<script src="{&#37; static 'datatables/dataTables.semanticui.min.js' &#37;}"></script>
<!-- DataTables 百分数条形图插件 -->
<script src="{&#37; static 'datatables/percentageBars.js' &#37;}"></script>

DataTables初始化的方式非常灵活多变,主要差别在于:

  • 数据源是DOM,AJAX json, js还是server-side processing
  • 分页是在前端还是服务器端
  • 初始化的设置是全部js执行还是html里指定列title的方式

我的建议当然还是根据应用场景因地制宜,本次项目我们之前的操作都是通过Pandas的.to_html()方法传一整个DOM表格(如果用drf做api则DataTabels初始化方法会完全不同)。一般分析的结果也不会有很多行不需要服务器端分页(但在表格条目特别多的场景服务器端分页然后异步加载则异常重要)。

总之,我们最后选择使用数据源为DOM,前端分页,JS设置的方法。

首先,在前两章的table.to_html()方法里多声明2个参数,给AJAX传的DOM表格指定一个css class和id:

def query(request):
...
table = table.to_html(formatters=build_formatters_by_col(table), # 逐列调整表格内数字格式
classes='ui selectable celled table', # 指定表格css class为Semantic UI主题
table_id='ptable' # 指定表格id
)
...

上方代码中的classes=’ui selectable celled table’即为Semantic UI的表格样式,可以参考下方页面选择自己喜欢的各种变种:

在filter.html里的js部分加入一个initTable方法,调用DataTables的默认初始化语句:

<script>
function initTable(table) {
table.DataTable(
{
}
);
}
</script>

再在第六章AJAX回传的success参数里根据后端新指定的id “ptable”指定表格元素并call这个initTable方法就可以了:

<script type="text/javascript">
$("#AJAX_get").click(function () {
...
$.ajax({
...
success: function (ret) { // 成功执行
...
$("#result_table").html(ret['ptable']);
initTable($("#ptable")) // 为id为ptable的DOM表格初始化DataTables,即上一行刚刚修改了DOM元素的那个
},
...
});
})
</script>

(注意上方代码块中的连续两行的两个’ptable’是完全不同的,第一行的’ptable’是视图端传来的context字典的键值,而第二行的’ptable’是我们刚刚设定的表的id。)

此时可以在前端测试下,查询后表格的呈现已经发生变化了,外观改变之余也多了前端分页,搜索筛选,排序等新功能:

但整体效果和各种细节依然肉眼可见的不完善,我们还有以下诉求:

  • 表格整体宽度与父元素一致且保持不变
  • 默认以第2列(销售额)由高到低排序
  • 默认前端分页设置为每页呈现25个结果
  • 所有UI的label本地化为中文显示
  • 表格内所有负数高亮为红色字体
  • 第7列(EI)是个描述增速是否跑赢大盘的指标,要求高于100的数值高亮绿色字体,低于100的数值高亮为红色字体
  • 第4列(份额)用条形图展示,增加醒目感
  • 增加一个按键一键复制表格,方便粘贴到Excel或其他地方,这是一个常用需求,用鼠标划选麻烦且容易错位。

前4个问题是DataTables的基本设置就可以搞定的,可以修改initTable方法。DataTables的基本设置已经足以实现很多实用功能了,具体请参考文档:

<script>
function initTable(table) {
table.DataTable(
{
order: [[1, "desc"]], // 初始以第2列(注意第一列索引为0)由高到低排序
pageLength: 25, // 前端分页,初始每页显示25条记录
autoWidth: false, // 不自动调整表格宽度
oLanguage: { // UI Label本地化
"sLengthMenu": "显示 _MENU_ 项结果",
"sProcessing": "处理中...",
"sZeroRecords": "没有匹配结果",
"sInfo": "显示第 _START_ 至 _END_ 条结果,共 _TOTAL_ 条",
"sInfoEmpty": "没有数据",
"sInfoFiltered": "(获取 _MAX_ 条客户档案)",
"sInfoPostFix": "",
"sSearch": "搜索:",
"sUrl": "",
"sEmptyTable": "表中数据为空",
"sLoadingRecords": "载入中...",
"sInfoThousands": ",",
"oPaginate": {
"sFirst": "首页",
"sPrevious": "上页",
"sNext": "下页",
"sLast": "末页"
},
},
}
);
}
</script>

5,6是个常见的条件格式问题,要用设置里的columnDefs参数解决,具体使用时主要涉及target和createdCell两个参数

<script>
function initTable(table) {
table.DataTable(
{
...
columnDefs: [
...
{
"targets": 6, // 指定第7列EI
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData < 100) {
$(td).css('color', 'red')
} else if (cellData > 100) {
$(td).css('color', 'green')
} else if (cellData.indexOf(",") !== -1) {
$(td).css('color', 'green')
}
}
},
{
"targets": [2, 4, 5], // 指定第3,5,6列绝对值变化,份额获取,增长率,这些有可能出现负数
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData.startsWith('-')) { // 因为涉及到百分数的问题,这里用检查字符串的方法而不是<0的方法判断负数
$(td).css('color', 'red')
}
}
},
]
}
);
}
</script>

把份额列以条形图展示的方法来自一个叫percentageBars的插件,插件的初始化也是在columnDef参数里进行的:

<script>
function initTable(table) {
table.DataTable(
{
...
columnDefs: [
{"width": "10%", "targets": 3}, // 保持第4列份额列宽度固定,使条形图更美观
{
targets: 3,
render: $.fn.dataTable.render.percentBar('square', '#000', '#BCBCBC', '#00bfff', '#E6E6E6', 1, 'ridge') // 根据一定的色彩方案初始化条形图
},
...
]
}
);
}
</script>

一键复制表格的功能我没有找到DataTables的内置方法,在display.html自行编写js配合前端按钮解决,注意html部分的ui top attached button和下面两个新的js:

...
<div class="ui tab segment" data-tab="competition">
<h3 class="ui header">
<div class="content">
最新横断面KPI一览
<div class="sub header">数据表格</div>
</div>
</h3>
<div class="ui divider"></div>
<div class="ui top attached button" tabindex="0"
onclick="selectElementContents( document.getElementById('ptable') );"
data-content="复制成功" data-position="bottom center">
<i class="copy icon"></i>
复制到剪贴板
</div>
<div class="ui hidden divider"></div>
<div class="ui container" id='result_table' style="width: 100%; overflow-x: scroll; overflow-y: hidden;">
<!-- Django渲染html代码时需要加入|safe,保证html不会被自动转义 -->
{&#123; ptable|safe &#125;}
</div>
</div>
...

<script>
// 复制有node结构的文本区域
function selectElementContents(el) {
var body = document.body, range, sel;
if (document.createRange && window.getSelection) {
range = document.createRange();
sel = window.getSelection();
sel.removeAllRanges();
try {
range.selectNodeContents(el);
sel.addRange(range);
} catch (e) {
range.selectNode(el);
sel.addRange(range);
}
} else if (body.createTextRange) {
range = body.createTextRange();
range.moveToElementText(el);
range.select();
}
document.execCommand("Copy");
}
</script>

<script>
// 按钮点击后有弹出文本,显示data-content内容“复制成功”
$('.ui.top.attached.button')
.popup({
on: 'click'
})
;
</script>

一个用户体验非常完善的表格就完成了: