【Rails6】Ajax + chart.jsでインタラクティブなグラフ描写

今回はRuby on Railsで簡単なチャート機能をajaxとchat.jsを使って実装します。例題としてプルダウンで選択した人物の成長記録を描写してみます。（横軸：記録日、縦軸：身長）
尚、Railsのバージョンは6.0.3.3を利用しています。
![身長.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641876/3f9034f8-aa92-6e83-9195-27e0de1443ab.gif)


#ご留意ください
#テーブル定義
|カラム名fi|カラム説明|データ型|
|:---|:---|:---|
|id|ID|integer|
|name|名前|string|
|height|身長|float|
|rec_date|身長記録日|date|
|created_at|登録日|datetime|
|updated_at|更新日|datetime|
今回は`seeds.rb`に二人分の身長記録データを登録しています。
#準備
##Gemに追記
```ruby:Gemfile
gem 'chart-js-rails', '~> 0.1.4'
```
##yarnでパッケージを追加
```bash:command
yarn add jquery chart.js
```
##jQueryを扱えるようにする
```javascript:config/webpack/environment.js
const { environment } = require('@rails/webpacker')
// jqueryを使用できるようにする
const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/src/jquery',
    jQuery: 'jquery/src/jquery'
  })
)
module.exports = environment
```
##chart.jsモジュールの読み込み
```javascript:app/javascript/packs/application.js
// chart.js利用のため追加
require("chart.js")
```
##モデルの作成
```bash:command
rails generate model Height name:string height:float rec_date:date
rails db:migrate
```
##初期データの作成＆投入
```ruby:seeds.rb
Height.create!(
  [
    {
      name: 'たかし',
      height: 150.3,
      rec_date: "1990-01-03",
    },
    {
      name: 'たかし',
      height: 168.3,
      rec_date: "1996-03-03",
    },
    {
      name: 'たかし',
      height: 178.4,
      rec_date: "2003-04-03",
    },
    {
      name: 'ゆき',
      height: 130.3,
      rec_date: "1987-05-07",
    },
    {
      name: 'ゆき',
      height: 144.1,
      rec_date: "1995-04-23",
    },
    {
      name: 'ゆき',
      height: 153.6,
      rec_date: "2000-05-13",
    },
  ]
)
```
```bash:command
rails db:seed
```
##コントローラの作成
`chart_sample_controller.rb`にindexアクションとsearchアクションを用意します。searchアクションは`$.ajax()`を使ったリクエスト送信を受け取り、モデル内検索により名前に該当する身長のデータをjson形式で返すためのアクションになります。

```bash:command
rails generate controller ChartSample index search
```
ここまでで、必要なパッケージ、Gem、モデル、コントローラの作成まで終わりました。続いてルート設定とビューを用意します。

#ルート設定とビュー
##ルート設定
```ruby:config/routes.rb
Rails.application.routes.draw do
  get 'chart_sample/index'
  get '/chart_sample/search', to: 'chart_sample#search'
end
```
##Views
`javascript_pack_tag`でjsファイルを読み込んでいますが、ここではAjax+chart.jsを使ってグラフを描写するスクリプトをjQueryで記述しています。jsファイルは`app/javascript/packs/chart_sample/chart_user_height.js`に記述します。

```html:app/view/chart_sample/index.html.erb
<h1>成長記録</h1>
<div class="contents">
  <select id="area">
    <option value="0">選択してください</option>
    <option value="1">たかし</option>
    <option value="2">ゆき</option>
  </select>
</div>

<div class="canvas">
  <canvas id="myChart" width="400" height="400"></canvas>
</div>

<%= javascript_pack_tag 'chart_sample/chart_user_height' %>
```
ビューの画面は以下のようになります。
出来上がりはプルダウンで名前を選択→下に成長記録のグラフが表示されるといった具合です。
![スクリーンショット 2020-09-30 16.24.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641876/6aee8a1e-ac25-6aad-8b7b-bf22452bfb93.png)


##Ajax+chart.js
ここのコードは長いので順を追って説明して最後にまとめてソースコードを貼ります。


```javascript:app/javascript/packs/chart_sample/chart_user_height.js（その1）
$(document).on('turbolinks:load', function () {
  $(function () {
    // chart表示用の自作関数を読み込む
    var chart_func = require("../origin_func.js")
    $('#area').change(function() {
      var selected_name = $('option:selected').text();
      var selected_index = $('option:selected').val();
```
ここではAjaxの影響等によりページ遷移でjQueryが正常に作動しない現象を防ぐため、`turbolinks:load`で初回読み込み時、リロードどちらでも発火するようにしています。`origin_func.js`はchart.jsによるチャート描写機能を関数化したものを読み込んでいます。これは後ほど説明します。先ほどのビューのセレクトボックスのインデックスを`selected_index`に、名前を`selected_name`に定義しています。

```javascript:app/javascript/packs/chart_sample/chart_user_height.js（その2）
      if (selected_index > 0) {
        // ajaxを使ってSelectBoxの値をjson形式でリクエスト送信する
        // messages/searchesコントローラのindexアクションが受け取る
        $.ajax({
          type: 'GET', // リクエストのタイプ
          url: '/chart_sample/search', // リクエストを送信するURL
          data:  { name: selected_name }, // サーバーに送信するデータ
          dataType: 'json' // サーバーから返却される型
        })
```
ここでは`selected_index>0`つまり名前（今回は「たかし」か「ゆき」）が選択された場合に処理される内容です。`$.ajax()`を使って取得した名前をコントローラに送信します。先ほどのルート設定の通り、`chart_sample_controller.rb`のsearchアクションへ送信されます。
`chart_user_height.js`の続きの前にリクエストを受け取ったコントローラの`search`アクションの内容について説明します。

```ruby:app/controllers/chart_sample_controller.rb
class ChartSampleController < ApplicationController
  def index
  end

  def search
    # ↓検索処理のコード(検索結果の一覧が入力される)
    @height = Height.where('name = (?)', "#{params[:name]}").order(rec_date: "ASC")    

    respond_to do |format|
      # リクエストされるフォーマットがHTML形式の場合
      format.html { redirect_to :root }
      # リクエストされるフォーマットがJSON形式の場合
      format.json { render json: @height }
    end
  end
end
```
`search`アクションでは先ほどAjaxで送信されたリクエスト(人の名前)がparams[:name]に格納されているので`Height`モデル内で一致検索し、モデルから返された中身の`rec_date`を古い順に並び替えています。これは後ほど、chart.jsでグラフ表示する際に横軸を`rec_date`にするためです。JSON形式でリクエストされているので`format.json`が実行され、検索結果がJSON形式で返されます。分かりにくい場合は`binding.pry`などでデバッグしてみるとイメージがつきやすいと思います。
再びjsファイルに戻り描写処理についてみていきます。

```javascript:app/javascript/packs/chart_sample/chart_user_height.js（その3）
        // ajaxからリクエストを受けとり
        .done(function (data) {
          var height_val = [];
          var height_name = [];
          var height_date = [];
          // chart.jsに渡すため配列に格納する
          $(data).each(function(index, height) {
            height_name.push(height.name);
            height_val.push(height.height);
            height_date.push(height.rec_date);
          });
          chart_func.bar_chart(document, 'myChart', "身長", height_date, height_val);
        })
      }
    })
  });
});
```
ここでは、モデルから受け取ったJSONフォーマットをchart.jsに渡すため配列型に直しています。例えば`height_val`はたかし君の過去の身長が配列として[(身長),(身長),(身長)]といった具合に入ります。（もう少しスッキリと書ける良い方法がある気がするのですが・・・）
そして、日付データと身長データが含まれた配列を読み込んだ自作関数に渡します。自作関数は`chart.js`でChartをインスタンス化しています。関数に渡す引数は`chart_func.bar_chart(document, canvasタグのid名, グラフに付けたいラベル名(任意), 横軸データの配列, 縦軸データの配列)`となります。

## chart.jsによる描写処理

```javascript:app/javascript/packs/origin_func.js
exports.bar_chart = function (document, id_name, label_name, data_x, data_y) {
    var elm = document.getElementById(id_name).getContext('2d');
    //既にチャートが描写されている場合はインスタンスを削除する
    if(myChart.constructor === Chart){
        myChart.destroy();
    };
    //棒グラフの描写
    myChart = new Chart(elm, { 
        type: 'bar',
        data: {
            labels: data_x,
            datasets: [{
                label: label_name,
                data: data_y,
                borderWidth: 1
            }]
        },
        options: {
            scales: {
                yAxes: [{
                    ticks: {
                        beginAtZero: true
                    }
                }]
            }
        }
    })
};
```

```javascript
if(myChart.constructor === Chart){
        myChart.destroy();
    };
```
ここの`if`文による記述はcanvasを再利用するとグラフが前のグラフに重なって記述されてしまうため、もし既に`Chart`のインスタンスが作られている場合はインスタンスを`Chart.destroy`で削除する必要があるためです。`myChart.constructor`は読み込み時点では`HTMLCanvasElement`からインスタンスが作成されると`Chart`となります。
残りは配列データをもとにChartインスタンスを作成しています。
実装は以上になります。
`chart_user_height.js`を途切れ途切れに説明したのでまとめて以下に貼り付けておきます。

```javascript:app/javascript/packs/chart_sample/chart_user_height.js（まとめて）
$(document).on('turbolinks:load', function () {
  $(function () {
    // chart表示用の自作関数を読み込む
    var chart_func = require("../origin_func.js")
    $('#area').change(function() {
      var selected_name = $('option:selected').text();
      var selected_index = $('option:selected').val();
      if (selected_index > 0) {
        $("h1").css("color", "blue");  
        // ajaxを使ってSelectBoxの値をjson形式でリクエスト送信する
        // messages/searchesコントローラのindexアクションが受け取る
        $.ajax({
          type: 'GET', // リクエストのタイプ
          url: '/chart_sample/search', // リクエストを送信するURL
          data:  { name: selected_name }, // サーバーに送信するデータ
          dataType: 'json' // サーバーから返却される型
        })
        // ajaxからリクエストを受けとり
        .done(function (data) {
          var height_val = [];
          var height_name = [];
          var height_date = [];
          // chart.jsに渡すため配列に格納する
          $(data).each(function(index, height) {
            height_name.push(height.name);
            height_val.push(height.height);
            height_date.push(height.rec_date);
          });
          chart_func.bar_chart(document, 'myChart', "身長", height_date, height_val);
        })
      }
    })
  });
});
```


# 最後に
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。今後はグラフの表現を持たせたり、保存機能を付けたりしてきたいと思います。
ここまでお付き合いいただきありがとうございました！