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

前回の続きです!引き続き、D3.jsでレーダーチャートを書きます。

レーダーチャートは書くことはできましたが、SVG領域の大きさが100×100であることが前提でコーディングされてます。

たとえば、前回作成したline関数はこんな感じでした。

var dataset = [
  [5, 5, 2, 5, 5],
  [2, 1, 5, 2, 5]
];
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');

.xに渡してる関数が、半径 × cos(角度)になっていて角度からレーダーチャートの頂点のx座標を求めてます。
10 * dの部分が半径で、データの値dに応じて半径の大きさを変えています。10を掛けているのはdの値を5段階評価としているので、値に応じて半径が10,20,30,40,50になるようにするためです。
+50の部分は、レーダーチャートの中心を(0, 0)から(50, 50)にずらしてます。

今日は、SVGの大きさに応じてレーダーチャートの大きさ(見た目上の大きさ)も動的に変化するようにしてみます。
あと、データの値の大きさ、データの要素数が変わった場合も動的に変化するようにします。

d3.scaleを使う

チュートリアルに書いてあったD3.jsのスケールが使えそう。データの値大きさと、表示上のピクセル値の大きさをマッピングするための機能が、D3.jsのスケール。
以下のように、d3.scale.linear()を使って関数を作る。domainに渡したデータ値の範囲を、rangeに渡したピクセル値の範囲にマッピングする関数が作れる。以下のrScala関数は引数が1の時は10を、引数が5の時は50を返してくれるようになる。

var rScale = d3.scale.linear()
               .domain([0, 5])
               .range([0, 50]);

そして、半径の計算をしている箇所で、rScale関数を使うようにする。

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

うん、問題なく動いてる。

さて、ここでrScale関数のrangeをSVGの大きさによって動的に変わるようにしてあげる。SVGの大きさは変数w,hで指定されているので、rangeの最大値をw/2にする。

var w = 100,
    h = 100;
var rScale = d3.scale.linear()
               .domain([0, 5])
               .range([0, w/2]);

うん、問題なく動いてる。じゃあ、SVGの大きさを100×100から200×200に変えてみる。

var w = 200,
    h = 200;

お、大きくなったけど、ずれてる。中心点も動的にする必要があるなぁ。+50の部分を+w/2に書き換えてあげる。

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

おー、できた!

rangeの方は動的になったけど、domainの方が5段階評価で固定になっちゃってる。こっちも動的にしよう。0から全データの最大値の範囲にすればいい。2次元配列の最大値を求めるにはd3.jsを使って以下のようにできる。求めたmaxを使ってdomainを指定する。
データ値として"8"を入れてみる。

var dataset = [
  [5, 5, 2, 5, 8],
  [2, 1, 5, 2, 5]
];
var max = d3.max(d3.merge(dataset));
var rScale = d3.scale.linear()
               .domain([0, max])
               .range([0, w/2]);

できてるできてる。
グリッド線を表すデータセットも固定になってるので、maxを使ってこんな感じにすればいいかな。

// 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],
//];
var grid = (function(){
  var result = [];
  for(var i=1; i<=max; i++){
    result.push([i, i, i, i, i]);
  }
  return result;
})();

できてるできてる。

これで点数がデータの値が大きくなっても動的にレーダーチャートのメモリが増えるようになった。あとはデータの要素数が変わったときにレーダーチャートの頂点が動的に変わるようにする。
まずはデータの要素数を5から8に変更。要素数をparamCountにいれておいて、gridとline関数の定義時にparamCountを使うようにする。

var dataset = [
      [5, 5, 2, 5, 8, 2, 3],
      [2, 1, 5, 2, 5, 6, 7]
    ],
    paramCount = dataset[0].length,
    grid = (function(){
      var result = [];
      for(var i=1; i<=max; i++){
        var arr = [];
        for (var j=0; j<paramCount; j++){
          arr.push(i);
        }
        result.push(arr);
      }
      return result;
    })(),
    line = d3.svg.line()
             .x(function(d, i){ return rScale(d) * Math.cos(2 * Math.PI / paramCount * i - (Math.PI / 2)) + w/2; })
             .y(function(d, i){ return rScale(d) * Math.sin(2 * Math.PI / paramCount * i - (Math.PI / 2)) + w/2; })
             .interpolate('linear');

うん、できた!

おしまい

これで、svg領域のサイズ、データの値の大きさ、データの要素数、それぞれの値が変わった場合にレーダーチャートの形も動的に変わるようにできました。

次は、各頂点にラベルをふるのと、チャート毎に色分けできるようにしたいと思います。

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

var w = 200,
    h = 200,
    svg = d3.select('body')
            .append('svg')
            .attr('width', w)
            .attr('height', h),
    dataset = [
      [5, 5, 2, 5, 8, 2, 3],
      [2, 1, 5, 2, 5, 6, 7]
    ],
    paramCount = dataset[0].length,
    max = d3.max(d3.merge(dataset)),
    rScale = d3.scale.linear()
               .domain([0, max])
               .range([0, w/2]),
    grid = (function(){
      var result = [];
      for(var i=1; i<=max; i++){
        var arr = [];
        for (var j=0; j<paramCount; j++){
          arr.push(i);
        }
        result.push(arr);
      }
      return result;
    })(),
    line = d3.svg.line()
             .x(function(d, i){ return rScale(d) * Math.cos(2 * Math.PI / paramCount * i - (Math.PI / 2)) + w/2; })
             .y(function(d, i){ return rScale(d) * Math.sin(2 * Math.PI / paramCount * i - (Math.PI / 2)) + w/2; })
             .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');