Ruby+d3.js+garb(Google-analytics-API)でユーザーの経路(パス)を描画

d3.js Advent Calendar 2014
http://qiita.com/advent-calendar/2014/d3

この記事に影響を受けて、Rails4+d3.js+garb(Google-analytics-API)でユーザー経路(パス)描画を実装してみました。
http://maulik-kamdar.com/2012/12/visualizing-paths-using-google-analytics/

Google Analyticsにあるフローレポートを補完する位置付けのグラフになります。
https://support.google.com/analytics/answer/2519986?hl=ja

GoogleAnalyticsにあるユーザーの流れを表すフローレポート（上記URLに含まれる画像をもってきました）

![FlowVizAnatomy.png](https://qiita-image-store.s3.amazonaws.com/0/13627/774a4086-2f6a-84a2-5002-2eddf3ff575a.png)

下画像が今回の記事で出力されるグラフの画像。青丸がnodeでページ。線がページからページへと移動したユーザーの多さです。

![dev.seoanalytics.jp-3000_home-index.png](https://qiita-image-store.s3.amazonaws.com/0/13627/694a7ef4-8c4f-40ac-5781-65354fed6684.png)


## 依存するライブラリ

### d3.js
http://d3js.org/

可視化ライブラリで今回はForce-Directed Graphを利用しています。
http://bl.ocks.org/mbostock/4062045

### garb
A Ruby wrapper for the Google Analytics API
https://github.com/Sija/garb

GoogleAnalyticsのレポートAPIをRubyから簡単に使うためのgemです。

## 本題

### GoogleAnalyticsAPIの認証

ユーザーとパスワードを利用するか、OAuthを利用する方法で認証を行います。以下はOAuthを使って認証するサンプルコードです。

```rb
client = OAuth2::Client.new(client_id, client_secret, :site => 'http://dev.xxxxxxxxxxx.jp:3000/')
session = Garb::Session.new
session.access_token = OAuth2::AccessToken.new(client, google_authentication.value)
profile = Garb::Management::Profile.all(session).find{|v| v.web_property_id == "UA-xxxxxx-x"} # 
```

サンプルで動かすにはユーザーとパスワードを使うのが良いと思います。

```rb
Garb::Session.api_key = api_key # required for 2-step authentication
Garb::Session.login(username, password)
```

今回はウェブアプリを作ってる途中で、Google APIs Console(https://code.google.com/apis/console/) への登録や、Googleとの認証に関する部分が既に実装してあるのでOAuthを使って認証しました。

### GoogleAnalyticsAPIからデータを取得

grabを使うと以下のようにレポートのルールを分かりやすく定義できます。PV数を前ページと次に移動したページを軸に取得します。

```rb
class GaPageviews
  extend Garb::Model
  metrics :pageviews
  dimensions :previousPagePath, :nextPagePath
end
```

このクラスを使って、日付を2014/10/1～2014/10/30と指定した結果を取得します。

```rb
results = GaPageviews.results(profile, {
  :start_date => Date.new(2014, 10, 1),
  :end_date => Date.new(2014, 10, 30),
  :filters => {:page_path.substring => '/venue'},
  :sort => :pageviews.desc,
})
```

### 取得したデータの加工

resultsの内容は以下のようにOpenStructになっていて、ここからPV数と前ページ、次ページが取れます。

```erb
#<OpenStruct previous_page_path="(entrance)", next_page_path="/venue/17", pageviews="532">
#<OpenStruct previous_page_path="(entrance)", next_page_path="/venue/45", pageviews="522">
#<OpenStruct previous_page_path="(entrance)", next_page_path="/venue/42", pageviews="510">
```

次にこの値をd3.jsのForce-Directed Graphで表示するための、ノード一覧とリンク一覧にデータを変換して行きます。

```rb
keys = {}
nodes = {}
links = {}

results.each{|v|
  [v.previous_page_path, v.next_page_path].each{|path|
    keys[path] ||= nodes.size
    nodes[path] ||= {
      number: nodes.size,
      name: path,
      group: 1,
      radius: 26,
      type: "uri",
      description: path,
      value: 0,
    }
    nodes[path][:value] += v.pageviews.to_i # ここがノードの丸の大きさ
  }
}

results.each{|v|
  key = (v.previous_page_path + "_x_" + v.next_page_path)
  links[key] ||= {
    name: "#{v.previous_page_path} to #{v.next_page_path}",
    source: keys[v.previous_page_path],
    target: keys[v.next_page_path],
    source_origin: v.previous_page_path,
    target_origin: v.next_page_path,
    value: 0
  }
  links[key][:value] += v.pageviews.to_i # ここがラインの太さ
}

links.keys.each{|key|
  if links[key][:value] < 50 # PVが50に満たないのを切り捨てる
    links.delete(key)
  end
}

# links(線）があるノードを集めて、
active_node_paths = []
links.values.each{|link|
  active_node_paths << link[:source_origin]
  active_node_paths << link[:target_origin]
}

# links(線）がないノードを削除します。
(nodes.keys - active_node_paths.uniq).each{|path|
  nodes.delete(path)
}
```

keysの内容はこのようにパスとlinksのsourceとtarget（リンクのつながり）で使う数字が入ります。

```js
["(entrance)", 0]
["/venue/17", 1]
["/venue/45", 2]
["/venue/42", 3]
```

nodesはこのようになります。

```js
["(entrance)", {:number=>0, :name=>"(entrance)", :group=>1, :radius=>26, :type=>"uri", :description=>"(entrance)", :value=>8594}]
["/venue/17", {:number=>1, :name=>"/venue/17", :group=>1, :radius=>26, :type=>"uri", :description=>"/venue/17", :value=>1592}]
["/venue/45", {:number=>2, :name=>"/venue/45", :group=>1, :radius=>26, :type=>"uri", :description=>"/venue/45", :value=>1568}]
```

linksにはノードとノードを繋ぐための情報が入っていますね。

```js
["(entrance)_x_/venue/17", {:name=>"(entrance) to /venue/17", :source=>0, :target=>1, :source_origin=>"(entrance)", :target_origin=>"/venue/17", :value=>532}]
["(entrance)_x_/venue/45", {:name=>"(entrance) to /venue/45", :source=>0, :target=>2, :source_origin=>"(entrance)", :target_origin=>"/venue/45", :value=>522}]
["(entrance)_x_/venue/42", {:name=>"(entrance) to /venue/42", :source=>0, :target=>3, :source_origin=>"(entrance)", :target_origin=>"/venue/42", :value=>510}]
```

これをjavascriptから扱えるようにjsonに変換します。

```rb
data = {
  nodes: nodes.values,
  links: links.values,
}.to_json
```

### d3.jsでグラフの表示

さあd3に食べさせるデータの準備が出来ました。これでグラフを表示します。

```js+erb
var graph = JSON.parse('<%= data.html_safe %>');
var width = 2400;
var height = 1200;
var color = d3.scale.category20();
var force = d3.layout.force().charge(-360).linkDistance(160).size([width, height]);
var svg = d3.select("svg");

var drawGraph = function(graph) {

    force.nodes(graph.nodes).links(graph.links).start();
    var link = svg.selectAll(".link").data(graph.links).enter().append("line").attr("class", "link").style("stroke-width",
        function(d) {
            return Math.sqrt(d.value);
        });

    var gnodes = svg.selectAll('g.gnode').data(graph.nodes).enter().append('g').classed('gnode', true);

    var node = gnodes.append("circle").attr("class", "node").attr("r", function(d) {
        return Math.sqrt(d.value);
    }).style("fill", function(d) {
        return color(d.group);
    }).call(force.drag);

    var labels = gnodes.append("text").text(function(d) {
        return d.name;
    });

    force.on("tick", function() {
        link.attr("x1", function(d) {
            return d.source.x;
        }).attr("y1", function(d) {
            return d.source.y;
        }).attr("x2", function(d) {
            return d.target.x;
        }).attr("y2", function(d) {
            return d.target.y;
        });
        gnodes.attr("transform", function(d) {
            return 'translate(' + [d.x, d.y] + ')';
        });
    });
};
drawGraph(graph);

```

### SVGを表示するHTMLと簡単なCSS

描画範囲が大きくなりがちなのでスクロールできるようにしています。

```html
<div class="outer">
    <div class="inner">
        <svg></svg>
    </div>
</div>
```

cssはこんな感じ。

```css
.node {
  stroke: #fff;
  stroke-width: 1.5px;
}

.link {
  stroke: #999;
  stroke-opacity: .6;
}

text {
  font-size: 11px;
}

.outer {
  width: 960px;
  height: 600px;
  overflow: scroll;
}

.inner {
  width: 2400px;
  height: 1200px;
}

svg {
  display: block;
  width: 100%;
  height: 100%;
}
```

これで冒頭に載せたd3.jsを使ったグラフが描画されます。

![dev.seoanalytics.jp-3000_home-index.png](https://qiita-image-store.s3.amazonaws.com/0/13627/694a7ef4-8c4f-40ac-5781-65354fed6684.png)

フィルターの指定で特定のパス下やカテゴリを指定すると、ディレクトリ単位でのユーザーの動きが分かるので、他ページに回遊して会員登録に至るフローに乗っているのかな？とかユーザーの動きで見えてくる部分があると思います。