Node.js+D3.js+Socket.io+StreamingAPIによる実況可視化ツールを作る

# 目的
　[前回](http://qiita.com/Suibari_cha/items/50f2860b2d5dc2b0c9ce)、**Twitter実況の可視化**を行いたいと展望で述べました。
　実況内容を集計するところまではできましたが、集計結果の内容は膨大であり、内容も変動するため、**実況から「今何がホットなのか」を提示するという目的は未達**だったというのが経緯です。
　これについて進展がありましたので報告します。

# 結論
　**Twitter野球実況の形態素解析結果を可視化（ワードクラウド化）**しました。
　これにより以下の恩恵が得られました。

1. **人間が一目で「今何がホットなのか」を判別**できる
2. 各球団の色でワードクラウドを着色していることにより、「**どの球団で**何がホットなのか」も知ることができる

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">「D3.js + d3-cloud.js + 形態素解析」による、野球HOTワード分析。 <a href="https://t.co/JNZwXY5qeh">pic.twitter.com/JNZwXY5qeh</a></p>&mdash; すいばり@&#39;16年度9試合2勝2敗 (@Suibari_cha) <a href="https://twitter.com/Suibari_cha/status/834759198873313280">2017年2月23日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

# 内容
## コード
　d3-cloudによる描画処理部（フロントエンド）のみ記載します。

```client.js
function wordCloud(selector) {
  const W = 800; // 描画範囲サイズwidth
  const H = 500; // 描画範囲サイズheight
  const P = 4;   // ワードクラウド文字間のpadding
  
  // wordCloud関数のコンストラクタ（描画初期設定）
  var svg = d3.select(selector).append("svg")
    .attr("width", W)
    .attr("height", H)
    .append("g")
    .attr("transform", "translate("+W/2+","+H/2+")");
    
  
  // 描画(draw)関数
  function draw(words) {
    var cloud = svg.selectAll("g text")
                   .data(words, function(d) { return d.word; })
    
    // ワード出現時＆ワード存在時の処理
    cloud.enter()
      .append("text")
      .style("fill", function(d) { return d.color; })
      .attr("text-anchor", "middle")
      .attr('font-size', 1)
    .merge(cloud)
      .transition()
        .duration(600)
        .style("font-size", function(d) { return d.size + "px"; })
        .attr("transform", function(d) {
          return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
        })
        .style("fill-opacity", 1)
        .text(function(d) { return d.word; });
        
    // ワード消去時処理
    cloud.exit()
      .transition()
        .duration(200)
        .style('fill-opacity', 1e-6)
        .attr('font-size', 1)
        .remove();
  }
  
  
  // 再描画時に呼び出す関数
  return {
    update: function(words) {
      var random = d3.randomIrwinHall(2);
      
      d3.layout.cloud().size([W, H])
        .words(words)
        .padding(P)
        .rotate(function() {
          //return Math.round(1-random())*90; //文字をランダム回転させたい場合
          return 0; //回転させない場合
        })
        .text(function(d) { return d.word; })
        .fontSize(function(d) { return d.size; })
        .on("end", draw)
        .start();
    }
  }

}

// 与えられたデータ（[{},{},{},...]）を描画可能なオブジェクトに成形する
function getWords(data) {
  var countMax   = d3.max(data, function(d){return d.count});
  var sizeScale  = d3.scaleLog().domain([1, countMax]).range([10, 50]); //ログスケール
  //var sizeScale  = d3.scaleLinear().domain([0, countMax]).range([10, 100]); //リニアスケール
  // 各チームの色を指定する関数
  var colorScale = function(t){
    switch(t) {
      case "baystars":  return d3.color("dodgerblue");
      case "giants":    return d3.color("orange");
      case "tigers":    return d3.color("yellow");
      case "swallows":  return d3.color("limegreen");
      case "dragons":   return d3.color("darkblue");
      case "carp":      return d3.color("red");
      case "hawks":     return d3.color("gold");
      case "lions":     return d3.color("aqua");
      case "buffaloes": return d3.color("darkgoldenrod");
      case "eagles":    return d3.color("firebrick");
      case "fighters":  return d3.color("steelblue");
      case "marines":   return d3.color("black");
      default:          return d3.color("gray");
    }
  };
  return data.map( function(d) {
    return {
      word:  d.word, 
      size:  sizeScale(d.count),
      color: colorScale(d.team)
    }
  })
}

function showNewWords(vis, data) {
  vis.update(getWords(data))
}

// -------------------------
// ワードクラウドのインスタンス生成
var myWordCloud = wordCloud('#main');

// Socket.io経由でサーバのレスポンスの度に以下実行
var ioSocket = io.connect("http://192.168.33.10:8080");
ioSocket.on('result_analyze', function(data) {
  showNewWords(myWordCloud, data);
});
```

## MeCabの新語辞書導入
　前回、野球選手名が適切に分解されない問題があり、**今回は[MeCabの新語辞書"mecab-ipadic-NEologd"](https://github.com/neologd/mecab-ipadic-neologd/blob/master/README.ja.md)を導入**しています。

　著名な選手ですと、**明らかに適切に分解されるよう**になりましたが、新戦力（ルーキーや新外国人など）ですと流石に分解されません。

### メモリ不足によるインストールエラー
　ちなみに、辞書インストール時に以下ログが出て**メモリ不足**となり、インストールが中止される不具合がありました。

```C++
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
Aborted (core dumped)
```

　調べると全く同じ現象について書かれた以下の記事があり、**一発でした。**

> 以下のようにvb.customizeを追記した（config.vm.providerも）。
> 
> ```vim
> Vagrant.configure(2) do |config|
>     config.vm.box = "ubuntu1404"
>     config.vm.provider :virtualbox do |vb|
>         vb.customize ["modifyvm", :id, "--memory", 1024]
>     end
> end
> ```
> *[空想ブログ: "Ubuntu14.04でmecabの辞書にWikipediaとはてな単語を追加"](https://runble1.com/ubuntu-mecab-dictionary-add/#2Vagrant)より抜粋*

# 展望
* **D3.js周りの設計はこの記事までとします**。今はD3.jsの利用方法の習得よりも、JavaScriptの基礎知識の習得を優先したいためです。「ドキュメントやFAQがほぼ英語（Qiita記事は230件しかない）」、「多機能すぎてネイティブJavaScriptとは別物」である点が習得を難しくさせていると感じます。
* 「野球選手用名解析用ユーザ辞書」の生成と導入を検討。→[導入しました](http://qiita.com/Suibari_cha/items/aeeb3fb07b94207c1ff3)
* **[「12球団ごとの毎秒ツイート数」をサーバサイドで計算させている](http://qiita.com/Suibari_cha/items/2589004e85999c6c5ed1)ので、それをDOMに反映する。**
* **阪神タイガースの文字色（yellow）が背景と同化**してしまっている。。
* **球団名などはワードクラウド表示させない**。例えば、ワードクラウド上の赤は広島カープのHOTワードを表しているが、赤で「広島」や「カープ」が大きく表示されるのは当たり前。既知の情報は除外する方が、情報として有用。

# 所見
* **D3.jsが異常に難しい。**コピーしたコードを実行するだけでかなりの時間を浪費してしまいました（主に**D3のバージョンがv3→v4間で大幅にAPIが変わった**点に苦しめられました。既存のドキュメントで対応できない）。
* 今回の目的は「情報を分かりやすく伝える」ことであり、「そのためのツールを使う」ことに時間がかかりすぎる（手段が目的と化す）のは避けたい。

# 参考
*1. [GitHub: "D3 API Reference"](https://github.com/d3/d3/blob/master/API.md) 
2. [GitHub: "d3-cloud API Reference"](https://github.com/jasondavies/d3-cloud#api-reference)
3. [GUNMA GIS GEEK: "【D3.js】「全ツイート履歴」からWord cloudを作ってみた。"](https://shimz.me/blog/d3-js/2711)
4. [d3.jsな日々: "スケーリング：domainとrange"](http://d3js.hatenadiary.jp/entry/2013/04/22/074725)
5. [About Blocks: "Animated d3 word cloud"](http://bl.ocks.org/jwhitfieldseed/9697914)*