RailsでD3.jsを使う

D3.jsはいろんなグラフを描けるJavaScriptのライブラリですね。柔軟性は高いけど、その分学習コストも高いとの噂。描画できるグラフサンプルはこちら。

Gallery · d3/d3 Wiki · GitHub

これをRails環境で利用してみます。環境です。

  • Rails 5.0.1
  • D3.js 4.4.1

こちらを参考にしています。

http://dotinstall.com/lessons/basic_svg

http://dotinstall.com/lessons/basic_d3js

D3.jsとRuby on Railsで棒グラフを表示してみる | Developers.IO

しかしながら、以下のスライドによりますと、昨年D3.jsのメジャーバージョンアップが行われ、web上に見つけられる情報はほとんどがVer3のものになるため、大分古い情報になってしまったらしいです。ガーン。

細かすぎて伝わらないD3 ver.4の話

Ver4を新規で学習するなら、以下のサイトが良いよとおすすめされていました(英語ですが)。

Read D3 Tips and Tricks v4.x | Leanpub

読んでると、確かにかなり丁寧に記載されており、チュートリアルには良さげな雰囲気です(英語ですが)。こちらのサイトの「Starting with a simple graph」の章を参考に、Rails上にグラフを作成してみようと思います。以下画面のグラフが出来上がりです。尚、「D3 Tips and Tricks v4.x」のサイトではドネートできる仕組みがあるので、お世話になった方はちゃんと寄付しておきましょう。

f:id:goodbyegangster:20170220031751j:plain

以下の通り作っていきます。

コントローラの作成

「sample_graph」というコントローラに、indexとlistというアクションを作成しておきます。

$ rails generate controller sample_graph index list

D3.jsの参照設定

D3.jsのソースを全ページで参照できるようにするため、「application.html.erb」ファイルに以下を記載しておきます。

<%= javascript_include_tag src=“https://d3js.org/d3.v4.min.js” %>

$ vim app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>D3</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

    <%= javascript_include_tag src="https://d3js.org/d3.v4.min.js" %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

スタイルシートの記述

グラフで利用するスタイルシートの情報も、全ページで参照できるように「custom.scss」ファイルに記載しておきます。

# vim app/assets/stylesheets/custom.scss
.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 2px;
}

アクションの記述

コントローラに必要なアクションを記載しておきます。indexは表示用のアクションなのでそのままですが、listはデータを渡す用のアクションにします。今回はサンプルなので、グラフ描画するようのデータは直書きしてしまいます。

# vim app/controllers/sample_graph_controller.rb
class SampleGraphController < ApplicationController
  def index
  end

  def list
    data = [
      {date:"2017-01-01",close:"58"},
      {date:"2017-01-02",close:"53"},
      (中略)
      {date:"2017-02-05",close:"76"},
      {date:"2017-02-06",close:"69"}
    ]
    render :json => data
  end

end

D3.jsを利用するJavaScriptの記述

で、実際のD3.jsを利用するためのJavaScriptを「index.html.erb」に作成します。

# vim app/views/sample_graph/index.html.erb
<script>

// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 50},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

// parse the date / time
var parseTime = d3.timeParse("%Y-%m-%d");

// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);

// define the line
var valueline = d3.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });

// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform",
          "translate(" + margin.left + "," + margin.top + ")");

// Get the data
d3.json('list', function(error, data) {
 if (error) throw error;

  // format the data
  data.forEach(function(d) {
      d.date = parseTime(d.date);
      d.close = +d.close;
  });

  // Scale the range of the data
  x.domain(d3.extent(data, function(d) { return d.date; }));
  y.domain([0, d3.max(data, function(d) { return d.close; })]);

  // Add the valueline path.
  svg.append("path")
      .data([data])
      .attr("class", "line")
      .attr("d", valueline);

  // Add the X Axis
  svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));

  // Add the Y Axis
  svg.append("g")
      .call(d3.axisLeft(y));

});

</script>

あっという間に全てを忘れていく未来の僕のために、JavaScript部分を細かくメモしておきます。処理の流れは以下な感じ。

  1. グラフの大きさやマージンを設定
  2. 取り込むデータを解析するパーサーを設定
  3. 「scale」と「range」を設定
  4. 折れ線グラフ作成用のジェネレーター(関数)を定義
  5. svgを設定
  6. データの取込(データをフォーマット/「domain」を設定/svgにグラフ情報を与える)
1. グラフの大きさやマージンを設定

ブラウザ上で描画するためのグラフやそのマージンの大きさを定義しておきます。

// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 50},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;
2. 取り込むデータを解析するパーサーを設定

取り込んだデータを解析するようのパーサーを設定しておきます。今回は日付データを取り込むので、それを解析するようのパーサーを定義しています。

// parse the date / time
var parseTime = d3.timeParse("%Y-%m-%d");
3. 「scale」と「range」を設定

ここで「scale」と「range」、また後述ですが「domain」というD3.js独自の概念が出てきます。それらを理解するに、以下のサイトが役立つよと「D3 Tips and Tricks v4.x」内で紹介されています(英語ですが)。こちらのサイトも非常に丁寧な解説サイトとなっています(英語ですが)。

d3: scales, and color. | Jerome Cukier

それらの概念について、簡潔には以下の通り説明されています。

scales transform a number in a certain interval (called the domain) into a number in another interval (called the range).

僕のざっくりとした理解ですと、「domain」とは投入されるデータの範囲で、「range」とはブラウザ上で描画できるサイズ、「scale(scaling)」とはその両者の差異を正規化してくれる仕組みって感じです。たとえば、「ある20から80まであるデータ」を、「120ピクセルの幅」内で描画をしようとした場合、データ一つ分の幅を1ピクセルで描画しようとすると大幅な余白が生まれてしまいますね。そういった余白やまたは不足を、いい感じに整えてくれる機能がD3.jsには用意されており、つまりそれが「scale」となります。

尚、「scale」(つまりいい感じに整えてくれる)方式として、以下のような種類が用意されています。

  • linear scales (線形)
  • logarithmic scales (対数)
  • power scales (指数)

今回のグラフは折れ線グラフになるので、X軸とY軸の2つをいい感じに整える必要ありますから、以下のようになります。それぞれのデータにあったスケール関数を与えてあげて、rangeとして事前に定義しているブラウザ上で描画するグラフのサイズを渡しています。domainの情報は、実際にデータを渡された時に指定することになります。

// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
4. 折れ線グラフ作成用のジェネレーター(関数)を定義

また実際にグラフ作成のためのジェネレーター(関数)を定義しておく必要もあります。折れ線グラフを描画するには、d3.line()という関数を利用することになるので、その関数に必要な情報を与えておきます。

// define the line
var valueline = d3.line()
    .x(function(d) { return x(d.date); })
    .y(function(d) { return y(d.close); });
5. svgの領域を設定

で、svgです。svgとはXML形式を利用して画像を描画できるもので、D3.js利用時にはこの領域をHTML上に作成し、その部分の画像情報を入れ込む流れになります。svgに関しては、本投稿の冒頭で紹介したドットインストールの動画を見るのが、理解が早いと思います。

以下では、bodyタグ内に、幅と高さを指定したsvg領域を作成し、さらにその中に「g」というマージンを指定した領域を作成しています。

// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform",
          "translate(" + margin.left + "," + margin.top + ")");

実際に作成されたHTMLを見ると、理解し易いと思います。 f:id:goodbyegangster:20170220133713j:plain

6. データの取込(データをフォーマット/「domain」を設定/svgにグラフ情報を与える)

ようやくデータを取り込みます。

まずはjson形式で渡されるデータをD3に取り込んであげる部分です。取り込む処理はrequestと呼ばれており、json形式以外にもcsvやtextなど多数関数が用意されています。

// Get the data
d3.json('list', function(error, data) {
 if (error) throw error;

用意されているrequestのapi一覧。

d3/API.md at master · d3/d3 · GitHub

その次に、取り込まれたデータをフォーマットしている処理です。date項目のデータは前段で定義したパーサーに、close項目のデータは「+」をつけてあげて数値型に宣言し直しています。

  // format the data
  data.forEach(function(d) {
      d.date = parseTime(d.date);
      d.close = +d.close;
  });

そしてここで、domainの定義をしています。おさらいですが、domainとは投入されたデータの範囲を定義するものとなりますので、実際のデータの値が分からないと定義ができないのですね。なので、このタイミングで指定しています。d3.extentとはd3.jsの関数で、与えられたデータ(この場合はdate項目部分)の最大値と最小値を返してくれるものです。d3.maxは、あえて言わずとも、何をしているかイメージできますね。

  // Scale the range of the data
  x.domain(d3.extent(data, function(d) { return d.date; }));
  y.domain([0, d3.max(data, function(d) { return d.close; })]);

最後に、事前に作成していたsvg領域に、グラフ部分であるpath領域を(処理内で事前に定義したvaluelineが呼びだされていますね)、X軸部分であるg領域を、さらにY軸部分であるg領域を作成しています。これも上記のHTMLを再度確認すると、処理イメージが掴みやすいと思います。

  // Add the valueline path.
  svg.append("path")
      .data([data])
      .attr("class", "line")
      .attr("d", valueline);

  // Add the X Axis
  svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));

  // Add the Y Axis
  svg.append("g")
      .call(d3.axisLeft(y));

とまあ、以上です。なかなか大変ですね。

D3.jsに関しては、サンプルサイトや紹介したAPI一覧のページをさくっと見るだけで、かなりいろいろなことができるものだと感じられると思います。APIにデータを渡せばグラフデータを返してくれるようなサービスではなく、かなり凝ったグラフを作成したいといった場合には、やはり有効であると思います。