Chart.js 自學分享

林芷蕾Alicia
11 min readJul 29, 2021

--

緣起

在Alpha Camp實作老爸的私房錢專案時,想要將支出/收入的紀錄用資料視覺化的方式呈現。

最後花了近一天的時間解決。

不知道?那就海量Google

因為不知道有什麼方式能實現資料視覺化,但這是一個很熱門的話題,所以絕對不可能是每個人都手刻資料視覺化功能,一定有寫好的套件可以用,於是我開始大量Google類似「Mongodb Data visualization」、或是用已知商業工具名稱+API來Google 像是PowerBI。

這個階段可以說是最頭痛的時候,資訊會爆炸..

搜尋結果:

PowerBI、Mongodb chart、3D、antV G2、chart.js..甚至一些付費的軟體都能實現資料視覺化。

這麼多工具要選哪個?

我是先從他的文件先看起來,並動手嘗試看看,因為時間有限,我選擇先挑相對簡單且使用者體驗好的套件,最後就是使用對新手友善的chart.js。

嘗試使用新套件

一開始我先看看有沒有Youtube教學,這樣入門成本比較低,可以先快速了解操作方式,於是我找到這兩部優質教學:

(40) Getting Started With Chart.js — YouTube

(40) ChartJS Tutorials #6 — Writing A Bar Chart From Scratch — YouTube

看完以後,自己嘗試動手做看看,但是發現還是會有點卡卡,所以就開始搭配Chart.js文件來實作,順便深入瞭解其他功能。

雖然我是希望用mongodb的資料來動態畫圖表,不過這是相對進階的功能,如果要達到這件事,勢必要先熟悉套件,知道怎麼畫圖表、相關的設定怎麼做,於是我選擇先乖乖學怎麼畫靜態資料做的圖表。

先從簡單的開始,熟悉套件

Getting Started | Chart.js (chartjs.org)

根據以上文件顯示,我可以用CDN的方式引入,創建圖表容器只要簡單放個canvas容器就好,畫圖功能則是用JS實現。

引入Chart.js,像引入CSS一樣。(放在head)

<script src=”https://cdn.jsdelivr.net/npm/chart.js"></script>

創建容器(畫好的圖表會被插入在裡面):

<div>
<canvas id="myChart"></canvas>
</div>

準備開始畫圖表

先抓住你要插入圖表的容器

const 要放在哪個容器 = document.getElementById('myChart').getContext('2d')

這邊要特別注意getContext語法是canvas tag專屬的, 一開始我跟著chart.js文件走,發現出錯(他沒加上這句),後來查找後來發現是因為要先告訴他,這是一個二維圖表(2d),讓jS能藉由getContext()取得渲染環境。

抓住了插入圖表的地方後,開始畫圖吧

new Chart這個方法是chart.js寫好的,不是js原生的語法,如果你沒有引入CDN就會無法使用!

let chart = new Chart(要放在哪個容器, {
type: 'pie', //圖表類型
data, //設定圖表資料
options: {} //圖表的一些其他設定,像是hover時外匡加粗
})

基本上Chart.js的使用就是圍繞在這三大主題,type、data、options,後兩者會有一個物件包住,裡面會有很多不同的key給你做設定,可以直接看文件。

Doughnut and Pie Charts | Chart.js (chartjs.org)

我是直接看文件範例+影片,先選定自己要畫的圖表類型,然後直接去該圖表類型的範例看程式碼,先觀察後實驗自己把玩,在此選定圓餅圖:

data = {
labels: [ //圓餅圖的每一塊,分別叫做什麼名字
'台北', //第一塊名字
'台中', //第二塊名字
'高雄'
],
datasets: [{
label: '三都人口', //這些資料都是在講什麼,也就是data 300 500 100是什麼
data: [300, 50, 100], //每一塊的資料分別是什麼,台北:300、台中:50..
backgroundColor: [ //設定每一塊的顏色,可以用rgba來寫
'rgb(255, 99, 132)',
'rgb(54, 162, 235)',
'rgb(255, 205, 86)'
],
hoverOffset: 4
}]
};

資料設定好了以後,就可以開始做一些額外的設定,也就是config,可以直接看文件Configuration的部分。

實作到這邊,其實基本上都已經沒問題了,可以說是初步掌握了如何畫圖表這件事,接下來就是頭痛的地方了,怎麼讓這些資料不是寫死的..

再次海量Google

這次是針對Chart.js跟資料庫連線的關鍵字來搜尋,像是Chart.js data from mongodb,但是這邊比較沒有收穫,所以我後來直接手刻OAO

開始手刻

成果

先想如何實作,在開始寫程式碼

主要要更動的是chart.js data的部分,有data陣列(收入/支出)、labels(每一塊叫什麼,在此為類別)

基本思想:把資料從路由丟到handlebars,在用JS把資料拿來用。

出手後遇到的問題:

一直出錯,不斷Debug、嘗試,還懷疑是不是要多做字串跟陣列的轉換..而瘋狂Google看一堆轉換方式,開始爆炸XD

後來發現是因爲再傳資料到hbs時,要先用JSON把它轉成文字在傳入,才不會造成錯誤,而在JS操作資料時,需要再用JSON把它變成js可操作的類型。

利用type判斷是收入還是支出,並將類別資料(放labels)和金額資料(放data)丟進去

res.render(‘chart’, { type, recordCategoryList: JSON.stringify(recordCategoryList), recordAmountList: JSON.stringify(recordAmountList) }

資料丟到hbs之後,因為資料沒有要顯示給使用者,只是要取用,所以放資料的容器要display none,處理完之後就來弄JS。

存放資料:

{{! — 為了讓js可以拿到資料給chart.js,所以放這,在用DOM取得值 — }}
<div id=”recordCategoryList” class=”d-none”>{{ recordCategoryList }}</div>
<div id=”recordAmountList” class=”d-none”>{{ recordAmountList }}</div>

取用資料:

//獲取圖表所需資料,來自mongodb let recordCategoryList = document.getElementById(‘recordCategoryList’).innerText let recordAmountList = document.getElementById(‘recordAmountList’).innerText

接下來需要把資料轉為JS可操作的格式,現在只是看起來像是陣列的字串而已。

//將像是陣列的字串轉成js可以操作的格式(為了有效傳入陣列,在路由處理時將其轉為字串,現在轉回來)recordCategoryList = JSON.parse(recordCategoryList)
recordAmountList = JSON.parse(recordAmountList)

更改chart.js data,變成來自資料庫

data: { 
labels: recordCategoryList,
datasets: [{
label: ‘金額’,
data: recordAmountList,
backgroundColor: [getRgba(), getRgba(), getRgba(), getRgba(), getRgba(), getRgba(), getRgba(), getRgba(), getRgba(), getRgba()], hoverBorderWidth: 3,
hoverBorderColor: ‘#000’,
}] },

此時雖然資料來自資料庫了,但又遇到一個bug,就是類別相同的會重複出現,例如餐飲支出有兩筆:200元,300元,他在圖表上卻不會合併成500元。

因為想不出來就開始Google看看有無人碰到一樣的問題,後來找到了這個方法:

javascript — Is it possible to merge duplicate labels (and their datas) into one column in Chart Js? — Stack Overflow

//將重複的類別其值合併 
let obj = recordCategoryList.reduce(function (a, b, i) {
//處理類別(key)
//檢查累加器是否含有該類別資料,如果沒有:
if (!a.hasOwnProperty(b)) {
a[b] = 0; 把這個類別加進來累加器,也就是把a物件中新增b key,且value = 0(value到下一行處理,對照到金額列表)
}
//處理金額(value)
a[b] += Number(recordAmountList[i]); //累加器中的該類別(key),將他的value加上對照到的該個陣列金額
return a; //返回a物件累加器
}, {});
//重新復值
recordCategoryList = Object.keys(obj); //處理好後的類別
recordAmountList = Object.values(obj); //處理好後的類別金額

雖然成功了,但是這行程式碼還是看不太懂,所以分別去Google看不懂的語法先了解他。(註解是理解之後的自己對每行程式碼在幹嘛的詮釋)

— 例子,可跳過 —

第一次迭代,處理類別以後長這樣:(累加器)

{休閒娛樂:0}

用這個實現的:a[b] = 0;

第一次迭代,處理類別的金額value:

{休閒娛樂:-799}

用這個實現的:a[b] += Number(recordAmountList[i])

最後的obj可能長這樣:

{休閒娛樂:-799, 餐廳飲品:-170, 交通:-474}

最後的Object.keys(obj)可能長這樣:

[休閒娛樂,餐廳飲品,交通]

最後的Object.value(obj)可能長這樣:

[-799, -170, -474]

— 例子,可跳過 —

recordCategoryList資料結構
陣列,每個陣列元素為類別名稱 ['休閒娛樂','餐廳飲品','交通出行','交通出行']
recordAmountList資料結構
陣列,每個陣列元素為金額
互相對照,像是recordCategoryList[0] 對照到recordAmountList[0]
Array.prototype.reduce()

Array.prototype.reduce() — JavaScript | MDN (mozilla.org)

語法:

arr.reduce(callback[accumulator, currentValue, currentIndex, array], initialValue)

他會迭代陣列,把每個每個陣列元素丟進一個call back function,經過這個函式之後,把陣列變成單一的值。

在此的i為當前索引,a為累加器,累加器預設為第一個元素,可以在第四個call back function做設定,在此有設定成一個空物件{}作為累加器。

看來就是會把每一次執行的結果丟到累加器裡面,b是陣列當前所迭代到的元素。

obj.hasOwnProperty(prop)

Object.prototype.hasOwnProperty() — JavaScript | MDN (mozilla.org)

回傳布林值,如果a物件存在該屬性(prop),回傳true。

如此一來功能就實現了,現在可以動態根據mongodb的資料來畫圖了!

回顧

整個最關鍵的大概就是耐心XD 有了耐心以後去Google(發散),找到以後去嘗試(收斂),最後學習如何使用(發散),閱讀他們(收斂),並且去嘗試動手做,卡關,就去找看看別人的解決方法,並試著了解,但整個過程必須根據時間成本決定要去了解的程度。

像是我只有一天的時間完成這個功能。

助教回饋:

先精通教案內容,在學延伸功能->十分贊同QAQ謝謝助教

因為對該套件的不深入,最後才會用奇怪的方式處理chart.js data from mongodb (希望有天我會搞懂OWO)

附上github:JHIH-LEI/expense-tracker (github.com)

--

--

林芷蕾Alicia
林芷蕾Alicia

Written by 林芷蕾Alicia

Junior Backend developer in IOT company , love to share.

Responses (1)