12/16/2013

d3.jsとAngular JS

だいぶ遅れてすみません。d3.js Advent Calendar 2013の16日目です。
d3は高機能な描画機能が売りの一つだと思いますが、一方で柔軟な表現が出来るがために、シンプルなチャートを描画するだけでも多くのコード量が必要になりますし、コードそのものも難解なものになりがちです。そこで記述を完結にするためにd3をベースにしたRickshaw、C3.jsといったチャート用のライブラリが増えてきています。
Rickshaw
http://code.shutterstock.com/rickshaw/
Rickshawでのチャート
C3.js
http://c3js.org/

上記は下記のようなJSの記述でチャートを生成しますが、
var chart = c3.generate({
    data: {
        rows: [
            ['data1', 'data2', 'data3'],
            [90, 120, 300],
            [40, 160, 240],
            [50, 200, 290],
            [120, 160, 230],
            [80, 130, 300],
            [90, 220, 320],
        ]
    }
});
一方でdangle.js
http://www.fullscale.co/dangle/

Radian
http://openbrainsrc.github.io/Radian/index.html
Angular JSのDirectiveを用いて、htmlを記述することで複雑なチャートも簡潔に記述ができるようになっています。Radianでの記述は以下のようになります。
<plot height=200 aspect=2 stroke-width=2
      x="[[seq(0,4*PI,101)]]"
      axis-x-label="Time"
      axis-y-label="sin(x) / cos(x)">
  <lines y="[[sin(x)]]" stroke="red"></lines>
  <lines y="[[cos(x)]]" stroke="blue"></lines>
</plot>
Radianのチャート
個人的にd3とAngularのDirectiveは相性が良いと思っています。自分のプロジェクトでもうまく使ってみたいなと思っていたところ、Angular.jsのMeetupでd3とAngularを扱った回があり、そのサンプルがちょうど良い分量だったので今回はそれを解説をしたいと思います。
こちらです。
https://github.com/lithiumtech/angular_and_d3
Step1ではAngularを使わずにゲージの表示を行っていますが、ステップを進む毎にAngularのintegrationが行われています。またゲージを表示するClassは既に作られており、それは改変せずにAngularからどのように呼び出すか?というチュートリアルになっています。(上記の細かいスッテップは省きます。)

1-1: Angularを使わないhtml
<body onload="initialize()">
<div>
     <span id="memoryGaugeContainer"></span>
     <span id="cpuGaugeContainer"></span>
     <span id="networkGaugeContainer"></span>
</div>
</body>
1-2: Angularを使うhtml
<body ng-controller="MyController">
<div>
    <gauge min="-50" max="50" value="values.p" label="Petrol"></gauge>
    <gauge min="-50" max="50" value="values.o" label="Oil"></gauge>
    <gauge min="-50" max="50" value="values.c" label="Coolant"></gauge>
</div>
<gauge>というタグを定義し、min、maxなどの独自のattributeを設定する。またbodyタグに ng-controller="MyController"属性を付与し、body以下をMyControllerのscopeとする。

2-1: Angularを使わないJS
var gauges = [];

function createGauge(name, label, min, max) {
  var config = {
    size: 250,
    label: label,
    min: undefined != min ? min : 0,
    max: undefined != max ? max : 100,
    minorTicks: 5
  }

  var range = config.max - config.min;
  config.yellowZones = [{
    from: config.min + range * 0.75,
    to: config.min + range * 0.9
  }];
  config.redZones = [{
    from: config.min + range * 0.9,
    to: config.max
  }];

  gauges[name] = new Gauge(d3.select("#" + name + "GaugeContainer")[0][0], config);
  gauges[name].render();
}

function createGauges() {
  createGauge("memory", "Memory");
  createGauge("cpu", "CPU");
  createGauge("network", "Network");
}

function updateGauges() {
  for (var key in gauges) {
    var value = getRandomValue(gauges[key])
    gauges[key].redraw(value);
  }
}

function getRandomValue(gauge) {
  var overflow = 0; //10;
  return gauge.config.min - overflow + (gauge.config.max - gauge.config.min + overflow * 2) * Math.random();
}

function initialize() {
  createGauges();
  setInterval(updateGauges, 5000);
} 
2-2: Angularを使うJS
angular.module('components', []).directive('gauge', function() {
  //componentsモジュールにgaugeディレクティブを追加します
  return {
    restrict: 'E', //Elementとしてdirectiveを使用する
    replace: true, //Eを使用するときは、HTML として不適当な要素名が記述される可能せがあるのでtrueが望ましい、trueにするとgauseタグが置き換わる
    scope: {
      label: "@", // interpolate(値、string)
      min: "=", // data bind
      max: "=", // data bind
      value: "=" // data bind
    },
    link: function(scope, element, attrs, ngModelCtrl) {
      //linkでは、scopeにあるモデルの変更を検知、またイベントに応じた処理を記述して、DOM や controller とのやり取りを行う
      //以下のコードは基本的にAngularを使わないJSと同じ
      var config = {
        size: 120,
        label: attrs.label,
        min: undefined != scope.min ? scope.min : 0,
        max: undefined != scope.max ? scope.max : 100,
        minorTicks: 5
      };
      var range = config.max - config.min;
      config.yellowZones = [{
        from: config.min + range * 0.75,
        to: config.min + range * 0.9
      }];
      config.redZones = [{
        from: config.min + range * 0.9,
        to: config.max
      }];

      scope.gauge = new Gauge(element[0], config);
      scope.gauge.render();
      scope.gauge.redraw(scope.value);
      //scopeに変更があったときに実行される処理
      scope.$watch('value', function() {
        if (scope.gauge)
          scope.gauge.redraw(scope.value);
      });
    }
  }
});
angular.moduleでgaugeディレクティブを追加します。returnオブジェクトの中にいくつかのパラメータを指定します。linkブロックはscopeにあるモデルの変更を検知、またイベントに応じた処理を記述して、DOM や controller とのやり取りを行います。

3: Jsdo.itのサンプル
このようにDirectiveを使うことで、html構文としてD3で作られる図を宣言することが出来、ロジックと図の宣言を明快に分けることができます。
コードの可読性も高まり、また再利用性も高まるのではないでしょうか?

参考:
AngularJS Directive なんてこわくない(その1)