データのアップデートとトランジション1

      データのアップデートとトランジション1 はコメントを受け付けていません

ここでは、動的なデータの視覚化をおこなっていきます。例えば、グラフをクリックすることによってデータの並び方を変えるなどの動作をつけることができるようになります。

スケールを使って棒グラフを描画する

それでは、前に作成した棒グラフを再利用します。「old_bar_chart.html」をダウンロードしてください。

このサンプルを、すでに学習したスケールを使って書き換えていきます。この場合、xScaleについては注意が必要です。y座標はd3.scaleLinear()メソッドで求められますが、x座標は散布図のような数値ではありません。この場合、d3.scaleBand()を使うことによって、項目数に応じたバーの幅やx座標が求められます。
以下のように書き換えます。

var xScale = d3.scaleBand()
            .domain(d3.range(dataset.length))   //datasetの要素数を計算
            .rangeRound([0, w])  //range()と同じ機能だが、整数値にする
            .paddingInner(0.05)  //バー同士の間のpadding
            .paddingOuter(0.5);  //バーの外側のpadding


var yScale = d3.scaleLinear()
            .domain([0, d3.max(dataset)])
            .range([0, h]);

わかりにくいのは2行目のd3.range(dataset.length)です。このコードはdatasetの配列の要素数を抽出し、以下のように順番に値が入った配列を新しく作ります。ですので、以下と同じ意味になります。

.domain([0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14])

つまり、この場合の入力(Input Domain)は、0〜14の連続的な範囲ということになります。


次に、rectのx, y, width, heightも変更します。

.attr("x", function(d, i) {
    return xScale(i);
})
.attr("y", function(d) {
      return h - yScale(d);
})
.attr("width", xScale.bandwidth())
.attr("height", function(d) {
   return yScale(d);
})


さらに、textのx, yも変更します。

.attr("x", function(d, i) {
      return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function(d) {
      return h - yScale(d) + 14;
})


これで、画面の幅(w)、高さ(h)を変えると中の棒グラフも一緒に拡大縮小するようになりました。


最終的なコードは以下です。

<script type="text/javascript">
  var w = 400;
  var h = 250;

  var dataset = [10, 5, 3, 20, 4, 8, 11, 3, 6, 9, 20, 4, 5, 8, 23];

  var xScale = d3.scaleBand()
     .domain(d3.range(dataset.length))   //datasetの数を計算
     .rangeRound([0, w])  //range()と同じ機能だが、整数値にする
     .paddingInner(0.05)  //バー同士の間のpadding
     .paddingOuter(0.5);  //バーの外側のpadding

  var yScale = d3.scaleLinear()
              .domain([0, d3.max(dataset)])
              .rangeRound([0, h]);

  //SVG要素を作成
  var svg = d3.select("body")
              .append("svg")
              .attr("width", w)
              .attr("height", h);

  svg.selectAll("rect")
     .data(dataset)
     .enter()
     .append("rect")
     .attr("x", function(d, i) {
         return xScale(i);
     })
     .attr("y", function(d) {
           return h - yScale(d);
     })
     .attr("width", xScale.bandwidth())
     .attr("height", function(d) {
        return yScale(d);
     })
     .attr("fill", function(d) {
        //dの最大値が20なので、青の最大値が240になる
        return "rgb(0, 0, " + (d * 12) + ")";
     });

  svg.selectAll("text")      //配列の数分だけtextを用意する
     .data(dataset)
     .enter()
     .append("text")         //textを追加
     .text(function(d) {
           return d;         //textを表示
     })
     .attr("x", function(d, i) {
           return xScale(i) + xScale.bandwidth() / 2;
     })
     .attr("y", function(d) {
           return h - yScale(d) + 14;
     })
     .attr("text-anchor", "middle")      //テキストを中央揃え
     .attr("font-family", "sans-serif")     //サンセリフ
     .attr("font-size", "11px")
     .attr("fill", "white");

</script>

データの更新

それでは、新しくデータを更新してみましょう。簡単なボタンを作るため、<body>内に以下のコードを追加しましょう。

<p>クリックするとデータを更新します</p>


そして、最後にclick機能を追加します。

//クリックすることによって新しいデータに更新
d3.select("p")
   .on("click", function() {
 
      //datasetの新しい値
      dataset = [ 4, 8, 20, 10, 15, 7, 18, 9, 20, 15, 23, 22, 5, 7, 8 ];
 
      //全てのバーを更新
      svg.selectAll("rect")
         .data(dataset)
         .attr("y", function(d) {
               return h - yScale(d);
         })
         .attr("height", function(d) {
               return yScale(d);
         });
   });


再生してテキストをクリックしてください。バーの高さが変わります。
しかし、これだけだと色が変わらないままで、テキストの位置も変わりません。以下のように書き換えてください。

//全てのバーを更新
svg.selectAll("rect")
   .data(dataset)
   .attr("y", function(d) {
         return h - yScale(d);
   })
   .attr("height", function(d) {
         return yScale(d);
   })
   .attr("fill", function(d) {
      return "rgb(0, 0, " + Math.round(d * 10) + ")";
   });
 
//ラベルを更新
svg.selectAll("text")
   .data(dataset)
   .text(function(d) {
         return d;
   })
   .attr("y", function(d) {
         return h - yScale(d) + 14;
   });

色とテキストの位置も更新されました。


最終的なコードは以下です。


<p>クリックするとデータを更新します</p>

<script type="text/javascript">
    var w = 400;
    var h = 250;

    var dataset = [10, 5, 3, 20, 4, 8, 11, 3, 6, 9, 20, 4, 5, 8, 23];

    var xScale = d3.scaleBand()
        .domain(d3.range(dataset.length))   //datasetの数を計算
        .rangeRound([0, w])  //range()と同じ機能だが、整数値にする
        .paddingInner(0.05)  //バー同士の間のpadding
        .paddingOuter(0.5);  //バーの外側のpadding

    var yScale = d3.scaleLinear()
                .domain([0, d3.max(dataset)])
                .rangeRound([0, h]);

    //SVG要素を作成
    var svg = d3.select("body")
                .append("svg")
                .attr("width", w)
                .attr("height", h);

    svg.selectAll("rect")
        .data(dataset)
        .enter()
        .append("rect")
        .attr("x", function(d, i) {
            return xScale(i);
        })
        .attr("y", function(d) {
                return h - yScale(d);
        })
        .attr("width", xScale.bandwidth())
        .attr("height", function(d) {
            return yScale(d);
        })
        .attr("fill", function(d) {
            //dの最大値が20なので、青の最大値が240になる
            return "rgb(0, 0, " + (d * 12) + ")";
        });

    svg.selectAll("text")      //配列の数分だけtextを用意する
        .data(dataset)
        .enter()
        .append("text")         //textを追加
        .text(function(d) {
                return d;         //textを表示
        })
        .attr("x", function(d, i) {
                return xScale(i) + xScale.bandwidth() / 2;
        })
        .attr("y", function(d) {
                return h - yScale(d) + 14;
        })
        .attr("text-anchor", "middle")      //テキストを中央揃え
        .attr("font-family", "sans-serif")     //サンセリフ
        .attr("font-size", "11px")
        .attr("fill", "white");

    //クリックすることによって新しいデータに更新
    d3.select("p")
        .on("click", function() {

            //datasetの新しい値
            dataset = [ 4, 8, 20, 10, 15, 7, 18, 9, 20, 15, 23, 22, 5, 7, 8 ];

            //全てのバーを更新
            svg.selectAll("rect")
                .data(dataset)
                .attr("y", function(d) {
                    return h - yScale(d);
                })
                .attr("height", function(d) {
                    return yScale(d);
                })
                .attr("fill", function(d) {
                    return "rgb(0, 0, " + Math.round(d * 10) + ")";
                });

            //ラベルを更新
            svg.selectAll("text")
                .data(dataset)
                .text(function(d) {
                    return d;
                })
                .attr("x", function(d, i) {
                    return xScale(i) + xScale.bandwidth() / 2;
                })
                .attr("y", function(d) {
                    return h - yScale(d) + 14;
                });
    });

</script>

トランジション

さて、ここからがD3の便利な機能であるトランジションです。トランジションとは、ある段階からある段階への「移行」のことを指します。D3では、Aの状態からBの状態へアニメーションを伴って移行することを指します。このトランジションは意外と簡単です。クリック時のsvg.selectAll(“rect”)を、以下のように変更します。

//全てのバーを更新
svg.selectAll("rect")
   .data(dataset)
   .transition()  //アニメーション
   .duration(2000)   //時間(2秒-2000ミリセカンド)


ラベルも同じように変更してみましょう。

/ラベルを更新
svg.selectAll("text")
   .data(dataset)
   .transition()
   .duration(2000)

イージング

このtansitionのアニメーションは単純な直線的な動きだけでなく、.ease()を使うと様々なパターンに変化します。
色々試してみましょう。次の例は、直線的な動きです。

.transition()  //アニメーション
.duration(2000)   //時間(2秒-2000ミリセカンド)
.ease(d3.easeLinear)


以下の機能も試してみましょう。

d3.easeCircleIn
d3.easeElasticOut
d3.easeBounceOut

イージングのパターンは様々です。以下のサイトで動きを確認してみましょう。
https://bl.ocks.org/mbostock/248bac3b8e354a9103c4

アニメーションの遅延

アニメーションは開始時間を遅らせることができます。delay()を追加してみましょう。単位はミリセカンドなので、1000ミリセカンド=1秒です。

.transition()  //アニメーション
.delay(1000)
.duration(2000)   //時間(2秒-2000ミリセカンド)
.ease(d3.easeLinear)


また、delay()は一つ一つの要素に適応させることもできます。以下の例は、右側のバーの方がアニメーション開始時間が100ミリセカンドずつ遅くなっています。

.delay(function(d, i) {
   return i * 100;		
})


次の例は、全体が2秒の間に左のバーから右のバーに従って順番に遅延する例です。

.delay(function(d, i) {
   return i / dataset.length * 2000;
})

.on(“click”内の完全なコードは以下になります。

//クリックすることによって新しいデータに更新
d3.select("p")
   .on("click", function() {

      //datasetの新しい値
      dataset = [ 4, 8, 20, 10, 15, 7, 18, 9, 20, 15, 23, 22, 5, 7, 8 ];

      //全てのバーを更新
      svg.selectAll("rect")
         .data(dataset)
         .transition()  //アニメーション
         .delay(function(d, i) {
            return i / dataset.length * 2000;
         })
         .duration(2000)   //時間(2秒-2000ミリセカンド)
         .ease(d3.easeLinear)
         .attr("y", function(d) {
               return h - yScale(d);
         })
         .attr("height", function(d) {
               return yScale(d);
         })
         .attr("fill", function(d) {
            return "rgb(0, 0, " + Math.round(d * 10) + ")";
         });

      //ラベルを更新
      svg.selectAll("text")
         .data(dataset)
         .transition()
         .delay(function(d, i) {
            return i / dataset.length * 2000;
         })
         .duration(2000)
         .ease(d3.easeLinear)
         .text(function(d) {
               return d;
         })
         .attr("x", function(d, i) {
               return xScale(i) + xScale.bandwidth() / 2;
         })
         .attr("y", function(d) {
               return h - yScale(d) + 14;
      });
});

ランダムに値を生成する

今までのサンプルは一回クリックするだけでしたが、クリックするたびに配列の値を更新するサンプルも作成してみましょう。クリック時のdatasetを以下のように書き換えます。

//新しいdatasetを自動作成
var dataNum = dataset.length;                 //配列の要素数を調べる
dataset = [];                                //配列を初期化
for (var i = 0; i < dataNum; i++) {           //for文でループする
   var newNum = Math.floor(Math.random() * 30); //0~29の新しい値
   dataset.push(newNum);                  //配列に新しい値を入れる
}


クリックするたびに値が更新され、アニメーションが作成されます。


ここで問題が出てきます。初めのdatasetの最大値が23だったので、ランダムに生成される値の最大値が29となって、高いバーは画面からはみ出してしまう場合が出てきます。どんな値になっても、画面内に収まるように、スケールを定義し直しましょう。

.on("click", function() {
 
   //新しいdatasetを自動作成
   var dataNum = dataset.length;                 //配列の要素数を調べる
   dataset = [];                                //配列を初期化
   for (var i = 0; i < dataNum; i++) {           //for文でループする
      var newNum = Math.floor(Math.random() * 30); //0~24の新しい値
      dataset.push(newNum);                  //配列に新しい値を入れる
   }
 
   yScale.domain([0, d3.max(dataset)]);   //domainだけ変更すればいい


これで、ランダムに生成される値が変わっても、バーの高さの最大値は一定になりました。


.on(“click”内の完全なコードは以下になります。

//クリックすることによって新しいデータに更新
d3.select("p")
   .on("click", function() {

      //新しいdatasetを自動作成
      var dataNum = dataset.length;                 //配列の要素数を調べる
      dataset = [];                                //配列を初期化
      for (var i = 0; i < dataNum; i++) {           //for文でループする
         var newNum = Math.floor(Math.random() * 30); //0~29の新しい値
         dataset.push(newNum);                  //配列に新しい値を入れる
      }

      yScale.domain([0, d3.max(dataset)]);   //domainだけ変更すればいい

      //全てのバーを更新
      svg.selectAll("rect")
         .data(dataset)
         .transition()  //アニメーション
         .delay(function(d, i) {
            return i / dataset.length * 2000;
         })
         .duration(2000)   //時間(2秒-2000ミリセカンド)
         .ease(d3.easeLinear)
         .attr("y", function(d) {
               return h - yScale(d);
         })
         .attr("height", function(d) {
               return yScale(d);
         })
         .attr("fill", function(d) {
            return "rgb(0, 0, " + Math.round(d * 10) + ")";
         });

      //ラベルを更新
      svg.selectAll("text")
         .data(dataset)
         .transition()
        .delay(function(d, i) {
           return i / dataset.length * 2000;
        })
         .duration(2000)
         .ease(d3.easeLinear)
         .text(function(d) {
               return d;
         })
         .attr("x", function(d, i) {
               return xScale(i) + xScale.bandwidth() / 2;
         })
         .attr("y", function(d) {
               return h - yScale(d) + 14;
      });
});

散布図のインタラクション

それでは、すでに作成した散布図をアレンジして、インタラクションをつけてみましょう。以下のファイルをダウンロードしてください。


まず、最上部のdatasetの値をランダムに作成するように変更します。

//ランダムにdatasetの中身を作成
var w = 500;
var h = 300;
var padding = 30;

var dataset = [];                            //空の配列を作る
var dataNum = 50;                            //配列の要素数
var maxNum = Math.random() * 200;            //値の最大値
for (var i = 0; i < dataNum; i++) {          //dataNumの数だけ値を作成
   var newX = Math.floor(Math.random() * maxNum);
   var newY = Math.floor(Math.random() * maxNum);
   dataset.push([newX, newY]);               //動的配列に値を入れる
}


さらに、クリック時のアクションを追加します。

//クリック時のアクション////////////////////////////////////
d3.select("p")
    .on("click", function(){

      var maxNum = Math.random() *200;
      dataset = [];

      for(var i = 0; i < dataNum; i ++){
        var newX = Math.floor(Math.random() * maxNum);
        var newY = Math.floor(Math.random() * maxNum);

        dataset.push([newX, newY]);

      }

      xScale.domain([0, d3.max(dataset, function(d){ return d[0]; })]);
      yScale.domain([0, d3.max(dataset, function(d){ return d[1]; })]);

      svg.selectAll("circle")
         .data(dataset)
         .transition()
         .duration(1000)
         .attr("cx", function(d) {
            return xScale(d[0]);   //二次元配列の一つ目の値
         })
         .attr("cy", function(d) {
            return yScale(d[1]);  //二次元配列の二つ目の値
        })
        .attr("r", function(d){
           return d[1]/10;
        });   //半径
    });


クリックするたびに値が入れ替わって円が移動するようになります。テキストも移動するように変更してみましょう。

//ラベルを更新
svg.selectAll("text")
   .data(dataset)
   .transition()
   .duration(1000)
   .text(function(d) {
         return d[0] + "," + d[1];
   })
   .attr("x", function(d) {
         return xScale(d[0]);
   })
   .attr("y", function(d) {
         //半径(d[1]/10)+10ピクセル分だけ下に下げる
         return yScale(d[1]) + d[1] / 10 + 10;
   }); 

動的に変化する目盛り

次に、データの最大値に合わせて目盛りの単位も動的に変化するようにしてみましょう。classを「x axis」と「y axis」にして、それぞれ独自の名称にします。

//x座標の目盛りを作成
svg.append("g")   //gはグループ用のsvgタグ
   .attr("class", "x axis")
   .attr("transform", "translate(0," + (h - padding) + ")") //目盛りを移動
   .call(xAxis);
 
//y座標の目盛りを作成
svg.append("g")   //gはグループ用のsvgタグ
   .attr("class", "y axis")
   .attr("transform", "translate(" + padding + ",0)")
   .call(yAxis);
//Update X axis
svg.select(".x.axis")
   .transition()
   .duration(1000)
   .call(xAxis);
 
//Update Y axis
svg.select(".y.axis")
   .transition()
   .duration(1000)
   .call(yAxis);


目盛りも動的に変化するようになりました。