Chartkickを利用して「Ruby on Railsの記述のみ」で積み上げ棒グラフを作る

# はじめに
プログラミングスクールでのポートフォリオ制作で学んだことをアウトプットします。
Rails初学者のため、誤りやアドバイスがありましたらご指摘いただけると嬉しいです！

今回は、Railsで以下のような「棒グラフ」を簡単に作る方法についてまとめます。
＊メインは「積み上げ棒グラフ」の作成方法で、補足として通常の「棒グラフ」や「水平方向の棒グラフ」などの作り方をまとめています。
使用するのはGemの「Chartkick」とJavaScriptのライブラリ 「Highcharts」です。
![index_column.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2635852/5b5bf803-825a-8b6e-ed07-935eea96134d.png)

:::note info
記載したコードは、わかりやすいよう一部省略・編集している場合があります。不明点はGitHubでご確認いただくか、お問い合わせいただければ幸いです。
:::

:::note warn
Highchartsは、「商用利用では有料」です。
:::

# 開発環境
- Ruby（2.6.5）
- Ruby on Rails（6.0.0）

# ポートフォリオ
URL: https://good-yen.onrender.com
GitHub: https://github.com/Asami119/good_yen


# 前提となる情報
- Chartkick（Gem）を利用することで、Highcharts（JavaScriptのグラフ描画ライブラリ）を「Rubyの記述だけ」で利用し、グラフを簡単に実装することができる！
- ポイントは、データを以下のような形式になるようセットし、ビューへ渡すこと
```ruby:コントローラー
@data = [{ name: 'Good yen!', data: [sample1, sample2, sample3] }, { name: 'あと一歩', data: [sample4, sample5, sample6] }]
# 変数のsample1からsample6には、それぞれ['1月', 1000]や['2月', 2000]のように「項目名（横軸）」と「数値(縦軸)」を配列の形で代入する
```
```ruby:ビュー
# 「column_chart（=縦向き棒グラフ）」はグラフの種類を指定する記述
# 続けて、データを代入したインスタンス変数を記述
# 「stacked: true」は、データを積み上げ式にするための記述
<%= column_chart @data, options = { stacked: true } %>
```
- `積み上げ式ではない「棒グラフ」`を作成したい場合は、データのセット形式とビューの記述を以下に変更（＊詳細は「補足」をご参照ください）
```ruby:コントローラー
@data = { '項目名1': sample1, '項目名2': sample2, '項目名3': sample3 }
# keyに項目名、valueに対応する数値（または数値を代入した変数）を指定
```
```ruby:ビュー
# オプションの「stacked: true」は不要
<%= column_chart @data %>
```

# 手順
:::note info
大まかな流れ
・環境設定（①）
・ビュー（②）・コントローラー（③）・モデル（④）への記述
＊②〜④の作業は、順番通りでなくて大丈夫です。
:::

## ①環境設定
こちらについては、以下記事の「手順①〜③」と同様です。
[Chartkickを利用して「Ruby on Railsの記述のみ」で簡単にドーナツグラフを作る
](https://qiita.com/Asami119/items/03511d576cfa20b880df)

## ②ビューに以下のような任意の記述を追加
:::note info
ポイントは以下の3つです。
・1行目の「column_chart」の記述でグラフの種類を指定
・続くインスタンス変数「@monthly_price_sums」には、反映したいデータが代入されている
・続く「options」以下の記述で、グラフの「色」や「サイズ」などの表示形式を指定
:::

```ruby:app/views/posts/search.html.erb
# 「column_chart」の記述で、グラフの種類を「棒グラフ」に指定
<%= column_chart  @monthly_price_sums, options = {
  stacked: true, # 積み上げ式にするための記述
  max_width: '100vw', # 幅を指定
  colors: ["#ACFF40", "#FFF33F"], # 色を指定
  thousands: ",", # 金額表示にする（カンマを入れる）
  suffix: "円" # 単位を指定
} %>
```
反映イメージは以下です。
![index_column.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2635852/a44606f2-9ffa-6a27-b515-3301d257eee7.png)

## ③ コントローラーに以下のような任意の記述を追加
以下の記述では、Postモデルに自作したcalc_column_and_barメソッドを呼び出し、引数postsを用いた計算結果を2つのインスタンス変数に代入しています。
これによりビューファイル「search.html.erb」にて、インスタンス変数の記述でデータをグラフ化できるようにしています。

```ruby:app/controllers/posts_controller.rb
def search
  posts = @q.result.order(date_of_post: :DESC, created_at: :DESC)
  @price_sum = posts.sum(:price)
  @monthly_price_sums, @monthly_price_average = Post.calc_column_and_bar(params, posts, @price_sum) if posts.present?
  # 引数postsには、反映したいデータ（検索で絞り込んだデータ）を代入
  # 引数postsが空だった場合、上記は実行しない（ifで条件分岐）
end
```

## ④ モデルに以下のような任意の記述を追加
制作したアプリでは、以下のような検索フォームに入力された値で`絞り込んだデータ`をグラフ化する設計にしています。
＊入力がなかった場合は、「すべてのデータ」がグラフ化されます。
そのため長い記述になっていますが、「前提となる情報」に記載した通り、ポイントは`目的の形式でデータをセットする`という点です。
![index_form_search.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2635852/285741b4-9591-96a8-4126-77b9430a7cc4.png)

コントローラーで呼び出したcalc_column_and_barメソッドから、さらにset_monthメソッドとcalc_monthly_averageメソッドを呼び出しています。

検索フォームの日付欄に入力があった場合はその期間内のデータ、指定がなかった場合はすベてのデータについて、`月ごとの「Good yen!(true)」と「あと一歩(false)」の合計金額`を算出→目的の形式（「前提となる情報」にて記載した形式）でセットして、「@monthly_price_sums」に代入しています。

また、該当データの金額合計を月数で割った金額（月平均）も算出し、「@monthly_price_average」に代入しています。

:::note warn
以下のcalc_column_and_barメソッドの記述は、Rubocopより「Method has too many lines（メソッドの行数が多すぎます）」との指摘を受けています。リファクタリング予定です。
:::

```ruby:app/models/post.rb
# 引数で渡されたデータについて、月ごとに「Good yen!(true)」と「あと一歩(false)」の金額を算出
# その値を目的の形式で配列に代入し、calc_column_and_barメソッドへ返す
def self.set_month(posts, year, first_month, last_month, array)
  first_month.upto(last_month) do |i|
    first_day = Time.new(year, i, 1)
    month_post = posts.select(:select_yen, :price).where(date_of_post: first_day.all_month)
    # all_monthメソッドは、うるう年にも対応
    price_true = month_post.where(select_yen: true).sum(:price)
    price_false = month_post.where(select_yen: false).sum(:price)

    array[0].push(["#{year}-#{i}月", price_true])
    array[1].push(["#{year}-#{i}月", price_false])
  end
  array
end

# データの合計金額を月数で割った金額（月平均）を算出し、calc_column_and_barメソッドへ返す
def self.calc_monthly_average(monthly_price_sums, price_sum)
  month_count = monthly_price_sums.dig(0, :data).count
  (price_sum / month_count)
end

# コントローラで呼び出したメソッド
def self.calc_column_and_bar(params, posts, price_sum)
# 検索フォームの日付欄に入力があった場合は、gteq/lteqにその日付を代入
# 日付の指定がなかった場合は、保存されている一番新しい/古い記録それぞれの日付を代入
  gteq = if params.dig(:q, :date_of_post_gteq).present?
            params.dig(:q, :date_of_post_gteq).to_date
          else
            posts.last[:date_of_post]
          end
  lteq = if params.dig(:q, :date_of_post_lteq).present?
            params.dig(:q, :date_of_post_lteq).to_date
          else
            posts.first[:date_of_post]
          end

  old_year = gteq.year # 「一番新しいデータの日付」の年
  old_month = gteq.month # 「一番新しいデータの日付」の月
  new_year = lteq.year # 「一番古いデータの日付」の年
  new_month = lteq.month # 「一番古いデータの日付」の月

  price_true_sums = []
  price_false_sums = []
  array = [price_true_sums, price_false_sums] # 配列の中に空の配列2つをセット

# 該当するデータがすべて同じ年のものなら、true
# データが年をまたぐ場合、else内の処理を実行
  if old_year == new_year
    set_month(posts, old_year, old_month, new_month, array)
  else
    january = 1
    december = 12
    set_month(posts, old_year, old_month, december, array)
    old_year += 1
    while old_year != new_year
      set_month(posts, old_year, january, december, array)
      old_year += 1
    end
    set_month(posts, old_year, january, new_month, array)
  end

  monthly_price_sums = [{ name: 'Good yen!', data: price_true_sums }, { name: 'あと一歩', data: price_false_sums }]
  [monthly_price_sums, calc_monthly_average(monthly_price_sums, price_sum)] # 戻り値2つをセット
end
```
最後に記述した角カッコ内の値が、「@monthly_price_sums」「@monthly_price_average」にそれぞれ代入されます。
「@monthly_price_sums」には、「前提となる情報」にて記載した形式でセットされたデータが代入されています。
「@monthly_price_average」には、該当データの合計金額を月数で割った金額（月平均）が代入されています。

# 補足
## ①「水平方向」の棒グラフを作成したい場合
ビューに記述するグラフの種類を「column_chart」から「bar_chart」に変更するだけで、棒グラフの向きを以下のような水平方向に変更することができます。
[![Image from Gyazo](https://i.gyazo.com/6eadbdd47d6f865f27b718665a7ceda6.png)](https://gyazo.com/6eadbdd47d6f865f27b718665a7ceda6)

## ②積み上げ式ではない「棒グラフ」を作成したい場合
積み上げ式ではない通常の「棒グラフ」を作成したい場合は、データのセット形式とビューの記述を以下のようにしてください。

```ruby:コントローラー
@data = { '項目名1': sample1, '項目名2': sample2, '項目名3': sample3 }
# keyに項目名、valueに対応する数値（または数値を代入した変数）を指定
```

```ruby:ビュー
<%= column_chart @data %>
```

:::note info
以下、公式サイトの記述例を引用します。
:::

```ruby:ビューファイル
#公式の例
<%= column_chart Order.group_by_day_of_week(:created_at, format: "%a").count %>
```

「Order.group_by_day_of_week(:created_at, format: "%a").count」で、Orderモデルの「created_at」カラムについて、「曜日ごとのレコード数」を算出しています。
group_by_day_of_weekメソッドは、「groupdate」というGemを導入することで利用できるようです。
＊参考：[Railsでシンプルなグラフを出力 gem-chartkick・groupdate使用](https://qiita.com/nakamura999/items/d0ebc9fb8cb4a3c0f135)
```ruby:groupメソッドとcountメソッドを併用した場合の戻り値（予想）
Order.group_by_day_of_week(:created_at, format: "%a").count
=> {"Sun"=>32, "Mon"=>45, "Tue"=>28, "Wed"=>21, "Thu"=>20, "Fri"=>17, "Sat"=>27}
```
戻り値はハッシュ形式で返されるため、上記のようなビューファイルの記述1行で、以下のような棒グラフの反映が可能となります。
[![Image from Gyazo](https://i.gyazo.com/4847a265f8705dbf2bd2286beaf0073a.png)](https://gyazo.com/4847a265f8705dbf2bd2286beaf0073a)
＊上記画像は[Chartkick公式サイト](https://chartkick.com/)より引用

## ③棒グラフを積み上げ式ではなく「並列して」作成したい場合
以下イメージのように、グラフをグループ化（1項目に棒2本を反映）することも可能です。
![42577d50e5292af62735ceb1fa3c8ffa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2635852/d2bad05d-6434-25bf-0ab4-6b9359993140.png)

作業②のビューの記述を基本とし、オプションの「stacked: true」を削除することでグループ化できます。

```ruby:コントローラー
@data = [
         { name: 'Good yen!', data: [['3月', 10000],['4月', 14000]]},
         { name: 'あと一歩', data: [['3月', 5000],['4月', 4000]]}
        ]
```
```ruby:ビューファイル
<%= column_chart @data, options = {
  # stacked: true, 削除する
  max_width: '100vw',
  colors: ["#ACFF40", "#FFF33F", "#FF404C"],
  thousands: ",", 
  suffix: "円"
} %>
```
＊上記のように、反映するデータが静的な（確定している）場合、インスタンス変数に代入するのではなく、`ビューに直接データを記述`する形でもグラフ化が可能です。

## ④③をさらに積み上げ式で作成したい場合
以下イメージのように、積み上げ棒グラフを「並列」することもできるようです。
＊新たに「Bad yen」（赤色部分）を付け足してみました。
![e9deb119411ee598d03776beffc95174.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2635852/5bc3ca6a-86bf-5cff-56f6-c91475f808a9.png)

データに「stack」のキーバリュー（valueは任意の値）を付け足すことでグループ化できます。
＊この場合、「stack」キーのvalue（以下の例で言うと「stack1」「stack2」）の画面への反映はありません。グループの「識別」に使われるだけです。
```ruby:コントローラー
@data = [
         { name: 'Good yen!', stack: 'stack1', data: [['3月', 10000],['4月', 14000]]},
         { name: 'あと一歩', stack: 'stack2', data: [['3月', 5000],['4月', 4000]]},
         { name: 'Bad yen', stack: 'stack2', data: [['3月', 2000],['4月', 3000]]}
        ]
```
```ruby:ビューファイル
<%= column_chart @data, options = {
  stacked: true,
  max_width: '100vw',
  colors: ["#ACFF40", "#FFF33F", "#FF404C"],
  thousands: ",", 
  suffix: "円"
} %>
```
＊上記のように反映するデータが静的な（確定している）場合、インスタンス変数に代入するのではなく、`ビューに直接データを記述`する形でもグラフ化が可能です。

今回は以上です。
読んでくださってありがとうございました！

# 参考URL（感謝です！）
- [Chartkick公式サイト](https://chartkick.com/)
- [Create beautiful JavaScript charts with one line of Ruby](https://jsrepos.com/repo/ankane-chartkick-javascript-data-visualization)