[Ruby on Rails 5] Chartkick + Rails で連続していないデータをグラフ描画する

Active Record から引っ張ったデータを整形して扱います。
欲しい表示を得るまでに手間取ったので、導入と Tips のメモ。

  • rails (5.1.4)
  • chartkick (2.2.4)
  • groupdate (3.2.0)

Installation

chartkick を bundle install します。

Gemfile
gem 'chartkick'

グラフの描画に Chart.js を利用するように Chart.bundle を合わせて指定します。
他のグラフライブラリも選択できます。

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

Configuration

共通設定は config/initializers/chartkick.rb に設定できます。
Chart.js の設定は、library: 以下に書けるよう。
細かな制御が必要な場合は、ライブラリのガイドを参考にする必要があります。

config/initializers/chartkick.rb
Chartkick.options = {
  height: '400px',
  library: {
    layout: {
      padding: {
        left: 16,
        right: 16,
        top: 16,
        bottom: 16
      }
    }
  }
}

実際に調整すると、global 設定できる機能とできない機能があるようで悩みます。

また、バージョンによってオプション名が変わっているようです。

Usage

models/item に対応するデータを models/ranking にから引っ張り、ランキングチャートを描画します。
models/item, models/rankingscaffold 等で既にあるものとします。

  • 1 – 20 位にランクインしていると、date と rank に値が入ったレコードがある。
  • ランク外の場合、レコードはない。

models/rankingcode は特殊な値が入っているので、実際には別処理を挟んでいます。)

Model

models/ranking にチャート用のクラスメソッドを用意します。

app/models/ranking.rb
require 'date'
# == Schema Information
#
# Table name: rankings
#
#  id            :integer          not null, primary key
#  code          :string
#  rank          :integer
#  date          :date
#  created_at    :datetime         not null
#  updated_at    :datetime         not null
#
class Ranking < ApplicationRecord
  def self.chart(code)
    return if code.blank?
    data_array = select(:date, :rank).where(
      code: code
    ).collect { |i| [i[:date].to_s, i[:rank]] }
    return if data_array.blank?
    x_values = data_array.map(&:first)
    chart_data = complement_blank(data_array, x_values.min.to_s, x_values.max.to_s)
    chart = { 'name': code, 'data': chart_data }
  end
  def self.complement_blank(data_array, date_from, date_to)
    from_to = [date_from, date_to].map { |s| Date.parse(s) }
    Range
      .new(*from_to)
      .each_with_object(data_array.to_h) { |date, h| h[date.to_s] ||= '21' }
      .sort
  end
end

chartkick には [["2015-11-08T19:59:57.000+08:00", 4], ["2015-11-09T00:02:37.000+08:00", 3]] のような形式でデータを渡す必要があるようなので、 配列を整えます

.collect { |i| [i[:date].to_s, i[:rank]] }

集計期間の 日付レンジ をデータから求めます。

x_values = data_array.map(&:first)
# x_values.min.to_s, x_values.max.to_s

このままでは日付が連続していないため X 軸がキレイに並びません。
下記でグラフ用に データのない日付を配列に追加 しました。

  def self.complement_blank(data_array, date_from, date_to)
    from_to = [date_from, date_to].map { |s| Date.parse(s) }
    Range
      .new(*from_to)
      .each_with_object(data_array.to_h) { |date, h| h[date.to_s] ||= '21' }
      .sort
  end

Controller

前述のメソッドを controllers/items_controller で呼び出し、インスタンス変数に入れます。

app/controllers/items_controller.rb
class ItemsController < ApplicationController
  before_action :set_item, only: [:show, :edit, :update, :destroy]
  # GET /items/1
  def show
    @ranking_chart = Ranking.chart(@item.code)
  end

View

Chartkick で指定するオプションは、作り込むと長くなります。
views/items/showchart_ranking ヘルパーを呼ぶ形にしておきます。

app/views/items/show.html.haml
= ranking_chart

Helper

Chartkick のオプションと Chart.js のオプションを指定します。

  • Chartkick
    軸タイトル表示範囲 (1位から20位)をオプション指定します。
    discrete: true オプションで、省略せずに X軸のラベルをすべて表示 することができました。
app/helpers/rankings_helper.rb
module RankingsHelper
  def ranking_chart
    data = @ranking_chart
    library_options = {
      scales: {
        xAxes: [{
          gridLines: { drawOnChartArea: true }
        }],
        yAxes: [{
          ticks: { reverse: true }
        }]
      }
    }
    line_chart data,
               xtitle: 'Date', ytitle: 'Rank',
               min: 1, max: 20,
               discrete: true,
               library: library_options
  end
end

さらに Chart.js のオプションを library: に指定していきます。

ランキング用途なので Y軸を反転 させます。

縦軸の表示オプションdrawOnChartArea を利用する必要がありました。

概ねこのような流れで、必要なグラフを得ることができました。

JSON で非同期にグラフを描画する

グラフを非同期通信で描画するように変更します。

Routes

グラフのデータを ranking_chart_item_path GET /items/:id/ranking_chart(.:format) のようなパスで渡すように設定を加えます。

config/routes.rb
Rails.application.routes.draw do
  resources :items do
    member { get :ranking_chart }
  end

Helper

ヘルパーの参照先を変更します。

app/helpers/rankings_helper.rb
module RankingsHelper
  def ranking_chart
    library_options = {
      scales: {
        xAxes: [{
          gridLines: { drawOnChartArea: true }
        }],
        yAxes: [{
          ticks: { reverse: true }
        }]
      }
    }
    line_chart ranking_chart_item_path,
               xtitle: 'Date', ytitle: 'Rank',
               min: 1, max: 20,
               discrete: true,
               library: library_options
  end
end

Controller

インスタンス変数から、JSON で値を渡すように変更します。

app/controllers/items_controller.rb
class ItemsController < ApplicationController
  before_action :set_item, only: [:show, :edit, :update, :destroy, :ranking_chart]
  # GET /items/1
  def show
  end
  # GET /items/1/ranking_chart.json
  def ranking_chart
    result = Ranking.chart(@item.code)
    render json: result
  end

以上で完了です。

Tutorial

Chartkick 公式で紹介されているチュートリアルが分かりやすいのでオススメです。

補遺