NVD3を使って、AngularJSで作ったハイブリッドアプリのグラフを統合しよう

ハイブリッドアプリにグラフを表示する方法はいくつもあります。ハイブリッドアプリは基本的にHTML,Javascript,CSSで動くウェブアプリケーションなので、JavaScriptのグラフライブラリならなんでも使うことができますし、バックエンドでグラフを作成して静止画として表示することもできます。

この記事では、D3というデータビジュアリゼーションライブラリに基づき、再利用することのできるNVD3に注目します。それでは、このNVD3がJavaScriptのみでどうやってグラフを作成し、AngularJSのディレクティブにどのようにしてラッピングされているのかを見>ていきましょう。

また、人気のJavaScriptライブラリは他には以下のようなものあります。

  • D3.js - D3はData-Driven Documentsの意味です。SVGを用いた、大変強力なデータビジュアリゼーションライブラリです。低級ライブラリなので簡単なグラフを作成するのにも大変複雑な作業を要しますが、完成したものは驚くべき出来栄えとなります。
  • Chart.js - D3.jsよりも簡単に使えますが、それでもまだかなりカスタマイズ性が高いものです。SVGではなくcanvasを用いており、そのため古いブラウザやウェブビューをサポートする必要がある際には非常に良い選択肢と言えるでしょう。
  • n3-charts - AngularJSとD3.jsの上位に作られており、このライブラリを使えばAngularJSでグラフを表示することはとても簡単になります。

私たちは近頃、OnsenUIではグラフ作成ライブラリをサポートすべきか、もしそうならどのライブラリを選ぶべきかということを議論してきました。そこで、簡単さとD3.jsの強力なデータ可視化が組み合わさったNVD3が候補の一つに挙がりました。またこの記事で紹介するように、NVD3はAngularJSアプリケーションに簡単に組み込むことができます。

NVD3の長所

  • レスポンシブ - NVD3を使って作られたグラフは、スクリーンのサイズ・向きごとにそれぞれに適した形で描画されます。
  • ダイナミック - データに変更があったときには簡単にグラフをリフレッシュできます。

下のイメージは、NVD3とOnsenUIを用いて作った、世界各国の人口を表示する簡単なアプリです。

人口データはWorld Population APIから取得しています。

NVD3でグラフを作成する

まず初めにHTMLファイルを新規作成し、以下のリソースをロードします。

  • d3.js
  • nv.d3.js
  • nv.d3.css

これらは全てCDNで利用可能なので、以下のようにすることができます


<html>
  <head>
    <title>My chart</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.1/nv.d3.min.js"></script>
    <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.1/nv.d3.min.css">
    <script src="chart.js"></script>
  </head>
  <body>
    <!-- D3 will use this "svg" tag to render the chart! -->
    <svg></svg>
  </body>
</html>

それでは最初のグラフを作成して、スクリーンに描画してみましょう。chart.jsと名前を付けたファイルを作成し、以下のコードを追加してみましょう。


var data = []; // We'll leave this empty for now.
nv.addGraph(function() {
  // Creates a new Line chart.
  var chart = nv.models.lineChart()
    .showLegend(false)
    .showYAxis(true)
    .showXAxis(true);
  // Tells NVD3 to display values with two decimal places.
  chart.xAxis
    .axisLabel('x')
    .tickFormat(d3.format('.2f'));
  chart.yAxis
    .axisLabel('y')
    .tickFormat(d3.format('.2f'));
  // Select the "svg" tag and render the chart.
  d3.select('svg')
    .datum(data)
    .call(chart);
  // This will render the chart every time the
  // window is resized, so it will always fit the
  // screen.
  nv.utils.windowResize(function() {
    chart.update()
  });
  return chart;
});

ブラウザでこのコードを実行してみると、まだデータを追加していないので"No data Available"と表示されると思います。データは以下のようなフォーマットで定義されます。


var data = [{
  key: 'Label',
  values: [{x: 0, y: 0}, {x: 1, y: 1}, ...]
}];

簡単なデータを作成し、グラフに渡してみましょう。


var data = []
// Generate y = ln(x) for x = [0..e].
for (var i = 1; i <= Math.E; i += 0.01) {
  data.push({x: i, y: Math.log(i)});
}

保存して再読み込みすると、ブラウザはこのようなグラフを表示するはずです。

See the Pen JdeOLd by Andreas Argelius (@argelius) on CodePen.

NVD3グラフをAngularJSのディレクティブでラッピングする

AngularJSの強みはデータバインディングです。AngularJSのディレクティブを作成し、そこでchartのデータをHTMLの属性にバインドしてみましょう。この章では<line-chart>コンポーネントを作成します。このコンポーネントは以下のようにして使用できます。


<line-chart data="data" height="200px" width="120px"></line-chart>

ユーザーがデータを変更できるように、<input>タグも追加します。最終的にはこのようになるはずです。


<div ng-app="app" ng-controller="MyController as main">
  <line-chart height="250px" data="main.data"></line-chart>
  <h3>Change the values: </h3>
  <p ng-repeat="datum in main.data[0].values">
    <input type="number" ng-model="datum.y">
  </p>
</div>

とても短いコードですが、これで全て完了です!アプリを読み込んで、data配列をダミーデータで初期化しましょう。


angular.module('app', [])
  .controller('MyController', ['$scope', function($scope) {
    this.data = [{
      key: 'Data',
      values: [{
        x: 0,
        y: 0
      }, {
        x: 1,
        y: 1
      }, {
        x: 2,
        y: 4
      }, {
        x: 3,
        y: 9
      }, {
        x: 4,
        y: 16
      }, {
        x: 5,
        y: 25
      }]
    }];
  }]);

ここではまだディレクティブのコードを書いていないので、input要素だけが動いています。ディレクティブを作成するのは真正直な作業なので以前書いたJavaScriptのみのコードから拝借します。ここで、2つのサービス(d3とnv)を使いました。これは、それぞれのオブジェクトを返すだけのもの
です。


angular.module('app')
  .directive('lineChart', ['d3', 'nv', function(d3, nv) {
    return {
      restrict: 'E',
      scope: {
        // Bind the data to the directive scope.
        data: '=',
        // Allow the user to change the dimensions of the chart.
        height: '@',
        width: '@'
      },
      // The svg element is needed by D3.
      template: '<svg ng-attr-height="{{ height }}" ng-attr-width="{{ width }}"></svg>',
      link: function(scope, element) {
        var svg = element.find('svg'),
          chart;
        // This function is called when the data is changed.
        var update = function() {
          d3.select(svg[0])
            .datum(scope.data)
            .call(chart);
        };
        // Render the chart every time the data changes.
        // The data is serialized in order to easily check for changes.
        scope.$watch(function() { return angular.toJson(scope.data); }, function() {
          // The chart may not have been initialized at this point so we need
          // to account for that.
          if (chart) {
            update();
          }
        });
        // The chart can not be rendered at once, since the chart
        // creation is asynchronous.
        scope.$on('chartinit', update);
        nv.addGraph(function() {
          // This code is the same as the example before.
          chart = nv.models.lineChart()
            .showLegend(false)
            .showYAxis(true)
            .showXAxis(true);
          chart.xAxis
            .axisLabel('x')
            .tickFormat(d3.format('.2f'));
          chart.yAxis
            .axisLabel('y')
            .tickFormat(d3.format('.2f'));
          nv.utils.windowResize(function() {
            chart.update()
          });
          // Emit an event so we can know that the
          // chart has been initialized.
          scope.$emit('chartinit');
          return chart;
        });
      }
    }
  }]);

$watchコールバック関数ではupdate()関数を呼び出す前にグラフが初期化されていることをチェックしています。nv.addGraph()関数は非同期的に実行されるので、コールバック関数はグラフが生成される前に発火する可能性があります。その際は以下のエラーが返却されます。


TypeError: Cannot read property 'apply' of undefined

下のグラフを使って遊んでみましょう。データバインディングのおかげで、データに変更があるたびにグラフは再び描画されます。

See the Pen NVD3 Line Chart Sample by Andreas Argelius (@argelius) on CodePen.

これに似たディレクティブがこの記事の冒頭の例でも使われています。コードは私のGitHubページにあります。

まとめ

この記事について何か質問があれば、下でコメントしてください。どんなフィードバッ>クでも歓迎します!