Link Search Menu Expand Document

22. 圖表資料分析

22-1 前言和 Chart.js 安裝

這一章介紹如何製作圖表來分析報名資料,包括:

  • 分析票種的報名人數
  • 分析票種的總金額
  • 分析票種的報名人數,並根據狀態分類(報名尚未完成、報名成功)
  • 分析每日報名人數,並根據狀態分類

製作圖表需要用到前端 JavaScript 套件,例如:

  • Chartkick 這一套搭配 Rails 最簡單,甚至不需要寫 JavaScript 代碼,有 Helper 可以使用
  • Chart.js 這一套好用又漂亮,需要寫 JavaScript
  • D3.js 這一套功能最強,是專業的數據可視化套件,但也比較複雜

我們將示範會使用 Chart.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>

image

這只是個 Chart.js 範例,並沒有用到資料庫的真實資料。在繼續下去之前,請刪掉它。

22-2 分析不同票種的報名人數

最常見的數據分析模式,就是根據不同分類計算數量或總和,例如我們來分析不同票種的報名人數。

image

以下數據沿用我們在 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 輸出後的數據格式長這樣:

image

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 分析票種的總金額

跟上一節類似,但改成計算報名成功的總金額。

image

編輯 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 分析票種的報名人數,並根據狀態分類

在上上節 “不同票種的報名人數” 圖表中,把報名未完成和報名成功的數據都算在一起了,有沒有辦法可以根據不同狀態分開算呢?

image

編輯 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 產生兩個資料集,分別是報名未完成和報名成功。

讓我們觀察一下實際輸出的數據格式長怎樣:

image

  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 分析每日報名人數,並根據狀態分類

分析從第一天到今天的每日報名人數,並根據狀態分類。

image

編輯 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 語法來做執行效率更好喔。


Copyright © 2010-2021 Wen-Tien Chang All Rights Reserved.