D3.jsでレーダーチャートを書く(1日目)

今年からD3.jsに入門しました。
チュートリアルD3 入門 | スコット・マレイ | alignedleft)を読み終えたので、作ってみたかったレーダーチャートを作ってみます。こちらのサイト(->svg要素の基本的な使い方まとめ)を参考にさせていただきました。

D3.jsで書くときって、どういう感じで作っていけばいいんでしょうか・・・。
まずは、目標のSVGがどういう感じかを考えてみようかなぁ。

目標を決める

レーダーチャートを描くには、SVGのpathを使うしかないのかな。

<svg width="100" height="100">
<path d="M50,0L100,40L80,100L20,100L0,40z"
      stroke="black"
      stroke-width="2"
      fill="none"></path>
</svg>

・・・うん、まぁこんな感じのpathを何個か書けばそれっぽくなるだろう。。

D3.jsで書いてみる

じゃあさっそくD3.js使ってみよう。
以下のようなHTMLを用意。scriptタグ内にゴリゴリ書いていく。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<script>
  //ここに書いていく
</script>
</body>
</html>

5段階評価ですべて5ポイントのレーダーチャートを書いてみる。まずはD3.jsでお決まり?のsvgをbodyに追加するコードと、入力データの配列を用意。

var w = 100,
    h = 100;
    svg = d3.select('body')
            .append('svg')
            .attr('width', w)
            .attr('height', h),
    dataset = [[5, 5, 5, 5, 5]];

pathを描く。d属性以外は簡単で以下のような感じ。

svg.selectAll('path')
   .data(dataset)
   .enter()
   .append('path')
   .attr('d', function(d){
     // d属性の値
   })
   .attr("stroke", "black")
   .attr("stroke-width", 2)
   .attr('fill', 'none');

D3.jsには、d3.svg.line()というd属性の値のための関数が用意されている。d3.svg.line()については、以下のブログエントリを参考にさせていただきました。
D3.js の d3.svg.line() を試してみた - てっく煮ブログ

d3.svg.line()を使うと以下のような感じに。

svg.selectAll('path')
   .data(dataset)
   .enter()
   .append('path')
   .attr('d', function(d){
     var line = d3.svg.line()
             .x(function(d, i){ // x座標の値 })
             .y(function(d, i){ // y座標の値 })
             .interpolate('linear');
     return line(d)+"z";
   })
   .attr("stroke", "black")
   .attr("stroke-width", 2)
   .attr('fill', 'none');

あとはx,y座標の値を求める処理を考える。5段階中、5ポイントの点を5個配置すればいいから、円周上に5箇所頂点を置けばいい。
円の半径はSVGの領域が100×100なので50とする。
求めたい円周上の点のx座標は、半径 × cos(2π / 頂点の数 × 頂点の連番)。y座標の場合はsin()。

svg.selectAll('path')
   .data(dataset)
   .enter()
   .append('path')
   .attr('d', function(d){
     var line = d3.svg.line()
             .x(function(d, i){ return 50 * Math.cos(2 * Math.PI / 5 * i); })
             .y(function(d, i){ return 50 * Math.sin(2 * Math.PI / 5 * i); })
             .interpolate('linear');
     return line(d)+"z";
   })
   .attr("stroke", "black")
   .attr("stroke-width", 2)
   .attr('fill', 'none');

これでどうだ!!

・・・なんかおかしい。頂点5個ないし。HTMLを確認してみると、座標がマイナスになってる!そうか円の中心を(0, 0)からずらす必要があるな。中心を(50, 50)にしたいので、x,yそれぞれに50足すようにする。

var line = d3.svg.line()
             .x(function(d, i){ return 50 * Math.cos(2 * Math.PI / 5 * i) + 50; })
             .y(function(d, i){ return 50 * Math.sin(2 * Math.PI / 5 * i) + 50; })
             .interpolate('linear');

おー、できてきた。これは右端の頂点からpathが始まってるんだな。-90度回転させたいので、-π/2する。

var line = d3.svg.line()
             .x(function(d, i){ return 50 * Math.cos(2 * Math.PI / 5 * i - (Math.PI / 2)) + 50; })
             .y(function(d, i){ return 50 * Math.sin(2 * Math.PI / 5 * i - (Math.PI / 2)) + 50; })
             .interpolate('linear');

よしよし。

お次は、1つの頂点だけ5ポイントじゃなくて2ポイントとかにしてみよう。

var dataset = [[5, 5, 2, 5, 5]];

半径を点数に応じて動的にしてあげればいいわけだから、半径50で固定になってる部分を書き換える。

var line = d3.svg.line()
             .x(function(d, i){ return 10 * d * Math.cos(2 * Math.PI / 5 * i - (Math.PI / 2)) + 50; })
             .y(function(d, i){ return 10 * d * Math.sin(2 * Math.PI / 5 * i - (Math.PI / 2)) + 50; })
             .interpolate('linear');

いい感じ。

ここまでくれば、もう1つのチャートを追加するのは簡単。

var dataset = [
  [5, 5, 2, 5, 5],
  [2, 1, 5, 2, 5]
];

グリッド線も書く

グリッド線も簡単。グリッド線用のデータを用意して、チャートと同じようにpathを描けばいい。同じline関数を使いたいので、外で定義するように変更する。

var line = d3.svg.line()
             .x(function(d, i){ return 10 * d * Math.cos(2 * Math.PI / 5 * i - (Math.PI / 2)) + 50; })
             .y(function(d, i){ return 10 * d * Math.sin(2 * Math.PI / 5 * i - (Math.PI / 2)) + 50; })
             .interpolate('linear');
svg.selectAll('path')
   .data(dataset)
   .enter()
   .append('path')
   .attr('d', function(d){
     return line(d)+"z";
   })
   .attr("stroke", "black")
   .attr("stroke-width", 2)
   .attr('fill', 'none');

これでline関数が使いまわせるようになったので、そいつを利用してグリッド線を書く。

var grid = [
      [1, 1, 1, 1, 1],
      [2, 2, 2, 2, 2],
      [3, 3, 3, 3, 3],
      [4, 4, 4, 4, 4],
      [5, 5, 5, 5, 5]
    ];
svg.selectAll("path.grid")
   .data(grid)
   .enter()
   .append("path")
   .attr("d", function(d,i){
     return line(d)+"z";
   })
   .attr("stroke", "black")
   .attr("stroke-dasharray", "2")
   .attr('fill', 'none');

おー、一気にそれっぽくなった。

おしまい

今日はここまで。次は、頂点数とか値の最大値とかを動的に扱えるようにがんばってみます。 あとラベルもつけたいなー。

ソースコード全体は以下です。

var w = 100,
    h = 100,
    svg = d3.select('body')
            .append('svg')
            .attr('width', w)
            .attr('height', h),
    dataset = [
      [5, 5, 2, 5, 5],
      [2, 1, 5, 2, 5]
    ],
    grid = [
      [1, 1, 1, 1, 1],
      [2, 2, 2, 2, 2],
      [3, 3, 3, 3, 3],
      [4, 4, 4, 4, 4],
      [5, 5, 5, 5, 5]
    ],
    line = d3.svg.line()
             .x(function(d, i){ return 10 * d * Math.cos(2 * Math.PI / 5 * i - (Math.PI / 2)) + 50; })
             .y(function(d, i){ return 10 * d * Math.sin(2 * Math.PI / 5 * i - (Math.PI / 2)) + 50; })
             .interpolate('linear');
svg.selectAll('path')
   .data(dataset)
   .enter()
   .append('path')
   .attr('d', function(d){
     return line(d)+"z";
   })
   .attr("stroke", "black")
   .attr("stroke-width", 2)
   .attr('fill', 'none');
svg.selectAll("path.grid")
   .data(grid)
   .enter()
   .append("path")
   .attr("d", function(d,i){
     return line(d)+"z";
   })
   .attr("stroke", "black")
   .attr("stroke-dasharray", "2")
   .attr('fill', 'none');