22. 圖表資料分析
22-1 前言和 Chart.js 安裝
這一章介紹如何製作圖表來分析報名資料,包括:
- 分析票種的報名人數
- 分析票種的總金額
- 分析票種的報名人數,並根據狀態分類(報名尚未完成、報名成功)
- 分析每日報名人數,並根據狀態分類
製作圖表需要用到前端 JavaScript 套件,例如:
- Chartkick 這一套搭配 Rails 最簡單,甚至不需要寫 JavaScript 代碼,有 Helper 可以使用
- Chart.js 這一套好用又漂亮,需要寫 JavaScript
- D3.js 這一套功能最強,是專業的數據可視化套件,但也比較複雜
Chart.js 安裝
Chart.js 有 chart-js-rails gem 可以安裝,但是因為只有後台報表那一頁有用到,所以我們就不包進 asset pipeline 了,而是使用 CDN 讓用戶直接從 CDN 伺服器下載回去。
在中國大陸境內的話,可用 BootCDN 這個免費的服務,編輯 app/views/admin/events/show.html.erb
把該位置貼上去:
在台灣則可用 cdnjs 這個 CDN 免費服務。
+ <script src="//cdn.bootcss.com/Chart.js/2.5.0/Chart.bundle.min.js"></script>
<h1><%= @event.name %></h1>
這樣用戶進到這一頁,就可以使用 Chart.js 了。
讓我們試試看效果,再次編輯 app/views/admin/events/show.html.erb
,貼上一段範例:
<script src="//cdn.bootcss.com/Chart.js/2.5.0/Chart.bundle.min.js"></script>
<h1><%= @event.name %></h1>
<p>
<%= link_to "打開前臺", event_path(@event), :target => "_blank", :class => "btn btn-primary" %>
</p>
+ <canvas id="myChart" width="400" height="200"></canvas>
+ <script>
+ var ctx = document.getElementById("myChart");
+ var myChart = new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
+ datasets: [{
+ label: '# of Votes',
+ data: [12, 19, 3, 5, 2, 3],
+ backgroundColor: [
+ 'rgba(255, 99, 132, 0.2)',
+ 'rgba(54, 162, 235, 0.2)',
+ 'rgba(255, 206, 86, 0.2)',
+ 'rgba(75, 192, 192, 0.2)',
+ 'rgba(153, 102, 255, 0.2)',
+ 'rgba(255, 159, 64, 0.2)'
+ ],
+ borderColor: [
+ 'rgba(255,99,132,1)',
+ 'rgba(54, 162, 235, 1)',
+ 'rgba(255, 206, 86, 1)',
+ 'rgba(75, 192, 192, 1)',
+ 'rgba(153, 102, 255, 1)',
+ 'rgba(255, 159, 64, 1)'
+ ],
+ borderWidth: 1
+ }]
+ },
+ options: {
+ scales: {
+ yAxes: [{
+ ticks: {
+ beginAtZero:true
+ }
+ }]
+ }
+ }
+ });
+ </script>
這只是個 Chart.js 範例,並沒有用到資料庫的真實資料。在繼續下去之前,請刪掉它。
22-2 分析不同票種的報名人數
最常見的數據分析模式,就是根據不同分類計算數量或總和,例如我們來分析不同票種的報名人數。
以下數據沿用我們在 18-1 製作後台管理報名資料 所產生的假活動和報名數據。
編輯 app/controllers/admin/events_controller.rb
def show
@event = Event.find_by_friendly_id!(params[:id])
+ colors = ['rgba(255, 99, 132, 0.2)',
+ 'rgba(54, 162, 235, 0.2)',
+ 'rgba(255, 206, 86, 0.2)',
+ 'rgba(75, 192, 192, 0.2)',
+ 'rgba(153, 102, 255, 0.2)',
+ 'rgba(255, 159, 64, 0.2)'
+ ]
+
+ ticket_names = @event.tickets.map { |t| t.name }
+
+ @data1 = {
+ labels: ticket_names,
+ datasets: [{
+ label: "# of Registrations",
+ data: @event.tickets.map{ |t| t.registrations.count },
+ backgroundColor: colors,
+ borderWidth: 1
+ }]
+ }
編輯 app/views/admin/events/show.html.erb
<script src="//cdn.bootcss.com/Chart.js/2.5.0/Chart.bundle.min.js"></script>
<h1><%= @event.name %></h1>
<p>
<%= link_to "打開前臺", event_path(@event), :target => "_blank", :class => "btn btn-primary" %>
</p>
+ <canvas id="myChart1" width="400" height="200"></canvas>
+ <script>
+ var ctx1 = document.getElementById("myChart1");
+ var myChart1 = new Chart(ctx1, {
+ type: 'bar',
+ data: <%= raw @data1.to_json %>,
+ options: {
+ scales: {
+ yAxes: [{
+ ticks: {
+ beginAtZero:true
+ }
+ }]
+ }
+ }
+ });
+ </script>
+
+ <hr>
我們在 controller 中準備了@data1
變數就是要餵給 Chart.js 的分析資料,在 View 中需要把這個 Ruby 變數透過 to_json
轉成 JavaScript 變數。
這個 @data1
輸出後的數據格式長這樣:
Chart.js 參數說明:
type: 'bar', // 長條圖
data: {
"labels": ["Guest","VIP 第一期","VIP 第二期"], // 橫軸的標籤有哪些
"datasets": [
{ "label": "# of Registrations", // 數據集的名稱
"data": [324,346,330], // 數據(這是陣列)
"backgroundColor":["rgba(255, 99, 132, 0.2)","rgba(54, 162, 235, 0.2)","rgba(255, 206, 86, 0.2)","rgba(75, 192, 192, 0.2)","rgba(153, 102, 255, 0.2)","rgba(255, 159, 64, 0.2)"], // 長條的顏色
"borderWidth": 1 // 要有框線
}
]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero:true // X 軸要從 0 起跳
}
}]
}
}
詳細關於這個資料結構的說明,可以參考 Chart.js 文檔。
22-3 分析票種的總金額
跟上一節類似,但改成計算報名成功的總金額。
編輯 app/controllers/admin/events_controller.rb
+ @data2 = {
+ labels: ticket_names,
+ datasets: [{
+ label: '# of Amount',
+ data: @event.tickets.map{ |t| t.registrations.by_status("confirmed").count * t.price },
+ backgroundColor: colors,
+ borderWidth: 1
+ }]
+ }
其中 data 的部分變成計算報名成功的數量乘上該票價。
編輯 app/views/admin/event.html.erb
加上圖表:
<hr>
+ <canvas id="myChart2" width="400" height="200"></canvas>
+ <script>
+ var ctx2 = document.getElementById("myChart2");
+ var myChart2 = new Chart(ctx2, {
+ type: 'bar',
+ data: <%= raw @data2.to_json %>,
+ options: {
+ scales: {
+ yAxes: [{
+ ticks: {
+ beginAtZero:true
+ }
+ }]
+ }
+ }
+ });
+ </script>
這樣就完成了。Guest 票因為票價是 0 元,所以總金額也是 0。
22-4 分析票種的報名人數,並根據狀態分類
在上上節 “不同票種的報名人數” 圖表中,把報名未完成和報名成功的數據都算在一起了,有沒有辦法可以根據不同狀態分開算呢?
編輯 app/controllers/admin/events_controller.rb
- @data1 = {
- labels: ticket_names,
- datasets: [{
- label: "# of Registrations",
- data: @event.tickets.map{ |t| t.registrations.count },
- backgroundColor: colors,
- borderWidth: 1
- }]
- }
+ status_colors = { "confirmed" => "#FF6384",
"pending" => "#36A2EB"}
+ @data1 = {
+ labels: ticket_names,
+ datasets: Registration::STATUS.map do |s|
+ {
+ label: I18n.t(s, :scope => "registration.status"),
+ data: @event.tickets.map{ |t| t.registrations.by_status(s).count },
+ backgroundColor: status_colors[s],
+ borderWidth: 1
+ }
+ end
+ }
datasets
參數其實就是資料集的陣列,本來只有一個資料集,現在改成透過 Registration::STATUS.map
產生兩個資料集,分別是報名未完成和報名成功。
讓我們觀察一下實際輸出的數據格式長怎樣:
data: {
"labels": ["Guest","VIP 第一期","VIP 第二期"], // 這是 Y 軸的標籤
"datasets":[
{ "label": "報名尚未完成", // 這是第一個資料集
"data": [160,164,160],
"backgroundColor":"#36A2EB",
"borderWidth":1
},
{ "label": "報名成功", // 這是第二個資料集
"data":[164,182,170],
"backgroundColor":"#FF6384",
"borderWidth":1
}
]
},
當有多個資料集時,它會沿著 Y 軸的標籤依序畫上去。
22-5 分析每日報名人數,並根據狀態分類
分析從第一天到今天的每日報名人數,並根據狀態分類。
編輯 app/controllers/admin/events_controller.rb
。其中 labels 參數是 Y 軸,這裡改成用 dates 陣列,代表從有人報名的第一天到今天。
+ if @event.registrations.any?
+ dates = (@event.registrations.order("id ASC").first.created_at.to_date..Date.today).to_a
+ @data3 = {
+ labels: dates,
+ datasets: Registration::STATUS.map do |s|
+ {
+ :label => I18n.t(s, :scope => "registration.status"),
+ :data => dates.map{ |d|
+ @event.registrations.by_status(s).where( "created_at >= ? AND created_at <= ?", d.beginning_of_day, d.end_of_day).count
+ },
+ borderColor: status_colors[s]
+ }
+ end
+ }
+ end
跟上一節類似,data
有有兩個資料集分別是報名尚未完成和報名完成。
編輯 app/views/admin/event.html.erb
加上圖表,這裡改成用 line
折線圖:
<hr>
+ <canvas id="myChart3" width="400" height="200"></canvas>
+ <script>
+ var ctx3 = document.getElementById("myChart3");
+ var myChart3 = new Chart(ctx3, {
+ type: 'line',
+ data: <%= raw @data3.to_json %>,
+ options: {
+ scales: {
+ yAxes: [{
+ ticks: {
+ beginAtZero:true
+ }
+ }]
+ }
+ }
+ });
+ </script>
這樣就完成了。
進階補充
本章節分析票種的總金額、分析票種的報名人數,並根據狀態分類,是先把數據全部撈出然後用 Ruby 進行分類,可以改成用 SQL 的 GROUP BY 語法來做執行效率更好喔。