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

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

[markdown]
* rails (5.1.4)
* chartkick (2.2.4)
* groupdate (3.2.0)

## Installation

chartkick を `bundle install` します。

> * [ankane/chartkick: Create beautiful JavaScript charts with one line of Ruby](https://github.com/ankane/chartkick)

“`ruby:Gemfile
gem ‘chartkick’
“`

グラフの描画に [Chart.js](http://www.chartjs.org/) を利用するように `Chart.bundle` を合わせて指定します。
他のグラフライブラリも選択できます。

“`javascript:app/assets/javascripts/application.js
//= require Chart.bundle
//= require chartkick
“`

## Configuration

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

> * [Chart.js | Open source HTML5 Charts for your website](http://www.chartjs.org/)

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

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

> * [ChartJS Tutorials #4 – Chart Options – YouTube](https://www.youtube.com/watch?v=AcoUu3bgKgM)
> * [ChartJS Tutorials #5 – Global Configuration Options – YouTube](https://www.youtube.com/watch?v=t8TgLqTzo5o)

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

## Usage

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

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

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

### Model

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

“`ruby: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]]` のような形式でデータを渡す必要があるようなので、 **配列を整えます**。 > * [ruby on rails – Chartkick, line_chart (not series) data with hash in array – Stack Overflow](https://stackoverflow.com/questions/34181537/chartkick-line-chart-not-series-data-with-hash-in-array)

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

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

> * [charts – Rails chartkick: want only integer values on axes. Use discrete or something else? – Stack Overflow](https://stackoverflow.com/questions/27590771/rails-chartkick-want-only-integer-values-on-axes-use-discrete-or-something-els)

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

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

> * [ruby – 配列の欠落したデータを補完したい – スタック・オーバーフロー](https://ja.stackoverflow.com/questions/8383/%E9%85%8D%E5%88%97%E3%81%AE%E6%AC%A0%E8%90%BD%E3%81%97%E3%81%9F%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E8%A3%9C%E5%AE%8C%E3%81%97%E3%81%9F%E3%81%84)

“`ruby
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` で呼び出し、インスタンス変数に入れます。

“`ruby: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/show` で `chart_ranking` ヘルパーを呼ぶ形にしておきます。 ```ruby:app/views/items/show.html.haml = ranking_chart ``` ### Helper Chartkick のオプションと Chart.js のオプションを指定します。 > * [Chartkick](https://www.chartkick.com/)
**軸タイトル** と **表示範囲** (1位から20位)をオプション指定します。
`discrete: true` オプションで、省略せずに **X軸のラベルをすべて表示** することができました。

“`ruby: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:` に指定していきます。

> * [Chart.js | Open source HTML5 Charts for your website](http://www.chartjs.org/)

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

> * [Chart.js reverse yAxes option not transferring over · Issue #296 · ankane/chartkick](https://github.com/ankane/chartkick/issues/296)

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

> * [Hide Vertical Grid Lines · Issue #2744 · chartjs/Chart.js](https://github.com/chartjs/Chart.js/issues/2744)

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

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

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

> * [ankane/chartkick: Say Goodbye To Timeouts](https://github.com/ankane/chartkick#say-goodbye-to-timeouts)

### Routes

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

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

### Helper

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

“`ruby: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 で値を渡すように変更します。

“`ruby: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](https://www.chartkick.com/) 公式で紹介されているチュートリアルが分かりやすいのでオススメです。 > * [Charts with Chartkick and Groupdate (Example) – GoRails](https://gorails.com/episodes/charts-with-chartkick-and-groupdate)
> * [Make Easy Graphs and Charts on Rails with Chartkick — SitePoint](https://www.sitepoint.com/make-easy-graphs-and-charts-on-rails-with-chartkick/)
> * [Practical Graphs on Rails: Chartkick in Practice — SitePoint](https://www.sitepoint.com/graphs-on-rails-chartkick-in-practice/)

## 補遺

* [Rails tips: Railsアプリに1行書くだけでチャートを作成できるchartkick/chartable gem(翻訳)](https://techracho.bpsinc.jp/hachi8833/2018_06_08/57502)

[/markdown]