12/25/2013

多次元解析のためのチャートライブラリ DC.js

d3.js Advent Calendar 2013の25日目です。皆様のおかげで全部埋めることができて良かったです。
今日はDC.jsという多次元解析のためのチャートライブラリをご紹介します。ちなみにこれはD3の情報ソースという5日目の記事で紹介したDashing D3.jsで最近紹介されていて知りました。
DC.jsによるダッシュボードサンプル

DC.jsはサイトを見ていただければどういうものか分かると思いますが、 複数のチャートにまたがって、データのフィルタが可能なチャートライブラリです。デモサイトのそれぞれのチャートを操作すると他のチャートもそれに従ってダイナミックに表示が更新されます。

DC.jsはCrossfilterという配列のフィルタ処理を行うライブラリとD3.jsに依存しています。CrossfilterはSquareというiPhoneをカードリーダーにするハードウェア、サービスを提供している会社が提供しているオープンソースのライブラリで自社のSquare Registerのために開発されました。
上記のCrossfilterのサイトでもDC.jsのサイトのようなデモが見れます。DC.jsの作者はこのCrossfilterのデモに感銘を受けて、これを手軽に扱えるライブラリとしてDC.jsを開発したとのことです。

Dashing D3.jsで紹介されていたDC.jsのチュートリアルがとてもわかり易かったのでそれを元にして紹介する形をとりたいと思います。分かりやすくするため、若干コードを変更しているところもありますが基本的には一緒です。
Making Dashboards with Dc.js - Part 1: Using Crossfilter.js
Making Dashboards with Dc.js - Part 2: Graphing

1.Crossfilter

まずCrossfilterがどのように動作するかCrossfilterのAPIドキュメントにあるにある データを使って見てみましょう。
var data = [
  {date: "2011-11-14T16:17:54Z", quantity: 2, total: 190, tip: 100, type: "tab"},
  {date: "2011-11-14T16:20:19Z", quantity: 2, total: 190, tip: 100, type: "tab"},
  {date: "2011-11-14T16:28:54Z", quantity: 1, total: 300, tip: 200, type: "visa"},
  {date: "2011-11-14T16:30:43Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T16:48:46Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T16:53:41Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T16:54:06Z", quantity: 1, total: 100, tip: 0, type: "cash"},
  {date: "2011-11-14T16:58:03Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T17:07:21Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T17:22:59Z", quantity: 2, total: 90, tip: 0, type: "tab"},
  {date: "2011-11-14T17:25:45Z", quantity: 2, total: 200, tip: 0, type: "cash"},
  {date: "2011-11-14T17:29:52Z", quantity: 1, total: 200, tip: 100, type: "visa"}
]; 
crossfilterのインスタンスを作成します。
var ndx = crossfilter(data); 
totalを軸としたdimensionを作成します。
var totalDim = ndx.dimension(function(d) { return d.total; }); 
その中からtotal=90のものを抽出します。
var total_90 = totalDim.filter(90); 
以下のjsdoitのコードで動作が確認できるので見てください。total=90のものだけ抽出されているのがわかると思います。


では次にstringでやってみましょう。
var typeDim  = ndx.dimension(function(d) {return d.type;});
var visa_filter = typeDim.filter("visa");
var cash_filter = typeDim.filter("cash");
以下のjsdoitのコードで動作が確認できるので見てください。visa、 cashでそれぞれ抽出できているのがわかると思います。


ちなみにコメントアウトしてるvar total_90 = totalDim.filter(90);を有効にするとfilterの結果は0になります。totalが90のものにvisa、 cashのものがないからです。このようにちゃんとcrossfilterされていることが確認できます。では次に実際にDC.jsを使ってチャートを描画してみましょう。

2.DC.jsでチャートの描画

2.1:ラインチャート

まずデータを用意します。以下ははデイリーのwebサーバのhit数を示した配列データです。
var data = [
    {date: "12/27/2012", http_404: 2, http_200: 190, http_302: 100},
    {date: "12/28/2012", http_404: 2, http_200: 10, http_302: 100},
    {date: "12/29/2012", http_404: 1, http_200: 300, http_302: 200},
    {date: "12/30/2012", http_404: 2, http_200: 90, http_302: 0},
    {date: "12/31/2012", http_404: 20, http_200: 90, http_302: 0},
    {date: "01/01/2013", http_404: 2, http_200: 90, http_302: 0},
    {date: "01/02/2013", http_404: 1, http_200: 10, http_302: 1},
    {date: "01/03/2013", http_404: 2, http_200: 90, http_302: 0},
    {date: "01/04/2013", http_404: 2, http_200: 90, http_302: 0},
    {date: "01/05/2013", http_404: 2, http_200: 90, http_302: 0},
    {date: "01/06/2013", http_404: 2, http_200: 200, http_302: 1},
    {date: "01/07/2013", http_404: 1, http_200: 200, http_302: 100}
];
日付データをd3のtime.formatクラスを使って、d3で扱えるデータにdateを変更します。またトータルのhit数を示すtotal属性も追加します。
var parseDate = d3.time.format("%m/%d/%Y").parse;
data.forEach(function(d) {
    d.date = parseDate(d.date);
    d.total = d.http_404 + d.http_200 + d.http_302;
});
crossfilterのインスタンスを作成し、X軸をtimelineにするためdateのdimensionを作成、またY軸用のkey-valueデータをgroup()のreduceSumを使って作成します。 
//dataからcrossfilterのインスタンスを作成
var ndx = crossfilter(data);

//X軸をtimelineにするためdateのdimensionを作成
var dateDim = ndx.dimension(function(d) {
    return d.date;
});

//Y軸にtotalを表示するためのkey-valueデータをdateDimから作成
var hits = dateDim.group().reduceSum(function(d) {
    return d.total;
});  
dateの最古値と最新値を取得します。
var minDate = dateDim.bottom(1)[0].date;
var maxDate = dateDim.top(1)[0].date; 
<div id="chart-line-hitsperday"></div>というdivを定義し、そこにラインチャートを表示させてみましょう。
dc.lineChartでインスタンスを作成し、parameterを設定して最後にdc.renderAll()で描画します。 
//dcのlineChartインスタンスを作成
var hitslineChart = dc.lineChart("#chart-line-hitsperday");

//parameter設定
hitslineChart
    .width(450).height(200)
    .dimension(dateDim)
    .group(hits)
    .x(d3.time.scale().domain([minDate, maxDate]));

//チャートを描画
dc.renderAll();   
以下のjsdoitのコードで動作が確認できます。チャートがひとつなので 複数のチャートにまたがっての動作はないですがチャートが描画されているのがわかると思います。またこのラインチャートはレンジセレクタが出来るようになっています。


2.2:パイチャートの追加 

yearの属性を追加する処理を追加します。 
var parseDate = d3.time.format("%m/%d/%Y").parse;
data.forEach(function(d) {
    d.date = parseDate(d.date);
    d.total = d.http_404 + d.http_200 + d.http_302;
    d.Year=d.date.getFullYear(); //yearの属性を追加
});    
パイチャート用のdimensionを作成、またパイチャートののkey-valueデータを作成します。
//パイチャートのdimensionを作成
var yearDim  = ndx.dimension(function(d) {return +d.Year;});

//パイチャートののkey-valueデータをyearDimから作成
var year_total = yearDim.group().reduceSum(function(d) {return d.total;});    
<div id="chart-ring-year"></div>というdivを定義し、そこにパイチャートを表示させてみましょう。
基本的にラインチャートと同じ手続きです。 
//dcのパイチャートインスタンスを作成
var yearRingChart = dc.pieChart("#chart-ring-year");

yearRingChart
    .width(200).height(200)
    .dimension(yearDim)
    .group(year_total)
    .innerRadius(30);    
以下のjsdoitのコードで動作が確認できます。ラインチャートのレンジセレクタで範囲を変更すると動的にパイチャートが変更されるが分かるかと思います。またパイチャートをクリックするとラインチャートが変化します。このように複数のチャートにまたがって、容易にフィルタすることができます。面白いですね。


2.3:ラインチャートにhttpステータス毎のデータを表示

httpステータス毎にgroupデータを作成します。 
//Y軸にtotalを表示するためのkey-valueデータをhttpステータス毎に作成
var status_200=dateDim.group().reduceSum(function(d) {return d.http_200;});
var status_302=dateDim.group().reduceSum(function(d) {return d.http_302;});
var status_404=dateDim.group().reduceSum(function(d) {return d.http_404;});     
status_302、 status_404はstackとすることでgroupで設定されたものに対して積み上げて表示することができます。
//lineChartの各種parameter設定
hitslineChart
   .width(450).height(200)
   .dimension(dateDim)
   .group(status_200,"200")
   .stack(status_302,"302")
   .stack(status_404,"404")
   .renderArea(true)
   .x(d3.time.scale().domain([minDate,maxDate]))
   .legend(dc.legend().x(50).y(10).itemHeight(13).gap(5))
   .yAxisLabel("Hits per day");      
ではjsdoitで確認してみましょう。ラインチャートがhttpステータス毎に表示されているのが分かるかと思います。


2.4:データテーブルの追加

最後にデータテーブルを追加してみましょう。
以下のデータテーブル用のhtmlを追加します。
<div style='font-size:11px; width:270px; margin-left:180px;'>
          <table id="dc-data-table">
               <thead>
               <tr class="header">
                    <th>Day</th>
                    <th>TPS 200</th>
                    <th>TPS 302</th>
                    <th>TPS Total</th>
               </tr>
               </thead>
          </table>
</div> 
データテーブルのインスタンスを作成し、parameterを設定します。基本的に他のチャートと同じ手順です。
//dcのデータテーブルインスタンスを作成
var datatable = dc.dataTable("#dc-data-table");
datatable
    .dimension(dateDim)
    .group(function(d) {return d.Year;})
    .columns([
        function(d) { return d.date.getDate() + "/" + (d.date.getMonth() + 1) + "/" + d.date.getFullYear(); },
        function(d) {return d.http_200;},
        function(d) {return d.http_302;},
        function(d) {return d.http_404;},
        function(d) {return d.total;}
    ]); 
ではjsdoitで確認してみましょう。データテーブルも動的に変更されているのが確認できると思います。


いかがでしょうか?かなり少量のコードでこのような複数のチャートにまたがったフィルタ処理ができるのはすごいですね。DashBoard等のUI開発にかなり役に立ちそうです。

ではd3.js Advent Calendar 2013はこれでおしまいです。投稿していただいた方、読んでいただいた方、本当にありがとうございました!
Happy D3 life and Happy Holiday, そして良いお年を!

12/18/2013

記事紹介:D3のセレクションの仕組み

d3.js Advent Calendar 2013の18日目です。なかなかカレンダーが埋まらないようなので、既存の記事の紹介です。
D3.jsでのselector、selectAllはD3.jsの根幹の機能の一つですが、一方で概念を理解するのが難しいものの一つと思います。(僕だけですかね‥)
この記事はD3.jsの作者Mike Bostock本人が書いた記事ですが、そのあたりのもやもやをかなりすっきりさせてくれる記事で、ちょっと長いですが一読をおすすめします。個人的にはグループ化、非グループ化の辺りがかなりすっきりしました。
How Selections Work
http://bost.ocks.org/mike/selection/
この長い英文を読むのはちょっと‥、という方は@FoD5さんが翻訳された記事もありますので(FoD5さん、Goodjobです、すばらしい翻訳記事だと思います!!)こちら御覧ください。
http://ja.d3js.info/mike/selection/

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)

12/05/2013

D3の情報ソース

こんにちは、林です。d3.js Advent Calendar 2013の5日目です。今日は私がd3の情報収集につかっているサイトを紹介します。

Dashing D3.js(おすすめ)

https://www.dashingd3js.com/
Dashing D3のチュートリアル
Tutorial、Screencasts(有料ですが‥)もあり、良いコンテンツが多いと思います。
また無料のWeekly Newsletterも提供しており、d3のみならずData Visualizaton全般の最新の記事を紹介してくれるのでおすすめです。
アーカイブがここでみれます。
https://www.dashingd3js.com/data-visualization-and-d3-newsletter

StackOverFlow

http://stackoverflow.com/
実装でわからないことがあるとやはりここにたどり着くと思います。
40%ぐらいの問題はここで解決できる印象です。

CODEPAN

http://codepen.io/search/?q=d3.js

JSdo.it

http://jsdo.it/tag/d3.js
CODEPEN
CODEPAN、 JSdo.itもd3.jsのキーワードで検索してコードみながら雰囲気つかむのに使っています。
d3.jsはコード読むだけだとよくわからないことも多く、実際にコード修正しながら確認できるこういったツールはとても有用です。

D3.js forum

https://groups.google.com/forum/#!forum/d3-js
トピックの流れが早いので、あまり追ってはいませんが、ここで解決したことも多いです。どうしても分からない時はここに来るという感じです。

D3.jsももっと日本語の情報増えると良いのですが‥。
みなさんの有益な情報ソースも教えていただけますと幸いです。

12/01/2013

D3.jsで既存のSVG Elementを扱う

こんにちは、林です。d3.js Advent Calendar 2013の1日目です。他のAdventカレンダーに比べると参加率が低い原因は、やはり日本ではd3.jsの知名度はまだ低いからなのでしょうか?海外ではData VisualizationといえばD3.jsというぐらいメジャーなライブラリです。日本語での情報もまだ少ないと感じるので、このAdvent Calendarが少しでも日本での普及に貢献できれば幸いです。

さて、今回のテーマは「D3.jsで既存のSVG Elementを扱う」です。
一般的にD3.jsでのサンプルは四角や丸などのプリミティブな形を使ったものが多いです。実際Data Visualizationでそれで困ることはあまりないかもしれませんが、まれにIllustrator等で作成したSVGデータをD3で扱いたいという状況も発生します。いくつかのやり方があるかと思いますが、自分がとったアプローチを紹介します。

1. html内に描画領域とは別の箇所にsvgをテンプレートとして埋め込む (SVGはIllustrator等で描いてsvgで保存、その後テキストエディタ等で細かいところを修正)
<body>
<!-- SVG element template -->
<svg id="svg_templates" display="none">
  <g class="instance_small device_body">
    <rect rx="4" ry="4" width="40" height="40" class="frame"></rect>
    <g transform="translate(10,6)" class="icon">
      <rect class="instance_bg" width="20" height="26" rx="1" ry="1"></rect>
      <rect x="4" y="3" fill="#FFFFFF" width="12" height="4"></rect>
      <rect x="4" y="9" fill="#FFFFFF" width="12" height="4"></rect>
      <circle class="active" cx="6" cy="19" r="2.6"></circle>
    </g>
  </g>
</svg>
</body>
2. テンプレートからd3.selectでnodeを取得しcloneNodeで複製し、d3のenter()処理内でappendChildする
var device = svg.selectAll('g.device').data(data);
device.enter()
  .append("g")
  .attr('class','device')
  .each(function(d, i){
       this.appendChild(
           d3.select('#svg_templates > .instance_small').node().cloneNode(true)
       );
  });
3. データをelementに反映させる
テンプレートのsvgでd3のデータを反映させたい箇所は予めclassを付与しselectで参照することで、任意のelementにデータを反映させることができます。
device
  .select('.instance_bg')
  .transition()
  .duration(500)
  .delay(function(d, i) {
      return i * 400;
   })
  .style('fill', function(d){return '#' + d});
実際のサンプルは以下です。サーバアイコンはhtmlに埋め込まれたsvgからcloneし、その背景色はdata配列に従って変更されています。


このやり方がベストかどうかは分からないのですが、今のところ自分の用途にはこのやり方が使い勝手が良かったです。何か他により良い方法知ってる方は是非教えて下さい。