做数据分析时,我们可能会很常使用jupyter notebook来操作
并且可能依照个人喜好来使用像是seaborn、matplotlib等工具把资料视觉化
但是如果是要呈现在网页上的话,可能就要找JavaScript可视化库会更有弹性
因此我选择ECharts来作为这次的工具
Apache ECharts
https://echarts.apache.org/en/index.html
Apache ECharts的前身是百度的Echarts,在经过Apache Incubator孵化完成后变成Apache软体基金会的顶级专案。ECharts是一个使用JavaScript实现的视觉化图表库,可以在PC与其他装置上使用,且具有以下特点
丰富的图表类型:
https://echarts.apache.org/examples/zh/index.html
除了一般常见的折线图、柱状图、圆饼图、散布图等等,还有包含k线图、地理座标图等等,并且也有许多酷炫的动画呈现
方便:
Echarts内置的dataset属性支持直接传入array、key-value等多种格式的数据类型,省去很多时候数据还需要转换的步骤
主题设计系统:
在示例中找到喜欢的图点进去,就可以直接看每个图应该要怎么生成,并且也可以透过程式码编辑马上看到更改后的成果,非常强大与方便
我这边就不特别介绍Django跟pandas的用法,今天要写的语法都是很基础的,所以也会直接掉过架设环境的部分
网路上有很多资源马上找就有了。另外js的部分因为我对于js了解还很浅,觉得污染眼睛的话感到抱歉XDD
架构图如上~
HTML:
首先在我们的页面中使用CDN导入ECharts服务,当然你也可以安装到你的专案资料夹,只是我觉得使用CDN比较方便,并且也不用管自己的网页有没有使用CDN服务或是做静态资源的cache。但是直接在网页使用第三方CDN还是有安全问题,所以自己评估https://echarts.apache.org/handbook/zh/get-started/
这里面有教学,可以自己参考然后做一个DOM元素,这边要设定好大小,或是你要在js再调整也没关係
Javascript:
首先要使用echart.init来初始化一个echart对象,接着指定好图表的内容(option变量)option里面需要配置好图表需要的一些配置项,以及资料资料透过ajax来呼叫Django的API,并且取得后端的资料使用stOption方法,让echart使用已经设定好的option,使html中的DOM元素渲染出图表样式Django:
urls.py设置好路由,配置要连结的视图函式(views.py)views.py中使用pandas处理要呈现的资料,并返回Jsonresponse以上就是大致的流程,其实每个图都大同小异,只要会了其中一种剩下的也不会到太困难
动态的那些图或是需要第三方计算回归线那些,我不确定做起来后用pagespeed分析后会不会分数很惨XD
所以我自己在使用上应该还是简单为主,另外我发现如果把几种基本图的code全部放上来,篇幅有点太长了
所以这次就先以折线图来说~
折线图
这是官方示例的图
接下来我们可以想一下,哪些部分是需要用到我们自己资料的
这些都是等等我们需要注意的地方
HTML:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>测试echart</title> <script src="https://cdn.bootcss.com/jquery/3.0.0/jquery.min.js"></script> <!--引入ECharts CDN--> <script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.3/echarts.min.js"></script></head><body><!--设置DOM元素 并设置大小--><div id="ecahrtLine" style="width: 100%; height:50vh;"></div><!--js档--><script src="/static/js/echart_theme/line.js"></script></body></html>
这边就是引入CDN跟设置元素,我自己是还有再额外引入jqery,因为我在js有用到相关语法
Django-views.py:
from random import randrangeimport pandas as pddef create_line_data(): math_score = { "math_score": [randrange(50, 90) for _ in range(6)] } english_score = { "english_score": [randrange(60, 100) for _ in range(6)] } m_series = pd.Series(math_score) e_series = pd.Series(english_score) return m_series, e_seriesdef trans_df_to_list(series): """因为echart的data需要接收list 所以需要转格式""" if isinstance(series, pd.Series): return series.values[0] else: returndef create_data(*args): res = [i for i in args] return resdef show_line(request): m_series, e_series = create_line_data() m_list = trans_df_to_list(m_series) e_list = trans_df_to_list(e_series) data = { "code": 200, "msg": "success", "data": create_data(m_list, e_list), } return JsonResponse(data)
首先我这边想要呈现的是两个科目中,这一个班级的6位学生他们各自的分数
这边可能要注意几个点:
Javascript:
var lineDom = document.getElementById('ecahrtLine');var myLine = echarts.init(lineDom);$( function () { fetchData(myLine); });function fetchData() { $.ajax({ url: "/article/show_line", type: "GET", dataType: "json", success: function (result) { var option = createOption(result.data); myLine.setOption(option); } });};function createOption (backendData) { // 做出x轴的列表 var indexList = backendData[0].map(function(_, index) { return index + 1; }); var option; option = { title: { text: '数学与英文成绩' }, tooltip: { trigger: 'axis' }, legend: {}, toolbox: { show: true, feature: { dataZoom: { yAxisIndex: "none" }, dataView: { readOnly: false }, magicType: { type: ['line', 'bar'] }, restore: {}, saveAsImage: {} } }, xAxis: { type: 'category', boundaryGap: false, data: indexList, name: "学生编号" // 设置名称 }, yAxis: { type: 'value', min: "dataMin", // 设置y轴最小值 axisLabel: { formatter: '{value} 分' }, name: "成绩" // 设置名称 }, series: [ { name: '数学成绩', type: 'line', data: backendData[0], // 我们的资料 markPoint: { data: [ { type: 'max', name: 'Max' }, { type: 'min', name: 'Min' } ] }, markLine: { data: [{ type: 'average', name: 'Avg' }] } }, { name: '英文成绩', type: 'line', data: backendData[1], // 我们的资料 markPoint: { data: [ { type: 'max', name: 'Max' }, { type: 'min', name: 'Min' } ] }, markLine: { data: [ { type: 'average', name: 'Avg' }, [ { symbol: 'none', x: '90%', yAxis: 'max' }, { symbol: 'circle', label: { position: 'start', formatter: 'Max' }, type: 'max', name: '最高点' } ] ] } } ] }; return option}
因为我没有特别需要转换太多x轴的形式,所以我直接用索引来改编我的x轴,今天如果是x轴的资料格式比较特别,或是点超级多,建议在django那边解决掉
其中在设置一些参数的API,我自己有改的部分有加上注解
如果还是看不懂,可以参考:
https://echarts.apache.org/zh/option.html#xAxis.name
这个官方文档已经算是非常详细的解释API了,并且可以点“试一试”,再点code的部分便可以直接去操作测试
最后的成果如下:
甚至你可以点击右上方的一些按钮,会有很多不错的特效
例如转换成柱状图等等
但是这样还不够~
我们此时去更改视窗宽度,会发现图表根本就没有变化
而echarts有resize方法,可以让图表随着视窗改变而改变
https://echarts.apache.org/zh/api.html#echartsInstance.resize
那一般我们没有特别要求的话可以直接这样调用
// 视窗调整时会更改echart图表window.onresize = function () { myLine.resize()};
这边额外说一下,如果我们使用下图这种圆饼图
我们缩小的时候,会更希望由左右两边变成上下并行
echarts还有类似css中media的设置方法,可以参考官方文档:
https://echarts.apache.org/zh/tutorial.html#%E7%A7%BB%E5%8A%A8%E7%AB%AF%E8%87%AA%E9%80%82%E5%BA%94
好~ 我们回到我们的折线图,我们的确可以让图片的宽度自适应,但是文字不会因为图片变小而让佔比放大
这样对于手机或是平板的使用者会非常痛苦
所以我们需要修改原本的方法,让font-size也能够自适应
参考:https://blog.csdn.net/jingjing217/article/details/114015832
原本的js逻辑顺序如下
建立初始变量获取id来建立DOMecharts用DOM来实例化$(function(){})等DOM载入后执行ajaxajax拿到资料后製作option变量echarts对象使用setOption方法拿option渲染图表页面监听resize,并且echarts对象随之调用resize方法改变大小但是现在要修改成:
建立初始变量获取id来建立DOMecharts用DOM来实例化建立一个空变量rowData建立一个fontMedia方法是根据当下视窗大小返回特定字体大小$(function(){})等DOM载入后执行ajaxajax拿到资料后製作option变量製作中会调用fontMedia来製作font-sizeecharts对象使用setOption方法渲染图表rowData来接后端传的资料页面监听resize重新製作option,不用再使用ajax跟后端要资料,直接拿rowData内的资料在製作过程中会调用fontMedia,达到字体大小自适应最后再调用resize方法更改图片大小修改后的js
var lineDom = document.getElementById('ecahrtLine');var myLine = echarts.init(lineDom);var rowData;$(function () { fetchData(myLine); });// 因为文字不会更改 所以要自己写方法function fontMedia(fontSizePx){ var deviceWidth = window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth; if (!deviceWidth) return; var fontSize = 150 * (deviceWidth / 1920); return fontSizePx*fontSize;}function fetchData() { $.ajax({ url: "/article/show_line", type: "GET", dataType: "json", success: function (result) { rowData = result.data; var option = createOption(result.data); myLine.setOption(option); } });};function createOption (backendData) { // 做出x轴的列表 var indexList = backendData[0].map(function(_, index) { return index + 1; }); var option; option = { title: { text: '数学与英文成绩', textStyle: { fontSize: fontMedia(0.4) // 让标题可以随之改变 } }, tooltip: { trigger: 'axis' }, legend: {}, toolbox: { show: true, feature: { dataZoom: { yAxisIndex: "none" }, dataView: { readOnly: false }, magicType: { type: ['line', 'bar'] }, restore: {}, saveAsImage: {} } }, xAxis: { type: 'category', boundaryGap: false, data: indexList, name: "学生编号" // 设置名称 }, yAxis: { type: 'value', min: "dataMin", // 设置y轴最小值 axisLabel: { formatter: '{value} 分' }, name: "成绩" // 设置名称 }, series: [ { name: '数学成绩', type: 'line', data: backendData[0], markPoint: { data: [ { type: 'max', name: 'Max' }, { type: 'min', name: 'Min' } ] }, markLine: { data: [{ type: 'average', name: 'Avg' }] } }, { name: '英文成绩', type: 'line', data: backendData[1], markPoint: { data: [ { type: 'max', name: 'Max' }, { type: 'min', name: 'Min' } ] }, markLine: { data: [ { type: 'average', name: 'Avg' }, [ { symbol: 'none', x: '90%', yAxis: 'max' }, { symbol: 'circle', label: { position: 'start', formatter: 'Max' }, type: 'max', name: '最高点' } ] ] } } ] }; return option}// 视窗调整时会更改echart图表window.onresize = function () { var option = createOption(rowData); myLine.setOption(option); myLine.resize();};
最后重整后,就可以发现我们的标题可以随着视窗宽度变化而改变大小
有点懒得做gif所以直接丢图XD
其他调整font-size就大同小异,就不示範了~
希望有帮助到想做图的人~