經過昨天刻苦的學完 JavaScript 之後,今天我們總算可以來畫圖囉!還記得為什麼我們想要畫圖嗎?簡潔有力的說法就是:沒圖沒真相。學術性且專業的說法則是,因為我們需要在面對大量資料的時候,也能夠快速且有效率的掌握資料當中的資訊。比如說,哪隻草泥馬的訓練量是最大的?或是我安排的訓練菜單各種訓練之間的配比是否合理等等。到目前為止,我們可以自由的調閱所有紀錄下來的訓練資料,然而呈現在網站上時,卻只能看到一筆一筆未經處理過的原始文字資料。這些資料當然也很可貴,但是要回答像上面提到的那些問題,我們還需要一些其他東西。而資料視覺化正是恰當的工具。
目前可以在瀏覽器當中藉由 JavaScript 執行的資料視覺化工具有相當多種,最有名的大概是 D3➀。這次鐵人賽當中,也有一名參賽者的題目是在介紹 D3➁。很好,很顯然我們時間不夠了,一個花了 30 天才能介紹的淋漓盡致的工具,對於才剛從 JavaScript 速成班畢業的我們來說,只是可望而不可及的工具。
當然,D3 是最負盛名且自由度最大的畫圖工具庫了,它等於是提供了一個平台讓我們可以從頭開始打造自己獨一無二的圖表。如果大家希望能對資料視覺化的圖表,包括圖表的種類、圖表的格式、或是圖表呈現時的動畫,有更多自由揮灑的空間的話,不妨好好研究學習一下➂。
但遺憾的是,我們時間真的不夠了,比賽就要結束,難道要放棄了嗎?
幸好還有 C3➃。
C3 是基於 D3 提供的資源繼續開發出來的簡易型的圖表工具。好心的開發者們已經幫我們將最困難的「製作圖表」給搞定了,重點是,這個套件居然還是 MIT License,可以讓大家胡亂取用,還允許胡亂用在各種場合,我們只需要給定幾個關鍵字,諸如圖表類型、圖表資料、坐標軸格式等等,C3 就能夠幫我們畫出精美的互動式圖表,太神啦!
另外說一下,類似的圖表套件還有很多,如簡易的 Chart.js➅,還有 Morris.js➆,還有 AntV➇,等等,我這邊選 C3 是因為它提供的圖表類型多了一點,而且剛好符合我們的需求。如果有興趣,也不妨上網找找其他看得順眼、美觀、且符合大家需求的圖表套件來試試。
好啦,不囉嗦,開始畫圖囉!
首先第一件事,因為 C3 是開發者幫我們先定義好如何作圖的一個套件,所以我們需要根據 C3 的格式,輸入相對應的資料,這樣套件才看得懂,資料進得去,圖表出得來,草泥馬發大財。
var chart = c3.generate({
bindto: '#chart',
data: { columns: [['data1', 30, 200, 100, 400, 150, 250],
['data2', 50, 20, 10, 40, 15, 25]],
type: 'donut'},
donut: {title: 'Title'}
});
很簡單,一開始看起來眼花撩亂沒關係,我慢慢說。一開始用c3.generate()
這個函數產生圖表。c3.generate()
則吃進一個物件,物件的鑰匙分別是bindto
、data
、跟donut
。
bindto
:bindto: '#chart'
意指這個函數產生的圖表會出現在<div id="chart"></div>
這個位置。data
:columns
跟type
。columns
就是我們放入數據的地方,等等再來研究。而type
則是我們指定圖表類型的方式。這邊我們用type: 'donut'
告訴 C3 我們想畫一張甜甜圈圖。donut
:donut
就是在詳細調整甜甜圈圖其他參數的位置。好啦,不困難吧?大家手癢想先看個結果的話,我們可以找 codepen 來幫忙。記得要先引入一些 CSS 跟 JavaScript:
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.11/c3.min.css">
</head>
<body>
<div id="chart"></div>
<!-- 先擺上 jQuery -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<!-- 因為是從 D3 發展而來,所以要擺 D3 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.12.0/d3.min.js"></script>
<!-- 最後擺上 C3 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.11/c3.min.js"></script>
</body>
圖一、只要輸入的資料得當,我們就可以得到一個這樣的甜甜圈
現在回到我們寫 Python 程式碼的地方。我們要新增一個路由("/donut_chart"
),讓使用者可以透過請求這個路由而拿到甜甜圈。先說我們可以怎麼寫:
@app.route("/donut_chart")
@login_required
def donut_chart():
table =web_select_overall()
table =total_seconds(table) # 等等要開始寫的函數
uniques =get_unique(table)
return render_template("donut_chart.html", table=table, uniques=uniques)
看起來也不困難吧?
table =web_select_overall()
web_select_overall()
來從 Heroku Postgres 資料庫中抓取所有草泥馬的訓練紀錄。還記得每一筆訓練紀錄是怎麼樣嗎?(record_no, alpaca_name, training, duration, date)
web_select_overall()
會回傳一份清單(List),清單內有許多筆像上面那樣的不可變更清單(tuple)。record_no
是訓練紀錄的編號,alpaca_name
是草泥馬的名字,字串物件。training
是訓練名稱,字串物件。duration
是訓練的長度,是屬於datetime.timedelta
的物件。date
則是訓練的日期,datetime.date
物件。
table =total_seconds(table)
datetime.timedelta
和datetime.date
這兩種物件,因此我們要先在 Python 當中做處裡。這就是total_seconds()
這個函數的目的。total_seconds()
:def total_seconds(table):
new_table = []
for i in table:
new_table.append((i[0], i[1], i[2], int(i[3].total_seconds()), str(i[4])))
return new_table
這就更簡單了,我只是把每一筆資料(record_no, alpaca_name, training, duration, date)
當中的duration
用datetime
提供的函數total_seconds()
換成相對應的秒數,然後再轉換為整數物件(int()
)。date
用輕輕鬆鬆地用str()
轉換為字串物件。
第六行: uniques =get_unique(table)
前幾天說過的,利用get_unique()
找出在資料庫中所有出現過的alpaca_name
、training
、date
。
第七行: return render_template("donut_chart.html", table=table, uniques=uniques)
將該處理的資料處理好了以後,我們就可以將資料從 Python 送到 HTML 5 當中囉!
好啦,現在 Python 送來了跟前幾天相差無幾的文字資料,我們接著就將這些資料轉換成 C3 可以利用的數據並好好做個圖吧!
{% block script %}
<script>
// 將 Python 資料轉換為 JavaScript 可以操作的資料
const table = {{ table|tojson }};
const uniqueAlpacaName = {{ uniques[0]|tojson }};
const uniqueDate = {{ uniques[2]|tojson }};
// 做出符合 C3 格式的資料
let alpacaNameDaily = []
uniqueAlpacaName.forEach(x => alpacaNameDaily.push([x].concat(uniqueDate.map(x => 0))))
let alpacaNameIndex = alpacaNameDaily.map(x => x[0])
table.forEach(function (x) {
tempDate = uniqueDate.indexOf(x[4])+1;
tempName = alpacaNameIndex.indexOf(x[1]);
alpacaNameDaily[tempName][tempDate] += x[3];
})
// 用 C3 畫圖
var alpacaNameChart = c3.generate({
bindto: '#alpacaNameDonut',
data: {columns: alpacaNameDaily,
type: 'donut'},
donut: {title: 'alpacaNameDonut'}
});
</script>
{% endblock %}
上面那些程式碼必須要放在 HTML 5 檔案中<script>
的標籤內。還記得我們有在base.html
裡增加一個段落{% block script %}
嗎?放在裡面,再加上<script>
的標籤就行了,這樣 HTML 5 就會知道這個段落是要用 JavaScript 來執行的。好,接下來一點一點來解釋這些程式碼分別做了些什麼吧!
第一段:將 Python 資料轉換為 JavaScript 可以操作的資料
上面有提過,JavaScript 不認識 Python 的資料,所以需要藉由轉換器來處理。不困難,強大的 Jinja2 已經幫我們設身處地的想過了。只要用{{ Python 的資料|tojson }}
就可搞定。
第二段:做出符合 C3 格式的資料
因為我們用的畫圖套件是 C3,因此必須依照 C3 的要求放入資料。到底什麼是符合 C3 格式的資料呢?讓我們再回頭看看 C3 範例中data
的部分:
data: { columns: [['data1', 30, 200, 100, 400, 150, 250],
['data2', 50, 20, 10, 40, 15, 25]],
type: 'donut'}
最後一行type: 'donut'
上面已經解釋過了,現在就專心在columns
上。columns
後面放入的是一個序列,序列中的項目(['data1', 30, 200, 100, 400, 150, 250]
)代表甜甜圈圖的一個切片,所以如果我繼續在序列中增加新的項目,如圖二,甜甜圈圖就會多一個切片。
圖二、甜甜圈切片數量增加了
接著,序列中的項目又是一個序列,序列的第一個項目(data1
)是個字串,代表每一個切片的名稱,而後面的數值加總,就是甜甜圈圖切片大小的依據。在甜甜圈圖裡,數值的數量似乎看不出差別,不過記得,序列的長度要相同,舉例來說:
data: { columns: [['data1', 30, 200, 100, 400, 150, 250], // 共 6 個數值
['data2', 50, 20, 10, 40, 15, 25], // 共 6 個數值
['新的項目', 10, 20, 30, 40, 50, 60]] // 共 6 個數值
type: 'donut'}
上面那樣是可以的,
data: { columns: [['data1', 30], // 共 1 個數值
['data2', 50], // 共 1 個數值
['新的項目', 10]] // 共 1 個數值
type: 'donut'}
上面那個也可以。但是下面這樣,代表資料的序列長度不相同,就不行了:
data: { columns: [['data1', 30, 50, 45, 20, 75, 30], // 共 6 個數值
['data2', 50, 40, 30, 65, 35, 50], // 共 6 個數值
['新的項目', 10]] // 卻只有 1 個數值,失敗
type: 'donut'}
話說如此,改變序列的長度好像看不出差異,也沒多增加幾個甜甜圈,如圖三。既然是用數值的加總,來決定甜甜圈切片的大小,那為什麼我還要給這麼多呢?因為之後會用到。
圖三、檢查序列內數字的多寡對甜甜圈圖的影響
好啦,既然已經了解 C3 輸入資料的方式了,那就來說說我想要怎麼樣的資料吧。我想要拿到像下面這樣:
alpacaNameDaily = [['吉姆', 第一天訓練總時間, 第二天訓練總時間, 第三天訓練總時間, 以下略],
['大衛', 第一天訓練總時間, 第二天訓練總時間, 第三天訓練總時間, 以下略],
['威廉', 第一天訓練總時間, 第二天訓練總時間, 第三天訓練總時間, 以下略],
以下略]
把上面那個序列alpacaNameDaily
送進columns
,C3 就會幫我們畫出代表草泥馬們訓練時間比例的甜甜圈圖了。所以接著,我用了一連串 JavaScript 的操作,做出了我想要的alpacaNameDaily
。JavaScript 程式碼用的都是昨天速成班提過的函數,大家可以試著讀讀看。
columns
,C3 就會幫我們畫好圖啦!記得要在base.html
加入相關的 CSS 跟 JavaScript 喔:<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.11/c3.min.css">
</head>
<body>
<div id="chart"></div>
<!-- 先擺上 jQuery -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<!-- 因為是從 D3 發展而來,所以要擺 D3 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.12.0/d3.min.js"></script>
<!-- 最後擺上 C3 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.11/c3.min.js"></script>
</body>
圖四、C3 送來的精美甜甜圈
大功告成,得到我們二十幾天來的第一張圖表!而且還是互動式的喔,把滑鼠移到上面就可以看到更仔細的資料說明。跟大家說個抱歉一下,今天寫稿也是寫得很趕。如果我的內容有哪些寫得不太清楚,或是大家在嘗試實作上出了哪些問題,歡迎在文章下面留言,我會再對文章內容作補充的。相關的程式碼我會放到 Github 上,若有興趣想研究一下也很歡迎過去參考。謝謝大家!
➀ D3官方網站
➁ 資料視覺化!D3入門到實戰
➂ FreeCodeCamp D3 教學影片
➃ C3 官方網站
➄ MIT License wiki
➅ Chart.js 官方網站
➆ Morris.js Github
➇ AntV 官方網站
註:對於此系列文有興趣的讀者,歡迎參考由此系列文擴編成書的 LINE Bot by Python,以及最新的系列文《賴田捕手:追加篇》
第 31 天 初始化 LINE BOT on Heroku
第 32 天 快速回覆 QuickReply 介紹
第 33 天 妥善運用 Heroku APP 暫存空間
第 34 天 妥善運用 LINE Notify 免費推播
第 35 天 製造 Deploy to Heroku 按鈕