D3.js 基本8 – transformで一挙に移動+回転

このページは"D3.jsチュートリアル"シリーズの8 / 20です。

transformのtranslateやrotateを使うと、要素の移動や回転ができます。一見地味に聞こえますが、実はD3で一番おもしろい所かもしれません。

どうしてかというと、transformで動かした要素はxやyの原点自体が移動するので、xやyが0のままでも動かせますし、もしxやyを変えると移動した先でその分だけ移動することになります。
たとえばtransformのrotateを使うと、円状に回転しながらも、xやyで位置を変えられます。

もうひとつ便利なのは、g要素をtransformで移動することで、そこに含まれているいろんな要素も一緒に移動できることです。テキストと円などを同時に動かしたい場合が多いので、そういう時に便利です。

 

transformのtranslateで縦横に移動する

基本は簡単です。gやcircleなど移動させたい要素の属性としてtransform、属性の値としてtranlate(x,y)を設定します。そうするとx分だけ横に、y分だけ縦に移動します。
HTMLでは結果的に下のようになるようにします。

<g transform="translate(380,300)">
</g>

D3でこの形を作るためには、たとえば下のようにします。

svg.append("g")
	.attr("transform","translate(" + 100 + "," + 200 + ")");

すこし面倒ですが、transformという属性に”translate(100,200)”を文字列として入れるために、+を使って文字列としてつないでいます。

これで原点を横に100、縦に200移動したことになります。
たとえばその中のcircleのcxとcyが0でも、画面上ではx=100,y=100の位置に円が移動しています。
さらにたとえばcxを-60にすると、原点のx=100から-60だけ移動することになるので、画面上のx=40のところに表示されます。
下のコードではcxもcyも0ですが、svgをtranslate(100,100)で移動した後なら、そのsvg要素に含まれるcircleは100,100の場所に来ます。

svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 20)
.attr("fill","blue");

下に具体的な例があります。

例:

画像をクリックするとアニメーションのページが開きます
画像をクリックするとアニメーションのページが開きます

コード:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
</head>
<body>

	<script src="http://d3js.org/d3.v3.min.js"></script>

	<script>
var width = 760,
height = 600;

var svg = d3.select("body").append("svg")
			.attr("width", width)
			.attr("height", height);

// transformのtranslateでgを横にwidth/2、縦にheight/2、つまり画面中央に移動。
var translated = svg.append("g")
					.attr("transform","translate(" + width/2 + "," + height/2 + ")");

// translateで移動しない場合。
var noTranslated =
svg.append("circle")
	.attr("cx", 0)
	.attr("cy", 0)
	.attr("r", 20)
	.attr("fill","blue");

// translateで移動する場合。
translated.append("circle")
		.attr("cx", 0)
		.attr("cy", 0)
		.attr("r", 20)
		.attr("fill","blue");

// アニメーションでtranslateの動きを表示。
noTranslated.transition().duration(1000)
			.attr("transform","translate(" + width/2 + "," + height/2 + ")");

	</script>

</body>
</html>

解説:
2つの円はどちらもcx=0、cy=0です。左上の円が、translationしていないもの。中央の円が、translation(width/2,height/2)に移動したものです。

var translated = svg.append("g")
                    .attr("transform","translate(" + width/2 + "," + height/2 + ")");

具体的にはここで画面の画面の中央にg要素を移動し、その中にcxとcyが0の円を作っています。
また、移動をわかりやすく見るために、最後にtransition()でアニメーションも作っています。

 

transformのrotateで回転する

rotateもtranslateと同じように使います。rotate(10)なら、原点を中心に10度回転します。D3ではたとえば下のようになります。

svg.append("g").attr("transform","rotate(" + 10 + ")" );

では具体的な例です。

例:

d3基本8-2rotate_circle
画像をクリックするとアニメーションのページが開きます

コード:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
</head>
<body>

	<script src="http://d3js.org/d3.v3.min.js"></script>

	<script>
var width = 760,
height = 600,
radius = width/4;

var svg = d3.select("body").append("svg")
			.attr("width", width)
			.attr("height", height);

// translateで画面中央に移動。
var translated = svg.append("g")
			.attr("transform","translate(" + width/2 + "," + height/2 + ")");

var data = [10,21,34,23,11,3,5,68,9,98,100,26,75,45,78,98,56,36,2,13,76,43,21,65,86,24,10,98,77,45,96,46];

// d3.extent()で、データの最大値と最小値が[最大値,最小値]で出る。
var dataExtent = d3.extent(data);	

// データリストの位置が0で角度0、最後の位置で360度になるように均等に角度を割りふる。
var r = d3.scale.linear()
			.domain([0,data.length])
			.range([0,360]);

var y = d3.scale.linear()
			.domain(dataExtent)
		    .range([radius, 0]); // 0で一番外側になるようにradiusと0の位置を本来と逆にしている。

var preRotated = translated.selectAll(".pre_rotated").data(data).enter()
							.append("g")
							.attr("class","pre_rotated");

var circles = preRotated.append("circle")
		.attr("cx", function(d){ return d;})
		.attr("cy", y(0))
		.attr("r", 6)
		.attr("fill", "red");

// データリストの位置=iごとにr()で一定角度で回転する。
circles.transition().duration(1000)
						.attr("transform",function(d,i){ return "rotate(" + r(i) + ")"; });	

	</script>

</body>
</html>

解説:
アニメーションを見るとわかりやすいですが、最初はtranslation(width/2,height/2)で原点を画面中央に移動した後、単純に円を横に並べています。下のように、それぞれの円にrotateを適用するだけで、原点を中心として回りこんでいます。r(i)は、データのインデックスを360度に割りふるために作ったスケール変換です。
ただし、この例ではアニメーションで表示するために、前にtransition().duration(1000)も挟んでいます。

circles.transition().duration(1000)
                        .attr("transform",function(d,i){ return "rotate(" + r(i) + ")"; });

この例では、このように均等に円周に割りふるために、少し工夫をしています。
円のcyに使うスケールは、普通なら.range([0,raidus])のように0を小さい方に設定するところを、下のコードのように.range([radius,0])と逆にしています。これで、普通ならcy=y(0)で原点の中央に円が集まってしまうところを、原点を中心とした円周を回るように移動できます。

var y = d3.scale.linear()
			.domain(dataExtent)
		    .range([radius, 0]); // 0で一番外側になるようにradiusと0の位置を本来と逆にしている。

さらに、すべてのデータを被りなく円周にマッピングするための工夫もあります。
下のコードのように、スケールに使う入力範囲を、.domain[0,data.length]として0~データの数にしています。
さらにスケールの出力範囲を.range([0,360])と、0~360度にしています。
このスケールを使えば、データのインデックス番号ごとに、0~360度の間を均等に割り振ってくれます。

// データリストの位置が0で角度0、最後の位置で360度になるように均等に角度を割りふる。
var r = d3.scale.linear()
			.domain([0,data.length])
			.range([0,360]);

使う時は、下のコードのようにインデックスiをr(i)として角度として変換しています。

// データリストの位置=iごとにr()で一定角度で回転する。
circles.transition().duration(1000)
						.attr("transform",function(d,i){ return "rotate(" + r(i) + ")"; });

 

3.ライン、円、テキストを同時に移動+回転する:

最後に少し複雑な例をひとつ。g要素にtransformのtranslationやrotateを適用することで、そこに含まれている要素も一緒に原点が移動します。これがtransformの一番便利なところと言えます。

例:

d3基本8-3rotate_circle_line_text
画像をクリックすると実際のページが開きます

コード:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<style>
	body { background: AliceBlue;}
	</style>
</head>
<body>

	<script src="http://d3js.org/d3.v3.min.js"></script>

	<script>
var width = 760,
height = 600,
radius = width/4;

var svg = d3.select("body").append("svg")
			.attr("width", width)
			.attr("height", height)
			.append("g")
			.attr("transform","translate(" + width/2 + "," + height/2 + ")");// translateで画面中央に移動。

// 性別と点数のリスト。
var data = [{"点数":100,"性別":"男"},{"点数":67,"性別":"男"},{"点数":86,"性別":"男"},{"点数":14,"性別":"女"},{"点数":66,"性別":"男"},{"点数":77,"性別":"男"},{"点数":86,"性別":"男"},{"点数":96,"性別":"男"},{"点数":10,"性別":"女"},{"点数":23,"性別":"女"},{"点数":45,"性別":"女"},{"点数":89,"性別":"男"},{"点数":66,"性別":"女"},{"点数":78,"性別":"男"},{"点数":24,"性別":"女"},{"点数":57,"性別":"男"},{"点数":84,"性別":"女"},{"点数":54,"性別":"女"},{"点数":56,"性別":"男"},{"点数":77,"性別":"男"},{"点数":79,"性別":"男"},{"点数":67,"性別":"男"},{"点数":77,"性別":"男"},{"点数":86,"性別":"男"},{"点数":99,"性別":"男"},{"点数":97,"性別":"女"},{"点数":86,"性別":"女"},{"点数":66,"性別":"男"},{"点数":76,"性別":"女"},{"点数":77,"性別":"女"},{"点数":86,"性別":"男"},{"点数":14,"性別":"男"},{"点数":53,"性別":"男"},{"点数":76,"性別":"女"},{"点数":99,"性別":"男"},{"点数":74,"性別":"女"},{"点数":88,"性別":"男"},{"点数":32,"性別":"女"},{"点数":28,"性別":"男"},{"点数":21,"性別":"男"}]

// d3.extent()で、データの最大値と最小値が[最大値,最小値]で出る。
var dataExtent = d3.extent(data,function(d){ return d["点数"]});
var r = d3.scale.linear()
			.domain([0,data.length])
			.range([0,360]);

var y = d3.scale.linear()
			.domain(dataExtent)
		    .range([radius, 0]); // 0で一番外側になるようにradiusと0の位置を本来と逆にしている。

var line = d3.svg.line()
			.x(0) // xは常に0
			.y(function(d) { return y(d); });

// データの数分だけ、gを作って回転。
var rotated = svg.selectAll(".surroundings").data(data).enter()
				.append("g")
				.attr("class","surroundings")
				.attr("transform",function(d,i){ return "rotate(" + r(i) + ")"; });

var linePath = rotated.append("path")
					.attr("d",function(d){ return line([0,d["点数"]]);}) // 0から点数分だけyに伸びる。
					.attr("stroke", function(d){ if (d["性別"] == "男"){ return "skyblue";} else { return "orange"; }})
					.attr("stroke-width",4)
					.attr("stroke-linecap","round");

rotated.append("circle")
		.attr("cx", 0)
		.attr("cy", function(d){ return y(0); })
		.attr("r", 7)
		.attr("fill", function(d){ if (d["点数"]>30) { return "gray";} else { return "red";}; });	

rotated.append("text")
		.attr("x",0)
		.attr("y", function(d){ return y(-10); })
		.attr("text-anchor", "middle") // テキストの位置を中央揃えに。
		.attr("width",100)
		.attr("height",100)
		.attr("stroke","black")
		.text(function(d,i){ return i; }); // リスト内の位置をテキストとして出す。

	</script>

</body>
</html>

解説:
ライン、円、テキストが円周に沿ってマッピングされています。データの意味は学生の点数と性別です。青が男性・オレンジが女性、ラインの長さが点数、円が赤いのは点数が30点以下の人です。さらにテキスト要素を使って、インデックス番号もつけてみました。
基本的には、2と3の例の応用です。円もラインもテキストも、xの位置は0ですが、下のようにそれを包んでいるgを回転しているので、一緒に回転しています。

var rotated = svg.selectAll(".surroundings").data(data).enter()
                .append("g")
                .attr("class","surroundings")
                .attr("transform",function(d,i){ return "rotate(" + r(i) + ")"; });

コメントを残す