iT邦幫忙

3

數據可視化:Django + pandas + ECharts + 前後端分離

  • 分享至 

  • xImage
  •  

做數據分析時,我們可能會很常使用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

https://ithelp.ithome.com.tw/upload/images/20230907/20161866pD2Myk5n9c.png
架構圖如上~

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全部放上來,篇幅有點太長了
所以這次就先以折線圖來說~

折線圖

https://ithelp.ithome.com.tw/upload/images/20230907/20161866kiWPp91lRT.png
這是官方示例的圖
接下來我們可以想一下,哪些部分是需要用到我們自己資料的

  • 標題以及圖例的名稱
  • x軸的值以及y軸的值還有各自的名稱
  • 最重要的就是我們的資料啦
    這些都是等等我們需要注意的地方

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 randrange
import pandas as pd

def 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_series


def trans_df_to_list(series):
    """因為echart的data需要接收list 所以需要轉格式"""
    if isinstance(series, pd.Series):
        return series.values[0]
    else:
        return


def create_data(*args):
    res = [i for i in args]
    return res


def 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位學生他們各自的分數
這邊可能要注意幾個點:

  • 因為最後echarts那邊接收的資料形式是array,所以這邊做好的資料要轉換格式
  • 並且因為是array,如果資料量大的話,盡量避免for操作
    • 我這邊是直接示範資料,所以用randrange。不然我會直接用read_csv之類的方式再去選取我要的series
    • create_data方法單純是把那兩條線的資料包起來所以沒差

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的部分便可以直接去操作測試

最後的成果如下:
https://ithelp.ithome.com.tw/upload/images/20230907/20161866PCPz7gV16Y.png
甚至你可以點擊右上方的一些按鈕,會有很多不錯的特效
例如轉換成柱狀圖等等
https://ithelp.ithome.com.tw/upload/images/20230907/20161866uZoH8BipFu.png

但是這樣還不夠~
我們此時去更改視窗寬度,會發現圖表根本就沒有變化
而echarts有resize方法,可以讓圖表隨著視窗改變而改變
https://echarts.apache.org/zh/api.html#echartsInstance.resize
https://ithelp.ithome.com.tw/upload/images/20230907/20161866D2cNkyYXXu.png

那一般我們沒有特別要求的話可以直接這樣調用

// 視窗調整時會更改echart圖表
window.onresize = function () {
    	myLine.resize()
};

這邊額外說一下,如果我們使用下圖這種圓餅圖
https://ithelp.ithome.com.tw/upload/images/20230907/20161866PiT7Cm42fA.png
我們縮小的時候,會更希望由左右兩邊變成上下並行
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來建立DOM
    • echarts用DOM來實例化
  • $(function(){})等DOM載入後執行ajax
  • ajax拿到資料後
    • 製作option變量
    • echarts對象使用setOption方法拿option渲染圖表
  • 頁面監聽resize,並且echarts對象隨之調用resize方法改變大小

但是現在要修改成:

  • 建立初始變量
    • 獲取id來建立DOM
    • echarts用DOM來實例化
    • 建立一個空變量rowData
    • 建立一個fontMedia方法是根據當下視窗大小返回特定字體大小
  • $(function(){})等DOM載入後執行ajax
  • ajax拿到資料後
    • 製作option變量
      • 製作中會調用fontMedia來製作font-size
    • echarts對象使用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();
};

最後重整後,就可以發現我們的標題可以隨著視窗寬度變化而改變大小
https://ithelp.ithome.com.tw/upload/images/20230907/20161866SBb2O8lDYv.pnghttps://ithelp.ithome.com.tw/upload/images/20230907/20161866liQEjLz5tI.pnghttps://ithelp.ithome.com.tw/upload/images/20230907/20161866DElbYHxSYM.png

有點懶得做gif所以直接丟圖XD
其他調整font-size就大同小異,就不示範了~
希望有幫助到想做圖的人~


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言