昨天我們利用了 LINE 聊天機器人幫忙儲存在 Heroku Postgres 的資料畫出了甜甜圈圖(donut chart)。根據這份甜甜圈圖,終於能夠快速的了解資料的特性和一些統計的結果。比如,從昨天製作的甜甜圈圖當中,我們可以看出草泥馬們在這幾天的訓練量是否平均,或是有沒有哪隻草泥馬的訓練量明顯較少的情況。此外,也可以判斷出我給的訓練菜單,不同訓練項目之間的比例是否符合我的預期。
然而,還是有一些資料是我無法輕易判斷的。從甜甜圈圖我看到了加總的結果,但是要我回答出草泥馬們每天的訓練量是否一致,或說草泥馬們是否都有切實執行某樣訓練內容,或是 9/23 之後,某隻草泥馬的訓練量是否提升了,我只能看著眼花撩亂的資料舉手投降說我辦不到。
因此,今天我們要來實作的,就是時間對訓練量的曲線圖。利用曲線圖,讓我們仔細的來比較一下草泥馬們每天訓練量的差異。
不需要懷疑,今天我們一樣要來利用昨天學到的 C3 來幫我們做曲線圖。首先來看看要讓 C3 畫出曲線圖,我們需要提供怎麼樣的資料格式:
var chart = c3.generate({
bindto: '#chart',
data: { columns: [['data1', 30, 200, 100, 400, 150, 250],
['data2', 130, 100, 140, 200, 150, 50]],
type: 'spline'}
});
有沒有很熟悉的感覺呢?沒錯,跟昨天用來畫甜甜圈圖的資料格式有 87% 像呢!唯一的差別只在data
裡頭的type
從'donut'
換成了'spline'
。好啦,我們先用 codepen (或是 codeply、JSFiddle、JSBin,選一個喜歡的)來實際的畫一畫,看看 C3 究竟會給我們怎麼樣的曲線圖。
記得要放入相對應的 CSS 和 JS 喔:
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.11/c3.min.css">
</head>
<body>
<!-- 先放 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>
圖一、這是一個簡單的小曲線圖
好的,回過頭來再討論一下columns
裡面的資料。
columns: [['data1', 30, 200, 100, 400, 150, 250],
['data2', 130, 100, 140, 200, 150, 50]]
'data1'
是第一筆資料的名稱,'data2'
是第二筆資料的名稱,接著後面分別都塞了 6 個數值。還記得我們昨天在畫甜甜圈圖時,全塞 1 個數值或是全塞 6 個數值好像看不出差別嗎?因為甜甜圈圖是把一筆資料當中所有數值做加總來代表一個切片的大小。在曲線圖就有所差別了,因為這代表一筆資料有幾個資料點。塞 6 個值就表示一筆資料有 6 的資料點,塞 16 個值就代表一筆資料有 16 個點。
那麼,知道為什麼我昨天畫甜甜圈圖時,堅持有幾天就放幾個數值在columns
裡面了嗎?雖然甜甜圈圖裡可以不需要這麼麻煩,但是這樣一來,今天我們畫曲線圖就簡單多囉!只要甜甜圈圖和曲線圖裡唯一的差別,把type
從'donut'
換成'spline'
,我們的第一張曲線圖就畫出來囉!
<script>
// 將從 Python 送來的資料換成 JavaScript 可以操作的格式
const table = {{ table|tojson }};
const uniqueAlpacaName = {{ uniques[0]|tojson }};
const uniqueTraining = {{ uniques[1]|tojson }};
const uniqueDate = {{ uniques[2]|tojson }};
// 準備符合 C3 格式的資料
let alpacaNameDaily = []
uniqueAlpacaName.forEach(x => alpacaNameDaily.push([x].concat(uniqueDate.map(x => 0))))
table.forEach(function (x) {
tempDate = uniqueDate.indexOf(x[4])+1;
tempName = uniqueAlpacaName.indexOf(x[1]);
alpacaNameDaily[tempName][tempDate] += x[3]/60; // 將單位從秒換成分
})
// 請 C3 幫我們畫圖囉!
var alpacaNameChart = c3.generate({
bindto: '#alpacaNameSpline',
data: {
columns: alpacaNameDaily,
type: 'spline'
}
});
</script>
這邊提供一個實作 JavaScript 網頁程式碼的小撇步給大家:要怎麼好好利用瀏覽器這個隱藏的集成開發環境來幫我們除錯或是更順利的編譯我們的程式碼呢?還記得我們已經把資料從 Python 送到指定的路由,並且用 Jinja2 的工具{{ |tojson}}
將所有需要的資料轉換成 JavaScript 可以操作的資料了吧?這時候,只要開啟瀏覽器,來到放有這些資料的路由(以我為例,是"/donut_chart"或"/spline_chart"),接著按下 Ctrl + Shift + i,進入瀏覽器的 console,就可以在裡面嘗試操作我們的資料啦!
讓我想想,我把所有的草泥馬訓練紀錄叫做const table = {{ table|tojson }};
,這時候在 console 輸入table
,看看發生了什麼:
圖二、讓我們實際的玩一玩table
喔喔,太好啦,總算不用再擔心寫不出程式碼或畫不出圖來啦。利用瀏覽器的 console 我們可以很快的偵錯或是嘗試改善我們的 JavaScript 程式碼。
雖然用了曲線圖,我們已經可以清楚看到'吉姆'
每天訓練量的變化了,如圖三,不過 x 坐標軸上的 1, 2, 3 這些數字到底是什麼啊?我知道這是代表不同日子的訓練量,但是 1 是哪一天,2 又是哪一天啊?能不能幫我們把 x 坐標軸從單純無意義的編號換成日期呢?
圖三、x 坐標軸是不明所以的1, 2, 3
這就要用到 C3 的時間序列圖表(timeseries chart)了。基本內容跟剛才的曲線圖差不多,我們只需要多加一個和 x 軸相關的資料就行。不囉嗦直接來看一看 C3 時間序列圖表需要的程式碼:
var chart = c3.generate({
bindto: '#chart',
data: { x: 'x', // 宣告我們要用 'x' 當作 x 軸的資料
columns: [['x', '2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04', '2013-01-05', '2013-01-06'],
['data1', 30, 200, 100, 400, 150, 250],
['data2', 130, 100, 140, 200, 150, 50]],
type: 'spline'},
axis: { x : { type: 'timeseries', tick: {format: '%Y-%m-%d'} }} // 告訴 C3 我們 x 軸的資料是 'timeseries'
});
第三行:x: 'x',
在data
當中加入另一個組鑰匙-資料,告訴 C3 我們要用'x'
當作 x 軸的資料。
第四行:['x', '2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04', '2013-01-05', '2013-01-06'],
所謂的'x'
是什麼呢?就在這裡,我們在columns
當中放入了一個'x'
開頭的序列,就用這個序列當作 x 軸。
第八行:axis: { x : { type: 'timeseries', tick: {format: '%Y-%m-%d'} }}
設定一些 x 軸的格式。首先用type
告訴 C3 這個 x 軸代表一組'timeseries'
的資料。接著用format
說明我們想要怎麼樣的日期格式。
圖四、先在 codepen 上看看效果
了解了,那麼就來重新做一份用時間序列來當作 x 軸的資料吧!
// 準備符合 C3 格式的資料
let alpacaNameDaily = [['x'].concat(uniqueDate)]; // 先做出一個時間序列
uniqueAlpacaName.forEach(x => alpacaNameDaily.push([x].concat(uniqueDate.map(x => 0))));
table.forEach(function (x) {
tempDate = uniqueDate.indexOf(x[4])+1;
tempName = uniqueAlpacaName.indexOf(x[1])+1; // 因為前面先塞了一個時間序列,要往後移位一個
alpacaNameDaily[tempName][tempDate] += x[3]/60; // 將單位從秒換成分
})
// 請 C3 幫我們畫圖囉!
var alpacaNameChart = c3.generate({
bindto: '#alpacaNameSpline',
data: {
x: 'x',
columns: alpacaNameDaily,
type: 'spline'
},
axis: { x: { type: 'timeseries', tick: { format: '%Y-%m-%d' } } }
});
圖五、我們把 x 軸變成日期了!
棒,是不是成功的將 x 軸改成較有意義的日期了呢?
接下來要講個有趣的東西,也是 JavaScript 的強大所在。回顧一下我們用 C3 在畫圖的時候,都是用 Python 送一些最基礎的資料到瀏覽器,接著透過 JavaScript 將資料轉換為符合 C3 需求的格式。不知道有沒有人懷疑為什麼我們不用 Python 先寫好符合的資料,再送到瀏覽器給 C3 使用就好了呢?
很棒的問題呢。第一個是因為,既然我們都上了 JavaScript 速成班,不好好顯擺一下自己的 JavaScript 有多流利怎麼行呢?第二個,也是真正的原因,是溝通的速度。想想看我們第 24 天在做選擇訓練紀錄的頁面時,我們讓使用者在瀏覽器填好表格,發送請求到伺服器,伺服器透過 Python 處理資料,最後回傳訊息給使用者。不同的資料就這麼來來回回在使用者(前端)跟伺服器(後端)之間跑了兩趟。
那 JavaScript 又可以做到什麼呢?JavaScript 可以在瀏覽器上直接處理資料。
以剛才那個例子,當伺服器將資料傳送給瀏覽器之後,使用者將資料填入表格,JavaScript 處理資料,並在瀏覽器上呈現結果。少了兩趟來回奔波的路程,少了在網路上塞車的苦悶,時間寶貴、一分鐘少說也幾十萬上下的使用者表示開心。
想想我們之前在操作表格的時候,伺服器將表格送到瀏覽器,使用者填寫表格,使用者提交表格,這時候我們才能拿到使用者填寫的資料。現在,伺服器一樣將表格送到瀏覽器,使用者一像填寫表格,問題來了,既然沒有提交表格回伺服器的動作,那麼要怎麼讓瀏覽器知道使用者填好表格了呢?
答案是靠 HTML 5 事件處理器(EventHandlers)。
所謂的事件處理器,就像是警鈴。使用者在瀏覽器上的一舉一動,瀏覽器都仔細地聽著。一個眼神,一個動作,只要符合條件,警鈴就會大做,這時我們可以放入一個 JavaScript 程式碼,告訴瀏覽器,當警鈴響起時,需要做什麼。
HTML 5 事件處理器的種類包羅萬象,從取消(oncancle
)、改變(onchange
)、點擊(onclick
)、雙擊(ondbclick
)、輸入(oninput
)、滑鼠移開(onmouseout
)、滑鼠移至(onmouseover
),還有許多許多。
今天最後的工作,我們就來利用 HTML 5 的事件處理器,實作出隨心所欲,即時呈現特定資料圖表的網頁。
那,我們的目標是什麼呢?回到草泥馬訓練身上,我們現在知道草泥馬每天訓練量的變化了,比如說'約翰'
的訓練量在 9/23 之後有些許的上升。但這時如果想看更詳細的資料,不僅僅想要知道訓練時間的多寡,還想知道究竟各種訓練分別執行了多久,這時候我們就需要一個選單,選擇是要把所有的訓練加總在一起呈現,還是不同的訓練分開呈現。
根據上面的目標,我們首先要提供一個選單:
<div class="row">
<div class="col">
<h2>alpacaName</h2>
<div id="alpacaNameSpline"></div>
</div>
</div>
<div class="row form-group">
<label class="col-2 col-form-label" for="alpacaNameSelect">查詢 training:</label>
<div class="col-3">
<select class="form-control" id="alpacaNameSelect">
<option value="overall" selected>Overall</option>
{% for label in uniques[1] %}
<option value="{{ label }}">{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
在 HTML 5 中用 Bootstrap 的元素做出美麗選單前面已經介紹過了,現在對我們而言應該是小菜一疊,就不在解釋,直接進入正題,看看該如何在 JavaScript 當中使用事件處理器:
let alpacaNameSelect = document.getElementById('alpacaNameSelect');
alpacaNameSelect.onchange = function (e) { console.log(e.target.value) };
第一行:let alpacaNameSelect = document.getElementById('alpacaNameSelect');
在 JavaScript 當中,利用document.getElementById()
找出 HTML 5 的檔案當中 id 為'alpacaNameSelect'
的物件。
第二行:alpacaNameSelect.onchange = function (e) { console.log(e.target.value) };
將alpacaNameSelect.onchange
指定到一個函數,當onchange
這個事件發生時,就執行該函數,也就是console.log(e.target.value)
,在 console 當中印出e.target.value
。什麼是e.target.value
呢?就是<option value="">
的value
。舉例來說,當我們在<select>
這個下拉式選單中點選"嚼食訓練"
時,此時的e.target.value
就是"嚼食訓練"
。執行效果如圖六所示。
圖六、onchange
偵測到使用者偷偷改變選項了
太好了,這樣一來,我們不用請使用者提交表單,瀏覽器也能馬上得知使用者選擇的選項。接下來只要修改onchange
事件發生時,瀏覽器需要執行的函數就行!
下面提供我寫的函數給大家參考看看。
// 回傳給 C3 畫圖用的 columns,所有資料
function showOverall (uniqueOverall, overallCol) {
let overallDaily = [['x'].concat(uniqueDate)];
uniqueOverall.forEach(x => overallDaily.push([x].concat(uniqueDate.map(x => 0))));
table.forEach(function(x) {
let tempDate = uniqueDate.indexOf(x[4])+1;
let tempOverall = overallDaily.map(x => x[0]).indexOf(x[overallCol]);
overallDaily[tempOverall][tempDate] += x[3]/60;
})
return overallDaily;
}
// 回傳給 C3 畫圖用的 columns,特定資料
function showDetail (focus, uniqueDetail, detailCol) {
let detailDaily = [['x'].concat(uniqueDate)];
uniqueDetail.forEach(x => detailDaily.push([x].concat(uniqueDate.map(x => 0))));
tempFocus = table.filter(x => x.some(x => x===focus));
tempFocus.forEach(function (x) {
let tempDate = uniqueDate.indexOf(x[4])+1;
let tempDetail = detailDaily.map(x=>x[0]).indexOf(x[detailCol]);
detailDaily[tempDetail][tempDate] += x[3]/60;
})
return detailDaily;
};
// C3畫圖函數
function c3Generate(bindto, columns){
return c3.generate({
bindto: bindto,
data: { x: 'x', columns: columns, type: 'spline' },
axis: { x: { type: 'timeseries', tick: { format: '%Y-%m-%d' } } }
});
}
// 利用事件處理器來更新曲線圖
let alpacaNameSelect = document.getElementById('alpacaNameSelect');
alpacaNameSelect.onchange = function(e) {
console.log(e.target.value);
// 若選擇特定資料,則用 showDetail 提供的資料畫圖
if (e.target.value !== 'overall') { alpacaNameSpline = c3Generate('#alpacaNameSpline', showDetail(e.value, uniqueAlpacaName, 1)); }
// 若選擇所有資料,則用 showOverall 提供的資料畫圖
else { alpacaNameSpline = c3Generate('#alpacaNameSpline', showOverall(uniqueAlpacaName, 1)); };
};
上面我先定義了三個函數,分別是showOverall()
、showDetail()
、跟c3Generate()
。showOverall()
以及showDetail()
可以產生符合 C3 畫圖時需要的columns
資料,而最後再用c3Generate()
這個函數真正將我們的曲線圖畫出來。
利用這樣,我們就可以做出根據使用者選擇進行即時更新圖表的網頁了。由於所需要的資料都已經在使用者第一次請求該網頁時,就從伺服器發送過去了,接下來所有與繪圖相關的運算和資料,都是直接在瀏覽器中進行,省去了來回瀏覽器、伺服器之間的路程和時間,所以可以更新的非常快。
圖七、最後我們終於可以隨心所欲請 C3 幫我們畫圖了!
今天的內容就到這裡了,有興趣直接看看結果的可以到我根據以上內容實作出來的 "phoebe-takescareof-alpaca.herokuapp.com" 來玩玩。另外,今天相關的程式碼我也會放到 Github 上。若對本篇文章的內容有疑惑或是覺得哪些地方說明不夠清楚,歡迎大家在下面留言,我會盡可能的回覆的,謝謝大家。
註:對於此系列文有興趣的讀者,歡迎參考由此系列文擴編成書的 LINE Bot by Python,以及最新的系列文《賴田捕手:追加篇》
第 31 天 初始化 LINE BOT on Heroku
第 32 天 快速回覆 QuickReply 介紹
第 33 天 妥善運用 Heroku APP 暫存空間
第 34 天 妥善運用 LINE Notify 免費推播
第 35 天 製造 Deploy to Heroku 按鈕