Partition Layoutで学ぶD3.jsでのデータ可視化の基礎

こんにちは。エンジニアの GM です。
D3.js が話題になって久しい今日この頃、遅ればせながら私も D3.js を触ってみました。ギャラリー を見ると短いコードでクールなグラフが実装されており、簡単に似たようなものを作ることができるのですが、使用するデータや、細かい表示の差異によってある程度カスタマイズする必要があります。今回は基礎的なことについて簡単に触れた後、2階層の円グラフを描画するためのコードを例に出し、D3.js の基本的な使い方を紹介したいと思います。

目次

  1. SVG要素を作成する
  2. 作成した要素にデータをバインドする(data())
  3. SVG 描画
  4. データのバインド
  • データとむすびつけたい要素を選択する
  • data() で要素とデータをむすびつける
  • enter() でデータとむすびつけた要素を作成する
(function() {
// svg 領域描画準備
var width = 900;
var height = 500;
var margin = {top: 50, right: 10, bottom: 10, left: 50};
var radius = Math.min(width - margin.right - margin.left, height - margin.top - margin.bottom);
var color = d3.scale.category20b();
var partition = d3.layout.partition()
// プロパティ data を children とする
.children(function(d, depth) {
return d.data !== void(0) ? d.data : null;
})
// プロパティ time を value とする
.value(function(d) {
return d.time;
})
.size([2 * Math.PI, radius / 3]);
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.innerRadius(function(d) {
return d.y;
})
.outerRadius(function(d) {
return d.y + d.dy;
});
d3.json("data/projects.json", function(data) {
// SVG 領域定義
var svg = d3.selectAll("#edit-area")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + (width - margin.left - margin.right) / 2 + "," + (height - margin.top - margin.bottom) / 2 + ")");
// データバインド
var arcs = svg.selectAll("g.arc")
.data(partition.nodes(data).slice(1))
.enter()
.append("g")
.attr("class", "arc");
// path 描画
arcs.append("path")
.attr("d", function(d) {
return arc(d);
})
.style("fill", function(d, i) {
return color(i);
});
arcs.on("mouseover", function(d) {
svg.append("text")
.attr("id", "tooltip")
.attr("x", arc.centroid(d)[0])
.attr("y", arc.centroid(d)[1])
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(function() {
if (d.depth === 1) {
return d.project + ": " + d.value + "[h]";
} else {
return d.value + "[h]";
}
});
})
.on("mouseout", function() {
d3.select("#tooltip").remove();
});
});
}).call(this);
ここでは以下のデータを描画します。
{
"name": "projects",
"data": [
{
"project": "humming bird",
"data": [
{"type": "task" , "time": 100},
{"type": "bug fix" , "time": 100},
{"type": "other" , "time": 5}
]
},
{
"project": "free bird",
"data": [
{"type": "task" , "time": 200},
{"type": "bug fix" , "time": 100},
{"type": "other" , "time": 5}
]
}
]
}
基本的な方針
基本的な方針としては、SVG 領域内に path 要素を作成し、d 属性で円弧、直線、円弧と定義することで、扇形の根本から小さな扇形を切り取った形(何と呼ぶのか知りませんが、簡単のため以下扇形と呼ぶことにします)を描画していきます。ただ、これを普通にやろうとするとまず全体の大きさを計算して各要素の割合を算出し、それに応じて中心角の大きさを求め、円弧の始点と終点を求め・・・と大変面倒なことになります。
D3.js にはそのような面倒を大幅に削減してくれる便利機能が多数揃っています。今回のような複雑な円グラフを描くために Partition Layout なる関数群が準備されているので、それを使うことにします。
複雑な円グラフを描くための Partition Layout
Partition Layout を使うためには、ちょっとした準備が必要です。それを行っているのがl.11 - 20 です。
(function() {
// svg 領域描画準備
var width = 900;
var height = 500;
var margin = {top: 50, right: 10, bottom: 10, left: 50};
var radius = Math.min(width - margin.right - margin.left, height - margin.top - margin.bottom);
var color = d3.scale.category20b();
var partition = d3.layout.partition()
// プロパティ data を children とする
.children(function(d, depth) {
return d.data !== void(0) ? d.data : null;
})
// プロパティ time を value とする
.value(function(d) {
return d.time;
})
.size([2 * Math.PI, radius / 3]);
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.innerRadius(function(d) {
return d.y;
})
.outerRadius(function(d) {
return d.y + d.dy;
});
d3.json("data/projects.json", function(data) {
// SVG 領域定義
var svg = d3.selectAll("#edit-area")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + (width - margin.left - margin.right) / 2 + "," + (height - margin.top - margin.bottom) / 2 + ")");
// データバインド
var arcs = svg.selectAll("g.arc")
.data(partition.nodes(data).slice(1))
.enter()
.append("g")
.attr("class", "arc");
// path 描画
arcs.append("path")
.attr("d", function(d) {
return arc(d);
})
.style("fill", function(d, i) {
return color(i);
});
arcs.on("mouseover", function(d) {
svg.append("text")
.attr("id", "tooltip")
.attr("x", arc.centroid(d)[0])
.attr("y", arc.centroid(d)[1])
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(function() {
if (d.depth === 1) {
return d.project + ": " + d.value + "[h]";
} else {
return d.value + "[h]";
}
});
})
.on("mouseout", function() {
d3.select("#tooltip").remove();
});
});
}).call(this);
この中ではまずchildren() を用いて元データ(今回の例では project.json)の構造を明示しています。今回の例では data というプロパティの中に子要素が含まれることを示しています。つまり、
  • data
  • 子要素1
  • 子要素2
という構造のデータであることを伝えています。
続いて、value() で各要素の値を定義します。今回の例では time を指定しています。上記のとおり、children と value を指定したうえで
partition.nodes(data)
とすると、以下のプロパティを持つデータが生成されます。
  • children
  • depth
  • dx
  • dy
  • value
  • x
  • y
元の JSON では最下層の要素のみ
{"type":"task", "time": 100}
のように値が定義されているのですが、value() で値を指定したことにより、その親要素も value というプロパティを持つようになります。そしてその値は、その子要素の value の合計値となっています。今回の例でいうと、以下のようなデータが作られることになります。
  • {“project”: “humming bird”, “value”: 205, …}
  • {“type”: “task”, “value”: 100, …}
  • {“type”: “bug fix”, “value”: 100, …}
  • {“type”: “other”, “value”: 5, …}
ここで得られた値を用いて path 要素を定義していくのですが、そのままでは扇形を描画することができません。扇形を描画するため、size()を用いて x を 2π 倍しています。
svg.arc を用いた扇形の描画
svg.arc() を使うことで、円弧を描くための path 要素の d 属性値を得ることができます。partition.nodes(data) で得られるプロパティ x, y, dx, dy を次のように用いることで、扇形を描画することでできます。
(function() {
// svg 領域描画準備
var width = 900;
var height = 500;
var margin = {top: 50, right: 10, bottom: 10, left: 50};
var radius = Math.min(width - margin.right - margin.left, height - margin.top - margin.bottom);
var color = d3.scale.category20b();
var partition = d3.layout.partition()
// プロパティ data を children とする
.children(function(d, depth) {
return d.data !== void(0) ? d.data : null;
})
// プロパティ time を value とする
.value(function(d) {
return d.time;
})
.size([2 * Math.PI, radius / 3]);
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.innerRadius(function(d) {
return d.y;
})
.outerRadius(function(d) {
return d.y + d.dy;
});
d3.json("data/projects.json", function(data) {
// SVG 領域定義
var svg = d3.selectAll("#edit-area")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + (width - margin.left - margin.right) / 2 + "," + (height - margin.top - margin.bottom) / 2 + ")");
// データバインド
var arcs = svg.selectAll("g.arc")
.data(partition.nodes(data).slice(1))
.enter()
.append("g")
.attr("class", "arc");
// path 描画
arcs.append("path")
.attr("d", function(d) {
return arc(d);
})
.style("fill", function(d, i) {
return color(i);
});
arcs.on("mouseover", function(d) {
svg.append("text")
.attr("id", "tooltip")
.attr("x", arc.centroid(d)[0])
.attr("y", arc.centroid(d)[1])
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(function() {
if (d.depth === 1) {
return d.project + ": " + d.value + "[h]";
} else {
return d.value + "[h]";
}
});
})
.on("mouseout", function() {
d3.select("#tooltip").remove();
});
});
}).call(this);
データのロードからSVG要素の描画
細かい点は他にもいろいろありますが、以上で円グラフを描くための準備ができました。あとは「データのバインド」で学んだことを行うだけです。一点異なるのはデータのロードの方法です。 d3.json() でデータをロードし、続く処理をそのコールバック関数として実装します。
(function() {
// svg 領域描画準備
var width = 900;
var height = 500;
var margin = {top: 50, right: 10, bottom: 10, left: 50};
var radius = Math.min(width - margin.right - margin.left, height - margin.top - margin.bottom);
var color = d3.scale.category20b();
var partition = d3.layout.partition()
// プロパティ data を children とする
.children(function(d, depth) {
return d.data !== void(0) ? d.data : null;
})
// プロパティ time を value とする
.value(function(d) {
return d.time;
})
.size([2 * Math.PI, radius / 3]);
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.innerRadius(function(d) {
return d.y;
})
.outerRadius(function(d) {
return d.y + d.dy;
});
d3.json("data/projects.json", function(data) {
// SVG 領域定義
var svg = d3.selectAll("#edit-area")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + (width - margin.left - margin.right) / 2 + "," + (height - margin.top - margin.bottom) / 2 + ")");
// データバインド
var arcs = svg.selectAll("g.arc")
.data(partition.nodes(data).slice(1))
.enter()
.append("g")
.attr("class", "arc");
// path 描画
arcs.append("path")
.attr("d", function(d) {
return arc(d);
})
.style("fill", function(d, i) {
return color(i);
});
arcs.on("mouseover", function(d) {
svg.append("text")
.attr("id", "tooltip")
.attr("x", arc.centroid(d)[0])
.attr("y", arc.centroid(d)[1])
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(function() {
if (d.depth === 1) {
return d.project + ": " + d.value + "[h]";
} else {
return d.value + "[h]";
}
});
})
.on("mouseout", function() {
d3.select("#tooltip").remove();
});
});
}).call(this);
続いて g 要素にデータをバインドします。
(function() {
// svg 領域描画準備
var width = 900;
var height = 500;
var margin = {top: 50, right: 10, bottom: 10, left: 50};
var radius = Math.min(width - margin.right - margin.left, height - margin.top - margin.bottom);
var color = d3.scale.category20b();
var partition = d3.layout.partition()
// プロパティ data を children とする
.children(function(d, depth) {
return d.data !== void(0) ? d.data : null;
})
// プロパティ time を value とする
.value(function(d) {
return d.time;
})
.size([2 * Math.PI, radius / 3]);
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.innerRadius(function(d) {
return d.y;
})
.outerRadius(function(d) {
return d.y + d.dy;
});
d3.json("data/projects.json", function(data) {
// SVG 領域定義
var svg = d3.selectAll("#edit-area")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + (width - margin.left - margin.right) / 2 + "," + (height - margin.top - margin.bottom) / 2 + ")");
// データバインド
var arcs = svg.selectAll("g.arc")
.data(partition.nodes(data).slice(1))
.enter()
.append("g")
.attr("class", "arc");
// path 描画
arcs.append("path")
.attr("d", function(d) {
return arc(d);
})
.style("fill", function(d, i) {
return color(i);
});
arcs.on("mouseover", function(d) {
svg.append("text")
.attr("id", "tooltip")
.attr("x", arc.centroid(d)[0])
.attr("y", arc.centroid(d)[1])
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(function() {
if (d.depth === 1) {
return d.project + ": " + d.value + "[h]";
} else {
return d.value + "[h]";
}
});
})
.on("mouseout", function() {
d3.select("#tooltip").remove();
});
});
}).call(this);
さらにバインドされたデータに基づき path 要素の属性値を指定して描画します。
(function() {
// svg 領域描画準備
var width = 900;
var height = 500;
var margin = {top: 50, right: 10, bottom: 10, left: 50};
var radius = Math.min(width - margin.right - margin.left, height - margin.top - margin.bottom);
var color = d3.scale.category20b();
var partition = d3.layout.partition()
// プロパティ data を children とする
.children(function(d, depth) {
return d.data !== void(0) ? d.data : null;
})
// プロパティ time を value とする
.value(function(d) {
return d.time;
})
.size([2 * Math.PI, radius / 3]);
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.innerRadius(function(d) {
return d.y;
})
.outerRadius(function(d) {
return d.y + d.dy;
});
d3.json("data/projects.json", function(data) {
// SVG 領域定義
var svg = d3.selectAll("#edit-area")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + (width - margin.left - margin.right) / 2 + "," + (height - margin.top - margin.bottom) / 2 + ")");
// データバインド
var arcs = svg.selectAll("g.arc")
.data(partition.nodes(data).slice(1))
.enter()
.append("g")
.attr("class", "arc");
// path 描画
arcs.append("path")
.attr("d", function(d) {
return arc(d);
})
.style("fill", function(d, i) {
return color(i);
});
arcs.on("mouseover", function(d) {
svg.append("text")
.attr("id", "tooltip")
.attr("x", arc.centroid(d)[0])
.attr("y", arc.centroid(d)[1])
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(function() {
if (d.depth === 1) {
return d.project + ": " + d.value + "[h]";
} else {
return d.value + "[h]";
}
});
})
.on("mouseout", function() {
d3.select("#tooltip").remove();
});
});
}).call(this);
実際の挙動
今回紹介したコードの実際の挙動についてはJSFiddleでご確認いただけます。
2階層の円グラフ描画方法まとめ
以上で2階層の円グラフが描画されます。今回のプログラムで行ったことをまとめます。
  • partition.children(), partition.value() でデータの親子関係を特定し、子要素の value の合計値を親要素の value に持たせる
  • partition.nodes() で path 要素の属性値を定義するためのパラメータ(x, y, dx, dy, …)を計算する
  • path 要素とデータを結びつける
  • svg.arc() で partition.nodes()から得られたパラメータ(x, y, dx, dy)を path 要素の d 属性値に変換する
上記各種メソッドを使うことで、特に面倒な計算をすることなく、元のデータを反映した DOM を描画することができました。
まとめ
ここまで、初歩的なところから始めて2階層の円グラフを描くことができるようになりました。今回行ったことを一般化してみます。
  • 元データから DOM を描画するためのデータを生成する
  • データと DOM をむすびつける
  • むすびつけられたデータから DOM の属性値を生成する
今回、2階層の円グラフを描くために、Partition Layout を用いて元データから DOM を描画するためのデータを生成し 、svg.arc を用いてデータから DOM の属性値を生成しました。D3.js には、円グラフに限らず複雑なデータ可視化を実現するための便利機能が多数準備されていますが、基本的な使い方は変わりません。サンプルもたくさんありますのでこれを機に D3.js を使ったデータ可視化に取り組まれてみてはいかがでしょうか。