[Rails]特定期間の投稿数の一覧表示とグラフ化

日々のアウトプットで投稿しています
今回は表題にもあるように特定期間の投稿数の一覧表示とグラフ化を行います
一度投稿したもの、bookの一覧でエラーが起きることに気がついたため修正して再掲載しています

#### 行いたいこと
１周間前までの投稿数を数えて一覧にし、さらにグラフ化する

#### 前提条件
ruby 3.1.2
Rails 6.1.7
device、bootstrap導入済み(bootstrapは見た目の問題なので機能には関係なし)
User及びBookモデル作成済み

# 1.投稿数の一覧表示
・１周間前、つまり今日含めて６日前まで取得する必要がある。
・今日  Time.zone.now.all_day
・1日前　1.days.ago.all_day
・2日前　2.days.ago.all_day
・３日前　3.days.ago.all_day
・４日前　4.days.ago.all_day
・５日前　5.days.ago.all_day
・６日前　6.days.ago.all_day
とすればいいが長い・・
仮に(無いとは思うが)90日前まで表示しようとするととんでもない記述が必要になる

ここでrails consoleで確認してみる
```
[1] pry(main)> 6.days.ago.all_day
=> Tue, 29 Nov 2022 00:00:00.000000000 UTC +00:00..Tue, 29 Nov 2022 23:59:59.999999999 UTC +00:00
[2] pry(main)> 1.days.ago.all_day
=> Sun, 04 Dec 2022 00:00:00.000000000 UTC +00:00..Sun, 04 Dec 2022 23:59:59.999999999 UTC +00:00
[3] pry(main)> 0.days.ago.all_day
=> Mon, 05 Dec 2022 00:00:00.000000000 UTC +00:00..Mon, 05 Dec 2022 23:59:59.999999999 UTC +00:00
```
**0.days.ago.all_dayで今日の日付が取得できる！**

ということは日付をnとして **n.days.ago.all_day** とすればn日前の日付が求められるはず！
scopeで書き方を探していると以下のように記述すれば良い事が判明
※scopeとは、モデルに処理を定義することで、コントローラー等での記述を簡潔にする作業の事。イメージとしてはショートカットアイコンを作るような物でしょうか？
今回はcreated_dayと記述することで以降の処理を行えるようにします

```
scope :created_day, ->(n) {where(created_at: n.days.ago.all_day)}
```

## モデルへの記述
上記で説明したscopeをBookモデルに記述
```model/book.rb
 scope :created_day, ->(n) {where(created_at: n.days.ago.all_day)}
```

## viewの記述
```view/books/_index.html.rb
<div>
  <h3>7日分の投稿数</h3>
</div>
<table class='table table-bordered'>
  <% day_range = (0..6) %>　#日数のデータ
  <thead>
    <tr>
    <% day_range.reverse_each do |n| %>
      <% if n == 0 %>
        <th>今日</th>
      <% else %>
        <th><%= n %>日前</th>
      <% end %>
    <% end %>
    </tr>
  </thead>
  <tbody>
    <tr>
    <% day_range.reverse_each do |n| %>
      <td><%= books.created_day(n).count %></td>
    <% end %>
    </tr>
  </tbody>
</table>
```
**day_range = (0..6)**
何日前までのデータかを表します。０＝今日なので1週間前である６まで指定
ここの値を変えれば何日前でも表示出来るようになります

**day_range.reverse_each do |n|**
reverse_eachというのは与えられた値を逆に処理する式です
今回は画面右側が今日になるように表を作りたいのでこのように記述
そして以下のifでnが０の時、つまり今日の場合のみ画面表示を「今日」にしています(0日前と表示されてしまうため)

**books.created_day(n).count**
ここのbooksはuser_controllerで処理された
@user = User.find(params[:id])
@books = @user.books
の@books(部分テンプレートで@は取れています)つまり、findで取得して来たidに紐づくユーザーが、投稿した本の全てを指します
これに対してモデルで作成したcreated_day(n)でn日前に作成されたデータを探し、.countで数を数えます

# 2.グラフ化
### chart.jsの導入
公式ホームページはこちら

https://www.chartjs.org/docs/latest/

導入には、npm、CDN、jsDelivr、Github等々あるのでお好みで

今回はnpmを使用しました。ターミナルで下記のように入力
```
npm install chart.js
```
次に公式ページにもありますが、Chart.js をページに含める必要があります。
私はこれに気が付かず、ChromeにChart.jsが無え！と怒られ、3時間程スペルチェックで時間を浪費しました(笑)
```views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <%= yield %>
  </body>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> #ここに追加
</html>

```

後は実際に表示したい場所に記述するだけです。
※_book_index.html.jsと記載していますが、コードを見やすくするためにjs形式の表示にしている為であり、実際は_book_index.html.erbです。新規にjs形式でファイルを作成する必要は有りません。
```views/users/_book_index.html.js
<div>
  <canvas id="myChart"></canvas>
</div>
<script>
  $(document).on('turbolinks:load',function(){ //turbolinkの無効化
    const ctx = document.getElementById("myChart"); //canvasで指定したidを取得し ctxに代入
      new Chart(ctx, {
      type: 'line',  //グラフの種類を表す。今回は折れ線なのでline
      data: {
        labels: ["6日前", "5日前", "4日前", "3日前", "2日前", "1日前", "今日"],　//横軸のラベル、今回は日付なのでこのように指定
         datasets: [{
           label: "投稿した本の数", //　凡例
           data: [<%= books.created_day(6).count %>,  //6日前の投稿数
           <%= books.created_day(5).count %>,  //5日前の投稿数
           <%= books.created_day(4).count %>,  //4日前の投稿数
           <%= books.created_day(3).count %>,  //3日前の投稿数
           <%= books.created_day(2).count %>,  //2日前の投稿数
           <%= books.created_day(1).count %>,  //昨日の投稿数
           <%= books.created_day(0).count %>], //今日の投稿数
          borderColor: "rgba(0,0,255,1)",  //線の色、他にもbackgroundcolor等も指定できます
          lineTension: 0.5  //折れ線の丸み具合。0で直線になります
         }],
      },
      options: {
        responsive: true,
        scales: {
          y: { //縦軸のオプション
            suggestedMin: 0,  //最小値に設定
            suggestedMax: 10, //最大値の設定
            ticks: {
              stepSize: 1,　//目盛りの数え方。今回は１刻みなため１を指定
            }
          }
        }
      }
    });
  });
</script>
```
labelで横軸に記載する日付のデータを、datasets内のbooks.created_day().count実際に投稿するデータの数を取得しています。
これで完成！
![スクリーンショット 2022-12-06 10.48.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950107/179ada9f-4a5b-fd69-0000-7bff30b44f18.png)


### 補足
私もそうですが、普段Rubyで勉強している人にはわかりにくいと思うので書き換え(勿論この記述でも動きます)
$(document).on ('turbolinks:load', function()の中に
・const ctx
・new Chart　の２つが入っていて、さらにnew Chart内に
　・type:
　・data:
　・options:　が入っている形になる
```views/users/_book_index.html.js
  $(document).on ('turbolinks:load', function(){
    const ctx = document.getElementById("myChart");
    new Chart(ctx, {
      type: 'line',
      data: {labels: ["6日前", "5日前", "4日前", "3日前", "2日前", "1日前", "今日"],
             datasets: [{label: "投稿した本の数",data: ~長いので省略~,borderColor: "rgba(0,0,255,1)",lineTension: 0.5}]},
      options: {scales: {y: {suggestedMin: 0,suggestedMax: 10,ticks: {stepSize: 1}}}},
   }); //new Chartの閉じカッコ
  });// 'turbolinks:load', functionの閉じカッコ
```
またoptionsを指定しないと、縦軸の目盛りは自動調整されるため、投稿数が少ないと下記のようになります
![スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950107/4dd93110-9625-41fa-2eed-30b880d5decf.png)

## 応用的な話

### グラフの再描画
自分の投稿数のグラフは問題なく表示されているかと思いますが、他人の詳細ページに行った際に、自分のグラフのままになっていることがあります。
公式のページを見ると、グラフを再描画する前に、すでに表示しているグラフの破棄が必要との事。

https://www.chartjs.org/docs/latest/developers/api.html

よって下記のようにコードを追加します
```views/users/_book_index.html.js
<script>
  var chart  //chartを新しく生成
  $(document).on ('turbolinks:load', function(){
    const ctx = document.getElementById("myChart");
    if ( chart ){ chart.destroy(); }  //もしすでにchartが有るならchartを削除
    chart = new Chart(ctx, {  // chart = を追加
      type: 'line',
       //以下略
```
追加、変更したのは３行です
var chart
if ( chart ){ chart.destroy(); }
chart = new Chart(〜
これによってchartを毎回破棄して新しく描画することが出来るようになります！

### グラフデータのリファクタリング
なんとかグラフ部分を配列処理できないかと思って色々試したところ下記のやり方を発見
```views/users/_book_index.html.rb
<% day_range = (0..6) %> #元々一覧作成で記述しているが説明のため記載

<% count = [] %> #空の配列を作成
<% day_range.reverse_each do |n| %>
  <% count << books.created_day(n).count %>　#空の配列に投稿数を追加していく
<% end %>
```
[0,1,0,2,1,1,0]のような配列が出来上がります
これをdatasets:のdata:に代入して
```views/users/_book_index.html.js
$(document).on ('turbolinks:load', function(){
    const ctx = document.getElementById("myChart");
    new Chart(ctx, {
      type: 'line',
      data: {labels: ["6日前", "5日前", "4日前", "3日前", "2日前", "1日前", "今日"],
             datasets: [{label: "投稿した本の数",data: <%= count %>,borderColor: "rgba(0,0,255,1)",lineTension: 0.5}]},
      options: {scales: {y: {suggestedMin: 0,suggestedMax: 10,ticks: {stepSize: 1}}}},
   });
  });

```
簡潔に書くことが出来ました。

ただ日付に関しては、勉強不足なのかあれこれ試したものの実装出来ませんでした。
良い方法ご存知の方はコメントなど頂けると幸いです。

# 最後に
ご覧いただいた方、ありがとうございました
意見、感想等ありましたらご自由にご記入ください