LoginSignup
10
11

More than 3 years have passed since last update.

Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #16 線グラフ, Chartkick編

Last updated at Posted at 2020-01-05

こんな人におすすめ

  • プログラミング初心者でポートフォリオの作り方が分からない
  • Rails Tutorialをやってみたが理解することが難しい

前回:#15 投稿機能, Active Storage編
次回:#17 VPC環境構築編

<追記:2020年2月20日>
記録時間をリセットする実装を追加しました。
データベースを初期化するDatabase Cleanerを追加しました。

今回の流れ

  1. 完成のイメージを理解する
  2. Chartkickを理解・導入する
  3. 各メソッドを作る
  4. コントローラを編集する
  5. ビューを作る
  6. 計測時間をリセットする画面を編集する
  7. テストを書く

この記事は、動画を観た時間を記録するアプリのポートフォリオです。
今回は入力したフォームからグラフを表示します。

完成のイメージを理解する

フォームに入力した時間を日付別に取り出し、合計したものを「週間・月間・年間」でグラフに表示できるようにします。

そのためにはグラフを描画するgem、Chartkickを使用します。
「週間・月間・年間」の切り替えには、ドロップダウンからクエリパラメータを送信・取得します。

最後に、以前プロフィール編集画面で実装予定だった、計測時間をリセットするボタンを実装します。

完成するとこんな感じです↓
lantern_figure.png
それでは手順通りに進めます。

Chartkickを理解・導入する

ここでの手順は以下の通りです。

  • Chartkickの動作を理解する
  • Chartkickを導入する

Chartkickの動作を理解する

Chartkickはグラフを描画するgemです。
設定を済ませると、下の記述だけでグラフを描けます。

hoge_controller.rb
@chart = { "2020-01-01" => 100, "2020-01-02" => 80, "2020-01-03" => 110, "2020-01-04" => 130, "2020-01-05" => 90 }
hoge.html.erb
<%= line_chart @chart %>

lantern_chart.png

Chartkickを導入する

早速、Chartkickを導入しましょう。
そのためにはインストールと設定が必要です。

Gemfile
+ gem 'chartkick'
bash
$ bundle install

ここからはRails5か6で手順が異なります
間違えないよう、気をつけましょう。

Rails 5以下

app/assets/javascripts/application.js
//= require chartkick
//= require Chart.bundle

Rails 6以上

bash
yarn add chartkick chart.js
app/javascript/packs/application.js
require("chartkick")
require("chart.js")

導入完了です。

あとはオプションを設定するためのファイルを作ります。
(Rails5、6ともに必要な手順です。)

bash
$ touch config/initializers/chartkick.rb
config/initializers/chartkick.rb
Chartkick.options = {
  curve: false,
  messages: {empty: "データがありません"}
}
  • 線グラフを曲線でなく直線にする
  • 空の場合のメッセージを加える

というオプションを追加しました。

補足:DBにSQLite3を使う場合は、Chartkickの日付管理に便利なGroupdateが使えない

Groupdateというgemを使うと、モデル(データベース)から期間ごとに取り出せるメソッドが使えるのですが、SQLite3には対応していません。
(当ポートフォリオも、SQLite3を使っています)

詳しくは、公式のドキュメントをご参照ください。
Chartkick(公式)
Groupdate(公式)

各メソッドを作る

ここでの手順は以下の通りです。

  • 日付と日ごとの合計時間をハッシュで出力するメソッドを作る
  • 期間内の合計時間(分、時間)を出力するメソッドを作る

日付と日ごとの合計時間をハッシュで出力するメソッドを作る

各ユーザの日付と日ごとの合計時間をハッシュで出力する、microposts_periodメソッドを作ります。
仕組みは以下の通りです。

  • 各期間の始めと終わりを引数で指定する
  • Active Recordで指定した期間を使い、micropostを取得する
  • ループ処理で日ごとの動画視聴時間を合計する
  • 日付と日ごとの合計時間をハッシュに代入する
app/models/user.rb
class User < ApplicationRecord
# 中略
  def microposts_period(period)
    current = Time.current.beginning_of_day
    case period
    when "week"
      start_date = current.ago(6.days)
    when "month"
      start_date = current.ago(1.month - 1.day)
    when "year"
      start_date = current.ago(1.year - 1.day)
    else
      start_date = current.ago(6.days)
    end
    end_date = Time.current
    dates = {}
    (start_date.to_datetime...end_date.to_datetime).each do |date|
      microposts = self.microposts.where(created_at: date.beginning_of_day...date.end_of_day)
      sum_times = microposts.sum(:time)
      dates.store(date.to_date.to_s, sum_times)
    end
    return dates
  end
# 中略

(Active Recordを呼び出しすぎて非効率かもしれません。)
(より良い方法が分かる方、ご教授を願います。)

参考にさせていただきました↓
ActiveRecord入門
Rails で1日前とか1週間前とか
商品の合計金額のようにModelの数値属性の合計値を求める方法

期間内の合計時間(分、時間)を出力するメソッドを作る

ビューで期間内の合計時間を表示したいので、そのためのsum_times、to_hourメソッドを作ります。

app/helpers/users_helper.rb
module UsersHelper
  def sum_times
    @chart.values.inject(:+) unless @chart.nil?
  end

  def to_hour
    hour = sum_times / 60.0
    hour.round(2)
  end
end

以上で必要なメソッドが揃いました。

コントローラを編集する

user_pathにグラフを描画するので、usersコントローラのshowアクションを編集します。
具体的には2つのインスタンス変数、periodとchartを追加します。

periodはmicroposts_periodメソッドの引数に入れる変数です。
chartはChartkickを描画するハッシュの変数です。

app/controllers/users_controller.rb
class UsersController < ApplicationController
# 中略
  def show
    # 中略
    @period = params[:period]
    @chart = @user.microposts_period(@period)
  end
# 中略

変数periodの値にはパラメータを使います。
このパラメータはビューから飛ばす予定のクエリパラメータです。
というわけで、次からはビューを作ります。

ビューを作る

パーシャルを用いてビューを作りましょう。

bash
$ touch app/views/layouts/_figure.html.erb
app/views/layouts/_figure.html.erb
<div class="container figure-container">
  <h1 class="title">グラフ</h1>
  <div class="figure-sum-and-button row">
    <span class="figure-sum-text col-sm">
      <div>合計:<em class="figure-sum"><%= sum_times %></em></div>
      <div><em class="figure-sum"><%= to_hour %></em> 時間)</div>
    </span>
    <span class="dropdown col-sm">
      <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
        <%= @user.microposts_period(@period).keys.first %> - <%= @user.microposts_period(@period).keys.last %>
      </button>
      <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
        <%= link_to "週間", user_path(@user, period: "week"), class: "dropdown-item" %>
        <%= link_to "月間", user_path(@user, period: "month"), class: "dropdown-item" %>
        <%= link_to "年間", user_path(@user, period: "year"), class: "dropdown-item" %>
      </div>
    </span>
  </div>
  <%= line_chart @chart %>
</div>

レイアウトにはBootstrap4を使用しています。
重要なのは、ドロップダウン内のリンクによるクエリパラメータです。

<%= link_to "週間", user_path(@user, period: "week"), class: "dropdown-item" %>
<%= link_to "月間", user_path(@user, period: "month"), class: "dropdown-item" %>
<%= link_to "年間", user_path(@user, period: "year"), class: "dropdown-item" %>

これにより、params[period]の形で値を取得できるようになります。
(showアクションのインスタンス変数に使用しましたね。)

ドロップダウンから期間を選択すると、グラフが変化します。
lantern-dropdown.png
lantern_month.png

計測時間をリセットする画面を編集する

計測時間のリセットする画面のひな形は、#11で作成済みです。
削除系なので、チェックを入れるとボタンが押せるようにします。
その実装に、jQureyを使います。

チェックオフ時:変更ボタンが押せない
lantern_check_off.png
チェックオン時:変更ボタンが押せる
lantern_ckeck_on.png

コードは次のようになります。

app/views/users/edit.html.erb
<% provide(:title, 'プロフィール編集') %>
<div class="container edit-container">
  <div class="edit-titles">
    <%= image_tag 'profile.png', class: 'edit-img' %>
    <h1 class="title edit-title">プロフィール編集</h1>
  </div>
  <div class="card-deck">
    <div class="col-md-6">
      <section class="card">
        <h2 class="card-header title form-title edit-user-card-title">アカウント情報変更</h2>
        <%= form_with(model: @user, url: user_path(@user), local: true) do |form| %>
        <div class="card-body">
          <%= render 'shared/error_messages', object: form.object %>

          <div class="form-group">
            <%= form.text_field :name, class: 'form-control', placeholder: "名前" %>
          </div>
          <div class="form-group">
            <%= form.email_field :email, class: 'form-control', placeholder: "メールアドレス" %>
          </div>
          <div class="form-group">
            <%= form.password_field :password, class: 'form-control', placeholder: "パスワード" %>
          </div>
          <div class="form-group">
            <%= form.password_field :password_confirmation, class: 'form-control', placeholder: "パスワード(再入力)" %>
          </div>
          <div class="form-group">
            <%= form.submit "変更", class: 'btn btn-info btn-lg form-submit' %>
          </div>
        </div>
        <% end %>
      </section>
    </div>

    <div class="col-md-6">
      <section class="card">
        <h2 class="card-header title form-title edit-user-card-title">記録時間リセット</h2>
        <%= form_with(scope: :microposts, url: user_path(@user), method: :delete, data: { confirm: "本当に削除しますか?" },  local: true) do |form| %>
        <div class="card-body">
          <div class="form-group form-group-reset_time">
            <%= form.check_box :reset_time, class: 'form-check-input' %>
            <%= form.label :reset_time, '記録時間をリセットする(時間がかかる場合があります)', class: 'form-check-label' %>
          </div>
          <div class="form-group">
            <%= form.submit "変更", class: 'btn btn-info btn-lg form-submit', id: 'microposts_reset_time_submit' %>
          </div>
        </div>
        <% end %>
      </section>
    </div>
  </div>
</div>
app/controllers/users_controller.rb
class UsersController < ApplicationController
# 中略
  def destroy
    @user = User.find(params[:id])
    if params[:microposts][:reset_time] == '1'
      @user.microposts.destroy_all
      flash[:success] = '記録時間とメモをリセットしました'
      redirect_to edit_user_path(@user)
    end
  end
# 中略
app/assets/javascripts/application.js
$(function() {
  $('#microposts_reset_time_submit').attr('disabled', true);

  $('#microposts_reset_time').click(function() {
    if ( $(this).is(':checked') ){
      $('#microposts_reset_time_submit').attr('disabled', false);
    } else {
      $('#microposts_reset_time_submit').attr('disabled', true);
    }
  });
});

というわけでアプリの全機能が整いました。

参考になりました↓
【Rails入門】データを削除する方法(destroy/delete)を確認しよう
jQuery:チェックされたらor入力されたら送信可能にするボタンを実装する方法

テストを書く

グラフ関係のテストにはModel/Request Specを使います。
プロフィール編集画面のテストにはSystem Specを使います。
(足りないテストは準備次第、追加します。)

spec/systems/edit_user_spec.rb
require 'rails_helper'

RSpec.describe "EditUser", type: :system do

  let(:user) { create(:user) }

  def valid_information(remember_me = 0)
    fill_in 'メールアドレス', with: user.email
    fill_in 'パスワード', with: 'password'
    check 'session_remember_me' if remember_me == 1
    find(".form-submit").click
  end

  describe "GET /users/:id/edit" do
    it "can press submit button when check button is pressed" do
      visit login_path
      valid_information
      visit edit_user_path(user)
      check 'microposts_reset_time'
      click_on 'microposts_reset_time_submit'
      page.driver.browser.switch_to.alert.accept
      expect(page).to have_selector '.alert-success'
    end
  end
end

参考になりました↓
Capybaraチートシート
【Rails】Selenium/RSpecでconfirmダイアログのテストをする

補足:テストごとにデータベースを初期化する

データベースのデータが残ることで、テストが失敗する場合があります。
テストごとにデータベースを初期化するよう、Database Cleanerを使います。
いつも通りGemを加え、設定をします。

Gemfile
group :development, :test do
  # 中略
+ gem 'database_cleaner'
end
spec/spec_helper.rb
require 'database_cleaner'

RSpec.configure do |config|
  # 中略 
  config.before(:suite) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

参考になりました↓
RSpecでテストDBに突っ込んだデータを消したい

長らく続いた第一部は、以上です。
次回、第二部ではAWSを学習します。


前回:#15 投稿機能, Active Storage編
次回:#17 VPC環境構築編

10
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
11