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, そして良いお年を!