突貫でD3を使うためのメモ書きです。自分はWeb系のプログラマーでも何でもないので、基本的には、コピペでしのげるための知識だけをしりたいよ。
D3ってなんだ?
こんな感じでJavaScriptを使って、かっちょいいグラフを書けるライブラリ。Data-Driven-Documentの略でD3。最新バージョンはv7.0.0
このサイトのサンプルをみたから、D3をやり始めたといっても過言ではない。ぐらいかっちょいい。
参考にした本
O’REILLYからでてるインタラクティブ・データビジュアライゼーション
https://www.oreilly.co.jp/books/9784873116464/
内容は入門レベルで、簡単なので、サクサク読み進められて、良い本だった。
たまにヨーロッパ風のおしゃれな言い回しが、ハイセンスすぎて、よくわからなかったけど、この記事の内容は大体この本を参考に書いてる。
SVG
D3は、SVGというのをよく使うらしいので、事前にSVGを勉強しよう。なにそれ。
SVGって?
html上で図形を描画するための機能みたい。機能的には、canvasに似てるけどちょっと違う。javaScriptから図形などを操作するのは、SVGの方がやりやすいみたい。とにかくお絵かき機能みたいなものか。
実際に円を書いてみる。
See the Pen
SVG001 by Michinori Takeuuchi (@michi1982)
on CodePen.
こんな感じ。cx, cyが円の中心で、ピクセル単位がデフォらしい。 rは円の半径、fillは塗りつぶしの色、strokeは、外形線の色、stroke-widthで外形線の太さを指示する。
次は四角形をいく。
四角形の大きさはwidthとheightで指定して、四角の位置は、左上の頂点を基準として、xとyで指示する。
ちなみにSVGの座標は、一般的なスタイルと同じで、画面の左上を原点にして、右方向がx軸の正方向、下方向がy軸の正方向で定義されている。小学校でならった座標系とは違うので注意。
See the Pen
SVG002 by Michinori Takeuuchi (@michi1982)
on CodePen.
次に棒線。
棒線の定義の仕方は、線の始点(x1,y1)と終点(x2,y2)を指示する。strokeで線色、stroke-widthで線の太さを設定する。
See the Pen
SVG003 by Michinori Takeuuchi (@michi1982)
on CodePen.
円を重ね描き。
SVGは上から順番にレンダリングしていく。Z方向の定義はないので、コードで書いた順番に図形が描かれる。ちなみに、塗りつぶしの色を設定するfillは
fill = "rgba(125,125,0,0.5)"
とかでも指定できる。四番目の引数は透明度をきめる。1.0で不透明で、値が小さくなると色が透けていく。
See the Pen
SVG004 by Michinori Takeuuchi (@michi1982)
on CodePen.
みると、右下の大きい円がカットされている。SVGのサイズを超えると図形は描画されないっぽい。
D3をよぶ
CDN経由でsrc属性にD3のライブラリパスを設定する。これをテンプレートにして、色々書いていく。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>D3 page !!!</title>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js">
</script>
</head>
<body>
<script type="text/javascript">
//ここにD3のコードを色々書く
</script>
</body>
</html>
D3はチェーン構文
D3はチェーン構文っていうのを使って、書いていく。下はチェーン構文でもって、新しく段落要素を追加したサンプル。
See the Pen
D3_001 by Michinori Takeuuchi (@michi1982)
on CodePen.
大事なのは、ここ。
d3.select("body")
.append("p")
.text("Pタグを追加してみた")
チェーン構文では、前のメソッドが次のメソッドにリファレンスを渡すような仕組みになっているようで、この場合だと。
d3. select(“body”) DOMのなかのbody要素を選択して、
append(“p”) そのbody要素にp要素を追加する。そして、そのp要素に対して、
text(“hoge”) hogeという文字列をテキスト表示する
前の処理で使った要素のリファレンスが次の処理に渡される。ドミノ倒し風に処理が渡されていっているようなイメージ。
データをバインドする
D3では、データバインドするっていう表現よくでてくる。どういう意味だろ。WPFではよく聞くフレーズだけど。
ちなみにバインドを直訳すると、結びつけるという動詞。
D3ではデータとDOMの要素を結びつけ、データを可視化していく、このデータをDOMとリンクさせる作業のことをデータバインドするなどというらしい。
実際の例は、下のとおりで。実例のほうが、概念が理解しやすい。
See the Pen
D3_002 by Michinori Takeuuchi (@michi1982)
on CodePen.
サンプルでは、データを1から10の整数値でdatasetという名前の配列に格納している。selectとかappendとかはさっき出てきたけど、data( ) とか enter ( )とかは新出。
var dataset = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
d3.select("body")
.selectAll("p")
.data(dataset)
.enter()
.append("p")
.text("Pタグを追加してみた")
少し特殊な感じがするが、むりくり各メソッドの内容を解釈すると、
selectAll(“p”)
これで、body要素にある全てのpタグ(ここでは、実際にまだ存在してないけど、これから存在するものに的な意味合い)に対して、処理をおこなう。
data( dataset )
次にdatasetに格納されているすべてのデータ点に対して、
enter( )
要素をバインドするためのプレースホルダ(場所取り用)を確保する。ブレースホルダは、データ点数分確保される。データセット分場所とってます。次になにやるか決めてねみたいな感じかな。
append(“p”)
ブレースホルダに対して、実際にp要素を実際に追加する。取った場所でpします。
無名関数をうまくつかう
上の例だと、データ点数分p要素を追加しただけで、あんまり処理的には、意味なかった。データの値を実際に使うには、無名関数をうまく使う必要がある。
下では、前の例で”Pタグを追加してみた”という文字列の代わりにdatasetの要素を表示させる例。
See the Pen
D3_003 by Michinori Takeuuchi (@michi1982)
on CodePen.
変更点は、.text(“Pタグを追加してみた”) の部分を下のコードに変更しただけになる。
.text(function(d){return d;})
function(d) { } は無名関数で、引数dはデータの各要素になる。別に引数名はdじゃなくても
.text(function(s){return s;})
でもなんでもいい。関数はdatasetの要素数の分だけ呼ばれ、textにその要素をそのまま何もしないで、返している。いまdatasetは1-10の整数値なので、関数は10回よばれ、1, 2, 3, …, 9,10の順にtextに値を渡している。
ちょっと凝って、色を変えるときはこうする。
See the Pen
D3_004 by Michinori Takeuuchi (@michi1982)
on CodePen.
末尾にこれを追加しただけ。
.style("color","red");
これも無名関数をつかうと、もっと賢くなる。1-5と6-10で文字色をかえてみた。
See the Pen
D3_005 by Michinori Takeuuchi (@michi1982)
on CodePen.
colorのプロパティ設定を無名関数を使って、動的に設定した。
.style("color",function(d){
if (d<6){
return "blue";
}else{
return "red";
}
SVGで描画する
D3のメソッドを利用して、最初に勉強したSVGで円を描いてみる。
See the Pen
D3_006 by Michinori Takeuuchi (@michi1982)
on CodePen.
まず、これでSVGを定義する。
var svg = d3.select("body").append("svg");
svg.attr("width",500)
.attr("height",100);
この例みたいにsvg という変数にbodyに追加したsvg要素のリファレンスを設定することで、後々、svg.xxxとかsvg.yyyとかしなくていい。
svg.attrはプロパティを変更するためのメソッド、ここでは、svgのサイズを設定している。
さっきp要素を追加した時と同じようにSVGのcircle要素を追加する。データがSVGのcircleにバインドされる。
var circles =svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
このままだと、同じ位置に円が重ね描きされるだけなので、cxの値を無名関数で計算する。
function(d,i)のiは配列のインデックスで、i*50とかして、配列のあたまから順番に右側にずらしながら円を描くようにしている。
circles.attr("cx",function(d,i){return i*50+20;})
.attr("cy",50)
.attr("r",function(d){return d*2;})
散布図を描く
datasetを二次元の変数に拡張すれば、上のコードを微修正するだけで、散布図っぽいのがかける。
See the Pen
D3_007 by Michinori Takeuuchi (@michi1982)
on CodePen.
二次元の配列はこう定義した。
var dataset=[
[5,12],[8,2],[1,1],[13,20],[13,7],
[4,2],[14,18],[19,7],[0,0],[11,18]
]
散布図を表示するには、データのx座標を円の”cx”にして、y座標を”cy”にすればいい。無名関数で取得した変数dは二次元の場合は、d[0], d[1]でそれぞれの要素の値をゲットできる。
circles.attr("cx",function(d){return d[0]*30;})
.attr("cy",function(d){return d[1]*4;})
.attr("r",5);
ラベルを追加する
データ点の近くに座標をテキスト表示する。
See the Pen
D3_007 by Michinori Takeuuchi (@michi1982)
on CodePen.
さっきの散布図のコードに座標値のラベルを表示するには、下のコードを追加すればいい。テキスト位置は、xとyで決め、fillで文字色を決められる。
label.text(function(d){return d[0]+","+d[1];})
.attr("x",function(d){return d[0]*30;})
.attr("y",function(d){return d[1]*4;})
.attr("font-size","13px")
.attr("fill","red")
スケール
さっきのテキストの表示位置にしろ、円の位置にしろ、データの値を直接使わないで、d[0]*30とか、d[1]*4とかで、自分の画面サイズに合うように適当に数値データをスケーリングしていた。
d3にはscaleLinearという便利な機能があるので、これを使うのが一般的らしい。
See the Pen
D3_008 by Michinori Takeuuchi (@michi1982)
on CodePen.
大事なとこはここ。scaleLinear() には、ドメインとレンジというメソッドがある。
ドメインが入力値の範囲、レンジが出力値の範囲になる。scaleXではドメインを0~20にしている。datasetのx軸が20未満なので。そしてレンジは0~500にしている。svgのwidthを500ピクセルにしているので、その範囲に収まるようにした。するとscaleX関数はscaleX(10)とすると、その戻り値は、250になり、入力と出力の関係を線形変換してくれる。
最後の円のcxとcyはそれぞれ、scaleX(d[0])、scaleY(d[1]) で値を決められるようになり、これまでの訳の分からないスケーリングゲインの帳尻合わせから解放された。
var scaleX = d3.scaleLinear()
.domain([0,20])
.range([0,500]);
var scaleY = d3.scaleLinear()
.domain([0,20])
.range([0,100]);
circles.attr("cx",function(d){return scaleX(d[0]);})
.attr("cy",function(d){return scaleY(d[1]);})
.attr("r",5);
パディング
散布図を描くとsvgの枠からはみ出たデータがうまく表示されないので、パッディング域を設定して、すべてのデータが表示されるようにする。
See the Pen
D3_008 by Michinori Takeuuchi (@michi1982)
on CodePen.
変更点はこれだけ。
var padding = 20;
var scaleX = d3.scaleLinear()
.domain([0,20])
.range([padding,500-padding]);
出力範囲の上下限をpadding分オフセットして、狭くしただけ。これでsvgのサイズと散布図の出力範囲の差だけ余白ができて、これまで、見切れていたデータもしっかりと表示することができる。
データ範囲に合わせて、スケールを変更
これまでの方法だとデータがある程度の範囲にきまった状態でくるか、静的なデータを取り扱うシチュエーションでないとうまくいかない。
配列の要素の値に応じて、スケーリングを変えるには要素の最小と最大を取得して、出力するレンジを変えるといい。
入力ドメインの最大値の設定をこうかえる。
var scaleX = d3.scaleLinear()
.domain([0,d3.max(dataset,function(d){return d[0];})])
.range([padding,500-padding]);
var scaleY = d3.scaleLinear()
.domain([0,d3.max(dataset,function(d){return d[1];})])
.range([padding,100-padding]);
大事なとこはここ。
d3.max(dataset,function(d){return d[0];})
maxメソッドにdatasetを渡し、無名関数をつかって、x座標の最大値をとるように指定する。y座標の場合は、こう。
d3.max(dataset,function(d){return d[1];})
軸
軸がないとグラフとはいえないので、軸を追加する。d3では、x軸の表示にはd3.axisBottom()をつかう。d3.axisTop()というのもあるが、軸ラベル位置の違いで、bottomは下側、topは上側につくようになる。
See the Pen
D3_010 by Michinori Takeuuchi (@michi1982)
on CodePen.
大事なのは、ここ。
var xAxis = d3.axisBottom()
.scale(scaleX);
svg.append("g")
.call(xAxis);
axisBottomにはscale()というメソッドがあり、これにさっき設定したscaleLinearのリファレンスを渡す。svgにappend(“g”)でg要素というものを追加する。
g要素はいろいろなsvg要素をいれるコンテナ的な役割をして、中の要素を一括で移動したり、回転したり、できる。
call (xAxis)で、前のリンクから受け取ったg要素にxAxisを追加できる。call() は具現化のトリガで、具現化先は、前のリンク。
軸をカスタム
軸のラベルのフォントとか、ラインの色とかを変えたい場合は、g要素にクラス名を定義して、styleタグ内で、適当な属性を編集する。
See the Pen D3_011 by Michinori Takeuuchi (@michi1982) on CodePen.
attr(“class”,”hoge”)で適当な名前をつける。
svg.append("g")
.call(xAxis)
.attr("class","axis");
styleタグ内でプロパティを適当に編集。
<style>
.axis path,
.axis line{
stroke:blue;
}
.axis text{
font-size:13px;
fill:blue;
}
</style>
CSSとSVGでプロパティが微妙に違うので、注意。下のリンクで要チェック。
Transformで軸を下に表示する
軸の位置を下にもっていくには、g要素をtransformで平行移動する。
See the Pen D3_012 by Michinori Takeuuchi (@michi1982) on CodePen.
大事なのはここ。
svg.append("g")
.call(xAxis)
.attr("class","axis")
.attr("transform","translate(0,"+(h-padding)+")");
translate(x, y) でg要素を平行移動できる。ここでは、下にずらしたいだけなので、translate (0, h-padding) としてる。ちなみに今回のコードから、svgのサイズを(幅w,高さh) =(500,300)としてる。
あともうひとつの変更点として、y軸方向の反転処理をくわえてる。いままで、y軸の正方向を画面↓方向になっていたので、これを画面↑方向が正になるようにした。これは、scaleをつかうと簡単で、出力レンジの最小と最大をひっくり返すだけでいい。
var scaleY = d3.scaleLinear()
.domain([0,d3.max(dataset,function(d){return d[1]})])
.range([h-padding,padding]);
Y軸も完成させる
x軸と同じようにy軸も追加する。
See the Pen D3_012_2 by Michinori Takeuuchi (@michi1982) on CodePen.
データの更新
散布図が大体完成したので、静的なデータじゃなくて、周期的に乱数を発生して、データを更新するようにしてみる。同時にスケールも動的に変更するようにした。
変更点はあまり多くなくて、下の関数を追加したぐらい。コメントに説明を書いておく
setInterval(update, 200); //200ms毎にupdate関数をよぶ
function update(){
max_x++; //見た目つまらないので、乱数の値をちょっとずつ大きくする
max_y++;
var newX=Math.floor(Math.random() * max_x); //新しい乱数x
var newY=Math.floor(Math.random() * max_y); //新しい乱数y
dataset.push([newX,newY]); //datasetの末尾にデータを追加
dataset.shift(); //配列の先頭のデータを削除
//入力ドメインを更新
scaleX.domain([0, d3.max(dataset, function(d) { return d[0]; })]);
scaleY.domain([0, d3.max(dataset, function(d) { return d[1]; })]);
//円を再描画
svg.selectAll("circle")
.data(dataset)
.attr("cx", function(d) {
return scaleX(d[0]);
})
.attr("cy", function(d) {
return scaleY(d[1]);
});
//テキストを再表示
svg.selectAll("text")
.data(dataset)
.text(function(d){return d[0]+","+d[1];})
.attr("x",function(d){return scaleX(d[0]);})
.attr("y",function(d){return scaleY(d[1]);})
//X軸の更新
svg.select(".xaxis")
.transition()
.duration(500)
.call(xAxis);
//Y軸の更新
svg.select(".yaxis")
.transition()
.duration(500)
.call(yAxis);
}
こんなところで、D3を使って、最終的には、散布図が書けるようになった。
静的なデータだと、エクセル使えば簡単に表示できるが、データを動的に取得して、スケーリングするあたりが、ちょっとかっこいい。
D3のホームページでは、このサンプルと比較にならないぐらい、魅惑的なデザインのグラフがたくさんある。いろいろ流用できたら、レポートの幅がひろがりそうだ。
ひきつづき、調査をつづけたい。
happy d3 !