D3.js on AngularJSの実装パターン比較

## はじめに

以前, [AngularJSでD3.jsをラップしてみよう](http://qiita.com/Quramy/items/b701b824c2f42c683aa7)のエントリで, 「いかにしてAngularJSとD3.jsを統合するか」を記載した.

その後, 自分で幾つかAngularJSとD3.js両方を使ったApplicationを作ったり, http://alexandros.resin.io/angular-d3-svg/ のブログを読んだりした上で, 双方のライブラリ両方を使う際の知見が増えたので, これを機にもう一度整理してみようかと.

## ライブラリとしての比較

AngularJSとD3.js, 双方を触ってみて思うのは, 特にコアな部分について, 備えている機能が重複していたり, 同等機能への実装アプローチが異なるなぁ、ということ.


| #   | 比較項目              | Angular's way                 | D3's way                    |
|:----|----------------------|-------------------------------|-----------------------------|
| 1.  | View構築スタンス       | Declarative(宣言的)           | Functional(手続的)          |
| 2.  | データバインディング  | `ngModel` や `{{...}}`     | `d3.selectAll(〜).data(〜)`   |
| 3.  | Collection CRUD追跡  | `ngRepeat`                    | `enter()`, `exit()`         |
| 4.  | Layout機能           | 無                            | `d3.layout.force` 等       |
| 5.  | SVG utility          | 無                            | `d3.svg.arc` 等           |


特徴的なのは, 1.〜3.の部分だろう.
まぁ, 4.と5.は可視化に特化したライブラリであるD3.jsだけが備えているのは違和感無いし...

以下で, それぞれの機能毎に考え方を見ていく.

### View構築スタンス

まずはViewに対するスタンスから. 
「SVGの領域へ (x, y)=(100, 100)に半径20の円を描画する」を例に考えてみる.

#### Angular's way
宣言的. HTMLテンプレートを用いて, マークアップしていく.

```html
<svg>
  <circle cx="100" cy="100" r="20"></circle>
</svg>
```

#### D3's way
手続的. jQueryライクなDOM操作関数でDOMを組み立てて行く.

```js
var svg = d3.select('body').append('svg');
svg.append('circle')
  .attr('cx', 100)
  .attr('cy', 100)
  .attr('r', 20);
```

### データバインディング

#### Angular's way
`ngModel`や, `{{...}}`等のDirectiveをHTMLテンプレートに記述することで, ScopeのデータがViewへバインドされる.
裏側では, Directiveの実装により, `$scope.$watch`や`$scope.$watchCollection`が動作する.

#### D3's way

D3の場合, バインディングはCollectionに対して行う.

`d3.selectAll(セレクタ).data(コレクション)` とすることで, セレクタ対象のDOMとコレクションが1:1で紐付けられる.
こいつがあってこその"Data Driven Document"と言っても過言ではないくらい.

`data()`メソッドでCollectionとの紐付けを実施した後は, `attr`メソッド等をメソッドチェーンで呼び出すことで, データ値の加工 -> DOM属性値へのセット をクロージャで処理できる.

```js
d3.selectAll('circle').data([100, 200, 300])
  .attr('cx', function (d) { return d;})
  .attr('r', function (d) {return Math.sqrt(d);});
```

### Collection CRUD追跡
AngularJS, D3.js共にコレクションを扱うための仕組みがある.

特にどちらのライブラリも, 配列要素 <-> DOM要素 を1:1にマッピングする機構と, Collection側の要素数増減まで含めてサポートしている.

#### Angular's way
`ngRepeat` Directiveが, Collection

```js:main.js
function MainCtrl(){
  this.datum = [
    {cx: 100, cy: 50, r: 50},
    {cx: 100, cy: 150, r: 30},
    {cx: 100, cy: 250, r: 20}
  ];
}
```

```html:main.html
<div ng-controller="MainCtrl as main">
  <svg>
    <circle ng-repeat="data in main.datum"
      ng-attr-cx="{{data.cx}}"
      ng-attr-cy="{{data.cy}}"
      ng-attr-r="{{::data.r}}" >
    </circle>
  </svg>
</div>
```

`ngRepeat` は Collection(上記の場合, `main.datum`)の要素数が変動した場合, 増分(減分)のみを再描画するように実装されている.

なお, 上記のViewのサンプルで2点補足:

+ `ng-attr-*`  
`<circle>` 等, SVG要素中のattributeにScopeの値をバインディングする際, `cx={{...}}` とすると, 一見何の問題もなくRenderingされるが, コンソールに` Error: Invalid value for <circle> attribute cx=以下略` のようなエラーを吐くことがある.  
このエラーの原因は, [AngularJSのDirectiveガイド](https://docs.angularjs.org/guide/directive#-ngattr-attribute-bindings)にもある通り, SVGのDOM APIが厳密にバリデーションを行うため.  
回避するためには, `ng-attr-`をバインドしたい属性に付与すればよい.
+ `{{::〜}`  
AngularJS 1.3.xから導入された[One Time Binding](https://docs.angularjs.org/guide/expression#one-time-binding). 一回だけ評価すればよい式に用いる.


#### D3's way

```js
var datum = [
  {cx: 100, cy: 50, r: 50},
  {cx: 100, cy: 150, r: 30},
  {cx: 100, cy: 250, r: 20}
];

var render = function (datum) {
  var svg = d3.select('svg#viz'), selection = svg.selectAll('circle').data(datum);
  selection.enter()
    .append('circle')
    .attr('r', function(d){return d.r});

  selection
    .attr('cx', function(d){return d.cx})
    .attr('cy', function(d){return d.cy});

  selection.exit()
    .remove();

};

render(datum);
```

D3.jsの場合、`d3.selectAll(data)`に、以下の部分集合を取り扱う為のAPIが用意されている:

 + `enter()`: 現時点ではDOMに存在しないが, 描画されるべきCollection要素の部分集合
 + `exit()`: Collection要素には既に存在しないが, DOMツリーに取り残されているDOM要素集合

したがって, `enter()`の結果はDOMに `append`, `exit()`の結果はDOMから`remove()` という様に, 描画用の関数(上記の`render()`)を作成しておくことで, 
Collectionの状態を意識することなく, Collection変更発生時に都度都度`render()`を呼び出すことで, DOMとCollectionの状態を同期し続けることが出来る.

### Layout, SVG utility
まぁ, Angularに、そこまで可視化に特化した機能はないですよね...

D3のよいところは, さくっと使えるLayout(`d3.layout.chord`や`d3.layout.force`)が揃っていたり, 面倒なSVGの各種属性を計算するためのユーティリティ関数がビルトインされている点.

## 実装パターン

とまぁ, ここまででAngularJS, D3.jsの似ている点・異なる点を見てきた.

これらを踏まえた上で, 両者を同時に利用する際の実装パターンを考えてみる.

### D3を主体とする方法

可視化領域配下のView構築, データバインディング, コレクション制御ついて, D3.js側の機能を利用するパターン.

上記で, 「D3.js's way」としてきた実装方法を組み合わせていく.

書籍の[AngularJSリファレンス](http://www.amazon.co.jp/AngularJS%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9-%E6%B1%A0%E6%B7%BB-%E6%98%8E%E5%AE%8F/dp/4844336681), [D3 on AngularJS](https://leanpub.com/d3angularjs) に記載されている方法も, こちらのパターンに属している.

このパターンの特徴は下記のようになる.

+ 可視化領域に対してDirectiveを作成し, Directive配下のDOMをD3.jsのメソッドで手続的に組み立てる.
+ Directiveの引数として, 可視化対象のデータコレクションが格納されたScopeのプロパティを受け取る.
+ `$scope.$watchCollection` や `$scope.$watch(... , ... , true)` (deep watch) を用いて, `$scope`の値変動時に動作するListenerを登録.
+ 上記Listenerの内部で, `d3.selectAll(...).data(...)` の `enter()` や `exit()`を呼び出し, 要素の追加/削除のDOM操作をd3的に記述.

上記のパターンが有用なのは, 下記だろうか.

+ 実装したい可視化アプリがD3.jsのコードとして出来あがっており, AngularJSへの繋ぎ部分を作ってしまえば良いケース

裏を返すと, このパターンには以下のデメリットが伴う.

+ 可視化部分の実装変更にはDirective修正が必ず伴う.
+ DirectiveのLink関数(それも`$scope.$watch`に登録するListener)にコードの大半が集中し, Directiveのコードが巨大化しがち.
+ DOM操作をD3.jsに委譲するため, ユーザインタラクションに`ngClick`や`ngMouseover`が利用出来ない


### Angularを主体とする方法

もう一つのパターンは, View構築, バインディング, コレクション変動時のDOM制御全てをAngularJSベースで実装するパターン.

思想的には [Radian](http://openbrainsrc.github.io/Radian/) はこちらのパターンに近い(ExpressionがAngularJSのものとは異なるが).

このパターンの特徴は下記である:

+ 可視化領域内部も含め, DOMはAnguarJSのHTMLテンプレートで構成していく.
+ SVGの属性についても, `ng-attr-cx={{...}}` のようにし, HTMLに直接記載していく.
+ D3の利用は, Layout構築やSVG便利機能(d3.svg.line())に留める.

メリットは以下あたりか:

+ 可視化領域含め, HTMLテンプレートでViewが構成されるため, 変更が容易.
+ D3.jsのメソッドチェーンで頻発していたクロージャが減る分, コードはすっきりする.

Angular主体の方法で実装した場合のデメリットは下記か.

+ アニメーション(`ngAnimate`)との相性が微妙.  
D3主体の方法であれば, `.attr('width', functoin(){...})` に `transition()`を噛ますだけで, ニュルニュル幅を増やしたりが簡単であった. Angular主体の場合, `ngAnimate`モジュールを使うことになるのだが, CSS属性ベースのアニメーションが基本のため, SVG要素の属性をtweenさせるようなアニメーションには不向きな印象がある.

## 実装サンプル
[以前のエントリ](http://qiita.com/Quramy/items/b701b824c2f42c683aa7)でも取り上げたが, 下図のようなサンプルに対して, D3主体の実装例, Angular主体の実装例を見ていこうと思う.

![cap01.png](https://qiita-image-store.s3.amazonaws.com/0/8808/8c69e1ce-ed98-971f-9dcf-291252bfc9d8.png)

### 準備：可視化と関係の無い共通部分実装
データをいじるアプリ部分.

```js:app/main/main.controller.js
'use strict';

angular.module('angularD3SampleApp').controller('MainCtrl', function () {

  var main = this;

  main.sampleData= [
    {name:'Chrome', users: 500},
    {name:'Internet Explorer', users: 200},
    {name:'Safari', users: 300}
  ];

  main.newData = {name: '', users: 0};

  main.add = function(){
    main.sampleData.push(main.newData);
    main.newData = {name: '', users: 0};
  };

  main.remove = function($index){
    main.sampleData.splice($index, 1);
  };

});
```

```html:app/main/main.html
<div class="header">
  <h3>D3 Sample</h3>
</div>
<div class="row marketing" ng-controller="MainCtrl as main">
  <div class="col-md-12">
    <table class="table">
      <thead>
        <tr><th>#</th><th>name</th><th>users</th><th>action</th></tr>
      </thead>
      <tbody>
        <tr ng-repeat="data in main.sampleData"><!-- data iteration {{{ -->
          <td>{{$index + 1}}</td>
          <td>{{data.name}}</td>
          <td> <input type="number" size="5" class="form-control input-sm" ng-model="data.users"> </td>
          <td>
            <button class="btn btn-sm btn-danger" ng-click="main.remove($index);">
              <span class="glyphicon glyphicon-minus-sign"></span>
              remove
            </button>
          </td>
        </tr><!-- }}} end iteration -->
        <tr><!-- new data {{{ -->
          <td>new</td>
          <td> <input type="text" size="10" placeholder="input name" class="form-control input-sm" ng-model="main.newData.name"> </td>
          <td> <input type="number" size="5" class="form-control input-sm" ng-model="main.newData.users"> </td>
          <td>
            <button class="btn btn-success btn-sm" ng-click="main.add()">
              <span class="glyphicon glyphicon-plus-sign"></span>
              add
            </button>
          </td>
        </tr><!--}}} new data -->
      </tbody>
    </table>
  </div>
  <div class="col-md-12">
    <div>
      <!-- ★★ D3主体の場合 ★★-->
      <d3-bar data="main.sampleData" key="name" value-prop="users" label="name"></d3-bar>

      <!-- ★★ Angular主体の場合 ★★-->
      <div ng-include="'app/viz/barLayout.html'"></div>
    </div>
  </div>
</div>
```

上記の`<!-- ★★ ★★ -->`部分は, D3主体 or Angular主体のどちらか一方を残すべし.

### D3主体とした場合の可視化領域実装

[jsfiddleに動作コードあり](http://jsfiddle.net/Quramy/cudnzkms/)

`<svg>` 要素配下のDOM(SVG)の構築, Collectionとのバインディング, 要素の増減追跡すべてをD3のメソッドで実現する.

DOMに直接アクセスするため, Angularとの糊付け部分はDirectiveで実現する形となる.

```js:components/d3/d3Bar.directive.js
'use strict';

angular.module('angularD3SampleApp').directive('d3Bar', function (d3, $parse) {
  return{
    restrict: 'EAC',
    scope: {data: '=', key: '@', valueProp: '@', label: '@'},
    link: function (scope, element) {
      var svg = d3.select(element[0]).append('svg').style('width', '100%');
      var colorScale = d3.scale.category20();

      var watched = {};

      var getId = $parse(scope.key || 'id');
      var getValue = $parse(scope.valueProp || 'value');
      var getLabel = $parse(scope.label || 'name');

      scope.$watchCollection('data', function () {
        if(!scope.data){
          return;
        }

        var dataSet = svg.selectAll('g.data-group').data(scope.data, getId);

        var createdGroup = dataSet.enter()
        .append('g').classed('data-group', true)
        .each(function(d){
          var self = d3.select(this);
          watched[getId(d)] = scope.$watch(function () {
            return getValue(d);
          }, function(v){
            self.select('rect').attr('width', v);
          });
        });
        createdGroup.append('rect')
        .attr('x', 130)
        .attr('height', 18)
        .attr('fill', function (d) {
          return colorScale(d.name);
        });
        createdGroup.append('text').text(getLabel).attr('height', 15);

        dataSet.exit().each(function(d){
          var id = getId(d);
          watched[id]();
          delete watched[id];
        }).remove();

        dataSet.each(function (d, i) {
          var self = d3.select(this);
          self.select('rect').attr('y', i * 25);
          self.select('text').attr('y', i * 25 + 15);
        });
      });
    }
  };
});
```

上記の`d3Bar` Directiveについては, 以前のエントリでも説明した通りのため, 解説については, [こちら](http://qiita.com/Quramy/items/b701b824c2f42c683aa7#3-5)を参照されたし.

### Angular主体とした場合の可視化領域実装

[jsfiddleに動作コードあり](http://jsfiddle.net/Quramy/tw6gvn5k/)

上述の通り, Angular主体の方法では, 可視化部分もD3は使わずに, `ngRepeat`やAngularJSのバインディング(`{{...}}`)を用いてViewを構築していく:

```html:app/viz/barLayout.html
<svg ng-controller="BarLayoutCtrl as barLayout" style="width:100%;">
  <g ng-repeat="data in main.sampleData track by data.name">
    <text ng-attr-y="{{$index*25+15}}" height="15">
      {{::data.name}}
    </text>
    <rect
      x="130"
      ng-attr-y="{{$index*25}}"
      ng-attr-width="{{data.users}}"
      height="18"
      fill="{{::barLayout.fill(data.name)}}"
    >
    </rect>
  </g>
</svg>
```

この例のような, 単純な棒グラフであれば, D3の活躍する余地は殆どなく, 辛うじて棒の塗りつぶし色(`fill`属性)を決定する部分で, `d3.scale.category20`を使っている程度.

```js:app/viz/barLayout.controller.js
'use strict';

angular.module('angularD3SampleApp').controller('BarLayoutCtrl', function (d3) {
  var barLayout = this;
  var colorScale = d3.scale.category20();

  barLayout.fill = function (id) {
    return colorScale(id);
  };

});
```

### Layoutの利用例(ドーナツチャート)
[前回](http://qiita.com/Quramy/items/b701b824c2f42c683aa7#2-3)も, Layoutを利用する例として, ドーナツチャート(`d3.layout.pie`)を内部的に利用したDirectiveを示した.
今回も同じく, Angular主体の方法でドーナツチャートに置き換えたら, どのようになるかを示してみる.

...というより, 棒グラフの例だと, 「D3.js無くてもよくね？」という結論になってしまいそうなので, D3に存在するがAngularには存在しない「Layout, SVG utility」を利用するサンプルも例示することとする.

上記の`app/biz/barLayout.html`, `app/biz/barLayout.controller.js`を下記で置き換える：
([jsfiddleに動作コード有り](http://jsfiddle.net/Quramy/q5m6sa8p/))

```html:app/biz/pieLayout.html
<svg ng-controller="PieLayoutCtrl as pieLayout" style="width:100%; height:420px">
  <g transform="translate(200,200)">
    <path
      ng-repeat="part in pieLayout.parts track by part.data.name"
      ng-attr-d="{{pieLayout.arc(part)}}"
      fill="{{::pieLayout.fill(part.data.name)}}"
    >
    </path>
  </g>
</svg>
```

```js:app/biz/pieLaytou.controller.js
'use strict';

angular.module('angularD3SampleApp').controller('PieLayoutCtrl', function (d3, $scope) {
  var pieLayout = this;
  var colorScale = d3.scale.category20();
  var arc = d3.svg.arc().outerRadius(200).innerRadius(100);

  pieLayout.fill = function (id) {
    return colorScale(id);
  };

  pieLayout.arc = function (part) {
    return arc(part);
  };

  $scope.$watch('main.sampleData', function (datum) {
    pieLayout.parts = d3.layout.pie().value(function (data) {
      return data.users;
    }).sort(null)(datum);
  }, true);

});
```

ドーナツチャートの扇型部分のSVGは, `<path d="...">`で表されるが, このd属性をD3無しにAngularだけで計算するのは相当面倒くさいが,  `d3.svg.arc()`と`d3.layout.pie`をうまく使えば瞬殺である.

### Controller分割
先の棒グラフの例でも意図的に分割していたのだけれど, Angular主体の方法では, 可視化部分がScopeとなるように子Controllerを用意していた.
これは、「Scopeのデータに従属する可視化要素(=座標情報, 色, etc...)を取り仕切るViewModel」を別途, メインのScopeの下位に用意しておきたかったらだ.
今回のサンプルで言うなら,  `MainCtrl`は「棒グラフの個別の色」に関心を抱くべきではないが, 色は棒グラフのデータ名称から決定している. 

アプリケーション自体が必ずしも, 「データの編集部分」 + 「データの可視化部分」で構成されるとは限らないので, 常に編集部分を親, 可視化部分を子としたController分割が成り立つとは言えないが, AngularJS主体の方法で実装した場合に, Fat Controllerとならないためにも、心がけておきたいポイントではないだろうか.

## まとめ

+ AngularJSアプリケーションでD3.jsを用いた可視化を行う場合, 以下の2通りがある
 1. 可視化領域をDirectiveとし, Directive内部ではD3.jsメインで実装する
 2. CollectionとのデータバインディングはAngularに一本化し, D3.jsはSVGのユーティリティとしてのみ用いる

結局, 1. と 2., どちらがいいのか？ という疑問に対してであるが, 僕は個人的には 2.の Angular主体とする方法が良いと思っている.

「ViewをHTML + α で宣言的に構築できる」はAngularJSでの気に入っている点の一つだし, View構築方法が2通りで混在していると, アプリケーション全体の統一感というか, 見通しが悪くなるのは避けたいと考えているからだ.

## 参考

+ http://alexandros.resin.io/angular-d3-svg/ 1年近く前 & 英語 ですが, 導入部の説明は読み物としてもおもしろいので是非