SVG
d3.js

d3.js超初心者向け ①→②を表現してみる

More than 5 years have passed since last update.

d3.js凄そうなんですけど、ちょっと応用しようとすると途端に表示されなくなったりとか、意図と違ったりとかで、挫折する気がします。

そんな超初心者向けのtipsです。


この記事で行うこと


  • d3.jsで丸を描く

  • d3.jsで線を引く

  • d3.jsで矢印を描く

  • svgの要素を知る


環境

Mac OS X(10.9.1) + Google Chrome(2014/2/26現在 最新) で確認しています。


準備

コピペして試せるように準備しておきます。

htmlとjavascriptファイルを分けて、javascriptファイル側だけ編集すればいいようにしておきます。

何度もリロードすることになると思うので、d3.jsのファイルはローカルに保存しておいたほうがさくさく進めます。


arrownode.html

<!DOCTYPE html>

<head>
<meta charset="utf-8">
<script src="d3.v3.min.js"></script>
</head>
<body>
<div id='example'></div>
<script src="arrownode.js"></script>
</body>
</html>


arrownode.js

createsvg();

function createsvg () {
// ここを編集していく
}


ここでは、arrownode.html, arrownode.js, d3.v3.min.jsは同じディレクトリに置きます。


1.丸を描く(svgの基本を確認)

まずはd3.jsで丸を1つ描いてみましょう。d3.jsの機能はほとんど使わずに、まずは静的に作ってみます。


arrownode.js

createsvg();

function createsvg () {
// id:exampleが指定されているタグ(ここではdivタグ)の下に、svgを追加します。
// widthとheightを指定します。
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480,
});

// svgの下にcircleを追加します。
// cx,cy:中心座標(x,y)、r:半径を指定します。
svg.append('circle')
.attr({
'cx': 100,
'cy': 90,
'r': 20,
});
};


index.htmlをWEBブラウザに読み込ませると、黒丸が表示されていると思います。

スクリーンショット 2014-02-25 21.44.03.png

d3.jsで丸を描くというのは、実際どうなっているのか要素を検証してみましょう。

<div id="example">

<svg width="640" height="480">
<circle cx="100" cy="90" r="20"></circle>
</svg>
</div>

黒丸を描くというのは、svg配下のcircleという要素で描画されているということがわかります。

d3.selectで指定したタグ以下に、appendしたタグが加わり、attrで指定した要素がそのまま入っていることがわかります。記述した要素がすべて一対一で反映されているのでわかりやすいですね。


2.丸を描く(d3.jsのdata機能を使う)

d3.jsは、Data-Driven Documentsの名の通り、データを主として扱うライブラリですので、データ(ここでは円を描画するために必要なデータ)を中心にした記述をしてみます。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480
});

// 座標(cx,cy)と半径(r)を指定
var c1 = [100, 90, 30];

// dataの挿入方法が独特なので注意が必要
// 詳しくは、[三つの小円](http://ja.d3js.node.ws/document/tutorial/circle.html)参照
var circle = svg.selectAll('circle').data([c1]).enter().append('circle')
.attr({
// enterに入っているデータ一つ一つで下の処理を行う
'cx': function(d) { return d[0]; },
'cy': function(d) { return d[1]; },
'r': function(d) { return d[2]; },
});
};


半径の部分だけ、1の時と変えてみましたが、先ほどとほとんど同じ(ちょっと大きい)になりました。

複雑にしたのに何もメリットがないように見えますが、データが少ないからそう見えるのであって、データが増えても全く同じ方法が使えるところがポイントです。

データを増やすと、こうなります。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480
});

// 指定を2つに
var c1 = [100, 90, 30];
var c2 = [200, 120, 20];

// 指定した値を配列にする
var carray = [c1, c2];

// dataに上で作成した配列を入れる
var circle = svg.selectAll('circle').data(carray).enter().append('circle')
.attr({
'cx': function(d) { return d[0]; },
'cy': function(d) { return d[1]; },
'r': function(d) { return d[2]; },
});
};


丸が2つ出来ましたね。このように、carrayに値を追加するだけで、丸がどんどん増えていきます。

スクリーンショット 2014-02-25 22.10.10.png

要素を確認すると、2つのcircle要素が並んでいることがわかります。

<div id="example">

<svg width="640" height="480">
<circle cx="100" cy="90" r="30"></circle>
<circle cx="200" cy="120" r="20"></circle>
</svg>
</div>


3.丸を描く(色をつけよう)

いつまでも丸が黒いと、気分が沈んでいきますので、色をつけましょう。

せっかくですので、d3.jsがデフォルトで定義している色ライブラリを使ってみます。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480
});

var c1 = [100, 90, 30];
var c2 = [200, 120, 20];
var carray = [c1, c2];

// 10種類の色を返す関数を使う
var color = d3.scale.category10();
var circle = svg.selectAll('circle').data(carray).enter().append('circle')
.attr({
'cx': function(d) { return d[0]; },
'cy': function(d) { return d[1]; },
'r': function(d) { return d[2]; },
// dはデータ要素そのもの、iはindex番号を返す
// color(i)で、n番目の色データを返す
'fill': function(d,i) { return color(i); },
});
};


これで、カラフルな円になります。色の値で悩まなくていいので、素敵ですね。

スクリーンショット 2014-02-25 22.23.23.png

要素を見ると、fillに色情報が追加されているのがわかります。

<div id="example">

<svg width="640" height="480">
<circle cx="100" cy="90" r="30" fill="#1f77b4"></circle>
<circle cx="200" cy="120" r="20" fill="#ff7f0e"></circle>
</svg>
</div>


4.丸に字をつけよう(グループ化もいっしょに)

さて、丸の中に文字を入れたくなりました。svgにはテキストを表示する機能もあるのですが、ちょっと複雑になりますので、基本に立ち返って、まずはどういうsvgなら円とテキストが共存できるのか書いてみましょう。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480,
});

// circle要素とtext要素をgという要素でまとめる
var g = svg.append('g')
.attr({
// 座標はg側で設定する
transform: "translate(100,90)",
});

// circleは中心座標無しで指定する。
// 上にテキストを重ねるので色を追加している
g.append("circle")
.attr({
'r': 20,
'fill': 'lightgreen',
});
// テキストを指定する
g.append("text")
.text("1");
};


円とテキストをgという要素でまとめることで、同一グループであるということになります。座標指定もgのところでまとめられますので便利です。

スクリーンショット 2014-02-25 22.42.37.png

要素を確認すると、circle要素とtext要素がgの下に並びます。

<div id="example">

<svg width="640" height="480">
<g transform="translate(100,90)">
<circle r="20" fill="lightgreen"></circle>
<text>1</text>
</g>
</svg>
</div>

ちなみに、svgは上から下に要素を順番に描画していきますので、circle要素とtext要素の順番を逆にすると、テキスト描いてから円を描くので、テキストが下になってテキストが描画されていないように見えます。


5.丸に字をつけよう(d3.jsのdata機能を使う)

「3.丸を描く」のスクリプトを拡張してテキストを追加します。ついでにテキストが円の真ん中にくるように調整します。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480
});

var c1 = [100, 90, 30];
var c2 = [200, 120, 20];
var carray = [c1, c2];

var color = d3.scale.category10();

var g = svg.selectAll('g')
.data(carray).enter().append('g')
.attr({
// 座標設定を動的に行う
transform: function(d) {
return "translate(" + d[0] + "," + d[1] + ")";
},
});

// g.appendでデータ毎に要素を追加できる
g.append('circle')
.attr({
'r': function(d) { return d[2]; },
'fill': function(d,i) { return color(i); },
});
g.append('text')
.attr({
// 真ん中若干下に配置されるように、文字色は白に。
'text-anchor': "middle",
'dy': ".35em",
'fill': "white",
})
// iは0から始まるので、+1しておく
.text(function(d,i) { return i+1; });
};


テキストが真ん中にきてますね。

スクリーンショット 2014-02-25 23.07.48.png

要素を確認すると、g要素が2つあることと、g要素の中にcircle要素とtext要素があることがわかります。

<div id="example">

<svg width="640" height="480">
<g transform="translate(100,90)">
<circle r="30" fill="#1f77b4"></circle>
<text text-anchor="middle" dy=".35em" fill="white">1</text>
</g>
<g transform="translate(200,120)">
<circle r="20" fill="#ff7f0e"></circle>
<text text-anchor="middle" dy=".35em" fill="white">2</text>
</g>
</svg>
</div>

その2


6.線を引こう(pathの使い方)

円は一通り描いたので、次は線を引きましょう。

svgで線を引く簡単な方法として、pathという機能を使う方法があります。

まずはシンプルに引いてみましょう。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480,
});

var c1 = [100, 90, 30];
var c2 = [200, 120, 20];
var carray = [c1, c2];

// line関数を定義 (x,y)は配列の[0],[1]とする。
var line = d3.svg.line()
.x(function(d) {return d[0];})
.y(function(d) {return d[1];});

// path要素を作成
var path = svg.append('path')
.attr({
'd': line(carray),
'stroke': 'lightgreen',
'stroke-width': 5,
});
};


シンプルにと言いましたが、svgでpathを表現する際の記法が独特なので、d3.svg.line関数に頼るのがよいです。

今回は、前回までのデータは変えずに、d3.svg.line関数を追加して、path要素をappendしました。

結果、このような線が引かれます。

スクリーンショット 2014-02-26 21.33.13.png

要素を確認すると、このようにdのところがd3.svg.line関数の出力結果に置き換わっています。数字は座標通りですが、MとかLとかが追加されていますね。

<div id="example">

<svg width="640" height="480">
<path d="M100,90L200,120" stroke="lightgreen" stroke-width="5"></path>
</svg>
</div>


付録1.線を引こう(3つの点を滑らかに)

試しに配列を3つ渡すと、このようになります。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480,
});

var c1 = [100, 90, 30];
var c2 = [200, 120, 20];
// 3つ目を追加
var c3 = [300, 100, 20];
var carray = [c1, c2, c3];

var line = d3.svg.line()
.x(function(d) {return d[0];})
.y(function(d) {return d[1];});

var path = svg.append('path')
.attr({
'd': line(carray),
'stroke': 'lightgreen',
'stroke-width': 5,
});
};


このように、3点で特に指定しないとpathの両端をつないで色を塗ってしまいます。'fill': 'none'と定義すると、内部の色がなくなり、線のみとなります。

スクリーンショット 2014-02-26 21.35.09.png

'fill': 'none'を指定するついでに、3つの点を滑らかにつないでみましょう。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480,
});

var c1 = [100, 90, 30];
var c2 = [200, 120, 20];
var c3 = [300, 100, 20];
var carray = [c1, c2, c3];

var line = d3.svg.line()
// interpolate指定で点のつなぎ方を指定する。
.interpolate('basis')
.x(function(d) {return d[0];})
.y(function(d) {return d[1];});

var path = svg.append('path')
.attr({
'd': line(carray),
'stroke': 'lightgreen',
'stroke-width': 5,
'fill': 'none',
});
};


点の座標は変えていないのに、指定一つ増やしただけで、超滑らかになっていますね!

スクリーンショット 2014-02-26 21.44.20.png

要素を調べてみると、もう人の手では無理というpathの値になっています。このあたりまでくると、d3.jsの便利さが際立ってきます。

<div id="example">

<svg width="640" height="480">
<path d="M100,
90L116.66666666666664,
95C133.33333333333331,
100,166.66666666666663,
110,199.99999999999997,
111.66666666666666C233.33333333333331,
113.33333333333333,
266.66666666666663,
106.66666666666666,
283.3333333333333,
103.33333333333331L300,100"
stroke="lightgreen" stroke-width="5" fill="none"></path>
</svg>
</div>


7.線に矢印をつけよう(markerの使い方)

d3.jsを勉強していくときに、純粋に矢印を調べようとするとなかなか資料がなくて、svg側から調べるとすぐにわかったりします。

svg要素の基本的な使い方まとめ/7.マーカーの定義と設定を参考にしました。

svgでpathに矢印をつけるのは、wordやexcelのようには簡単ではなく、結構細かい指定が必要になります。

矢印を描くのはちょっと面倒なのですが、svg内でまず矢印の形を定義して、その後pathに対してその定義を割り当てるという方法を取ります。

その6で作った線の一番後ろに、矢印をつけてみます。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480,
});

var c1 = [100, 90, 30];
var c2 = [200, 120, 20];
var carray = [c1, c2];

// defs/markerという構造で、svgの下に矢印を定義します。
var marker = svg.append("defs").append("marker")
.attr({
'id': "arrowhead",
'refX': 0,
'refY': 2,
'markerWidth': 4,
'markerHeight': 4,
'orient': "auto"
});
// 矢印の形をpathで定義します。
marker.append("path")
.attr({
d: "M 0,0 V 4 L4,2 Z",
fill: "steelblue"
});

var line = d3.svg.line()
.interpolate('basis')
.x(function(d) {return d[0];})
.y(function(d) {return d[1];});

var path = svg.append('path')
.attr({
'd': line(carray),
'stroke': 'lightgreen',
'stroke-width': 5,
'fill': 'none',
// pathの属性として、上で定義した矢印を指定します
'marker-end':"url(#arrowhead)",
});
};


奇麗な矢印が出てきました!

スクリーンショット 2014-02-26 22.29.11.png

要素を見ると、javascriptで要素を追加した通りにsvgの構造が出来ていますので、難易度が高いというより、svg知っているかどうかという話になってきそうです。

<div id="example">

<svg width="640" height="480">
<defs>
<marker id="arrowhead" refX="0" refY="2" markerWidth="4" markerHeight="4" orient="auto">
<path d="M 0,0 V 4 L4,2 Z" fill="steelblue"></path>
</marker>
</defs>
<path d="M100,90L200,120" stroke="lightgreen" stroke-width="5" fill="none" marker-end="url(#arrowhead)"></path>
</svg>
</div>


8.丸と線を矢印でつなげよう(全部まとめる)

それではこれまでやってきたことをまとめてみましょう。

丸2つを矢印でつなげてみます。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480,
});

var c1 = [100, 90, 30];
var c2 = [200, 120, 20];
var carray = [c1, c2];

// 矢印定義
var marker = svg.append("defs").append("marker")
.attr({
'id': "arrowhead",
'refX': 0,
'refY': 2,
'markerWidth': 4,
'markerHeight': 4,
'orient': "auto"
});
marker.append("path")
.attr({
d: "M 0,0 V 4 L4,2 Z",
fill: "steelblue"
});

// 色定義
var color = d3.scale.category10();

// 丸と文字のグループ定義
var g = svg.selectAll('g')
.data(carray).enter().append('g')
.attr({
transform: function(d) {
return "translate(" + d[0] + "," + d[1] + ")";
},
});

// 丸定義
g.append('circle')
.attr({
'r': function(d) { return d[2]; },
'fill': function(d,i) { return color(i); },
});

// 文字定義
g.append('text')
.attr({
'text-anchor': "middle",
'dy': ".35em",
'fill': 'white',
})
.text(function(d,i) { return i+1; });

// 線関数定義
var line = d3.svg.line()
.interpolate('basis')
.x(function(d) {return d[0];})
.y(function(d) {return d[1];});

// 線要素定義
var path = svg.append('path')
.attr({
'd': line(carray),
'stroke': 'lightgreen',
'stroke-width': 5,
'fill': 'none',
'marker-end':"url(#arrowhead)",
});
};


さて、どうでしょう!

あれ?? コレじゃない感じがしますね。丸に重ならないように線が引きたいのに、丸の中心まで線がきてしまうのと、矢印が線の終わりからさらに伸びているので何を指しているのかわかりません。

スクリーンショット 2014-02-26 22.36.45.png

ここから先は一工夫が必要になるようです。


9.丸と線を矢印でつなげよう(pathの長さと矢印の位置をコントロールする)

さて、この重なり具合を解消するためにできることは、pathの長さと矢印の位置をコントロールする必要があります。

pathの長さについては、stroke-dasharrayという破線を表示するための属性と、getTotalLengthというpathの長さを調べる関数を使うことで、コントロールできます。

矢印は、refX、refYという属性で相対位置を調整できるのでこちらを使います。


arrownode.js

createsvg();

function createsvg () {
var svg = d3.select("#example").append("svg")
.attr({
width: 640,
height: 480,
});

// pathの計算で使うので、半径と矢印の微調整パラメータを別定義にしている。
var r1 = 30;
var r2 = 20;
var ref1 = 8;
var c1 = [100, 90, r1];
var c2 = [200, 120, r2];
var carray = [c1, c2];

var marker = svg.append("defs").append("marker")
.attr({
'id': "arrowhead",
// 矢印の位置を一番後ろから手前に少しずらす
'refX': ref1,
'refY': 2,
'markerWidth': 4,
'markerHeight': 4,
'orient': "auto"
});
marker.append("path")
.attr({
d: "M 0,0 V 4 L4,2 Z",
fill: "steelblue"
});

var color = d3.scale.category10();

var g = svg.selectAll('g')
.data(carray).enter().append('g')
.attr({
transform: function(d) {
return "translate(" + d[0] + "," + d[1] + ")";
},
});

g.append('circle')
.attr({
'r': function(d) { return d[2]; },
'fill': function(d,i) { return color(i); },
});

g.append('text')
.attr({
'text-anchor': "middle",
'dy': ".35em",
'fill': 'white',
})
.text(function(d,i) { return i+1; });

var line = d3.svg.line()
.interpolate('basis')
.x(function(d) {return d[0];})
.y(function(d) {return d[1];});

var path = svg.append('path')
.attr({
'd': line(carray),
'stroke': 'lightgreen',
'stroke-width': 5,
'fill': 'none',
'marker-end':"url(#arrowhead)",
});

// pathの長さを調べて、丸の半径2個分+矢印を後ろに下げる分の長さを引きます。
var totalLength = path.node().getTotalLength();
var t = totalLength - (r1+r2+ref1);
path.attr({
// 破線の指定を行います。
'stroke-dasharray': "0 " + r1 + " " + t + " " + r2,
// 破線の開始相対位置を指定します
'stroke-dashoffset': 0,
});
};


破線の指定のところは、線を描く長さ、線を描かない長さ、線描く長さというように交互に指定します。

ここでは、最初0描いて(まったく描かないで)、丸1の半径分描かないで、その後計算した長さ分描いて、その後丸2の半径分描かないとしています。計算上ちょっと余るのですが、その先はまた0描いて、丸1の半径分描かないで…と指定を繰り返すので、実質問題なさそうです。

詳しくは、Chained transitions between points on a path/lineの実装が参考になります。

で、描画結果はこのようになりました。だいぶ思った通りになりました。

スクリーンショット 2014-02-26 22.58.01.png

要素を見ると、かなり複雑になっていますが、ここまで一通り記事を眺めたら、なんとなくわかるのではないでしょうか。

<div id="example">

<svg width="640" height="480">
<defs>
<marker id="arrowhead" refX="8" refY="2" markerWidth="4" markerHeight="4" orient="auto">
<path d="M 0,0 V 4 L4,2 Z" fill="steelblue"></path>
</marker>
</defs>
<g transform="translate(100,90)">
<circle r="30" fill="#1f77b4"></circle>
<text text-anchor="middle" dy=".35em" fill="white">1</text>
</g>
<g transform="translate(200,120)">
<circle r="20" fill="#ff7f0e"></circle>
<text text-anchor="middle" dy=".35em" fill="white">2</text>
</g>
<path d="M100,90L200,120"
stroke="lightgreen" stroke-width="5" fill="none" marker-end="url(#arrowhead)"
stroke-dasharray="0 30 46.40306854248047 20" stroke-dashoffset="0"></path>
</svg>
</div>


まとめ

d3.jsを使って何かしようとする時に、どのような構造になるのかがわかっていると、学習もしやすくなりますし、理解もはやくなると思います。

d3.jsの華やかなサンプルやアニメーションに目が眩みがちになりますが、基礎を抑えておくことが勝利への近道です。