做數據分析時,我們可能會很常使用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">
        <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>
    <!--設置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就大同小異,就不示範了~
    希望有幫助到想做圖的人~