埼玉県は日本で一番市町村が多い県だと聞いています。平成の大合併の前の知識なので現在もそうであるかは知りません。しかし市町村が多いことは確かで、同じ県に住んでいてもとても把握できません。今回は埼玉県の公式ホームページから最新のデータをExcel形式で取ってきて、ツリーマップで表示して見ましたのでご紹介します。以下のURLでツリーマップが参照できます。ツリーマップの使い方や効果を確かめたいのが動機でしたが、人口に視点を置いた時の市町村の存在感が直感的に把握できるのが実感できました。
https://s3-ap-northeast-1.amazonaws.com/kuki-app-bucket/saitama/treemap-saitama.html
※(参考)また同じデータから以下の2種類のグラフも描いてみました。
付録1:埼玉県市町村ツリー
https://s3-ap-northeast-1.amazonaws.com/kuki-app-bucket/saitama/tree-saitama.html
付録2:埼玉県市町村パックチャート
https://s3-ap-northeast-1.amazonaws.com/kuki-app-bucket/saitama/pack-saitama.html
#0.D3jsのビルドイン・レイアウト
D3.jsでは複雑な描画を行うためにビルドイン・レイアウトをいくつか提供してくれています。レイアウトは大雑把に言って、2種類に分けられます。hierarchicalとnon-hierarchical(normal)です。それぞれ以下のようなレイアウトがあります
###non-hierarchical(normal) :
- Histogram
- Pie
- Stack
- Chord
- Force
###hierarchical :
- Tree
- Cluster
- Tree map
- Partition
- Pack
今回使ったのはhierarchicalのTreeとTree map、Packの3つです。まずTree mapを詳細にみて、同様な例としてTreeとPackを見ます。
#1.データ構造
埼玉県の公式サイトから入手したデータを、以下のようなCSVファイルに保存します。先頭のヘッダーとregion欄は手動で追加しました。埼玉県では県内を10の地域に分けているので、regionとして指定します。
name,region,size
西区,さいたま地域,88692
北区,さいたま地域,144810
大宮区,さいたま地域,115731
見沼区,さいたま地域,162894
中央区,さいたま地域,99615
桜区,さいたま地域,98227
浦和区,さいたま地域,158868
南区,さいたま地域,186050
緑区,さいたま地域,120535
岩槻区,さいたま地域,110660
川越市,川越比企地域,353190
熊谷市,北部地域,196937
川口市,南部地域,584825
行田市,利根地域,80562
秩父市,秩父地域,61944
所沢市,西部地域,341079
---
CSVファイルを読み込んだら、D3.jsで扱うために以下のような階層化されたjsonデータに変換します。
{"name":"埼玉県地域別人口マップ",
"children":[
{"name":"さいたま地域","children":[
{"name":"西区","region":"さいたま地域","size":"88692"},
{"name":"北区","region":"さいたま地域","size":"144810"},
{"name":"大宮区","region":"さいたま地域","size":"115731"},
{"name":"見沼区","region":"さいたま地域","size":"162894"},
{"name":"中央区","region":"さいたま地域","size":"99615"},
{"name":"桜区","region":"さいたま地域","size":"98227"},
{"name":"浦和区","region":"さいたま地域","size":"158868"},
{"name":"南区","region":"さいたま地域","size":"186050"},
{"name":"緑区","region":"さいたま地域","size":"120535"},
{"name":"岩槻区","region":"さいたま地域","size":"110660"}]},
{"name":"川越比企地域","children":[
{"name":"川越市","region":"川越比企地域","size":"353190"},
{"name":"東松山市","region":"川越比企地域","size":"92125"},
{"name":"坂戸市","region":"川越比企地域","size":"101826"},
{"name":"鶴ヶ島市","region":"川越比企地域","size":"70211"},
{"name":"毛呂山町","region":"川越比企地域","size":"36531"},
{"name":"越生町","region":"川越比企地域","size":"11374"},
{"name":"滑川町","region":"川越比企地域","size":"18800"},
{"name":"嵐山町","region":"川越比企地域","size":"18146"},
{"name":"小川町","region":"川越比企地域","size":"30181"},
{"name":"川島町","region":"川越比企地域","size":"20291"},
{"name":"吉見町","region":"川越比企地域","size":"19026"},
{"name":"鳩山町","region":"川越比企地域","size":"13977"},
{"name":"ときがわ町","region":"川越比企地域","size":"11107"},
{"name":"東秩父村","region":"川越比企地域","size":"2763"}]},
{"name":"北部地域","children":[
{"name":"熊谷市","region":"北部地域","size":"196937"},
{"name":"本庄市","region":"北部地域","size":"77635"},
{"name":"深谷市","region":"北部地域","size":"142996"},
{"name":"美里町","region":"北部地域","size":"10977"},
{"name":"神川町","region":"北部地域","size":"13516"},
{"name":"上里町","region":"北部地域","size":"30357"},
{"name":"寄居町","region":"北部地域","size":"33327"}]},
以下続く
#2.プログラム説明
上のデータ変換は以下の関数pop()で行います。加えてpop()関数は最後に市町村のリストをDOM上に表示します。ここで注目してほしいのはnest()関数です。keyをregionとして、CSVデータをグループ化します。つまり{key:"北部地域"、values:["熊谷市","本庄市"...]}のようなオブジェクトを要素とする配列を作り出します。nest()関数は便利ですね。最後にD3.jsのappend()関数やtext()関数でul要素やli要素をDOMに追加しています。jQueryなどを使わなくとも簡単なDOM操作ができるのはありがたいことです。
function pop() {
d3.csv("saitama201710.csv", function (error,data) { // ** CSVデータをJSON形式で読み込む
// ** keyをregion("さいたま地域"や"川越比企地域"など)で階層化する
var nest = d3.nest()
.key(function(d) {
return d.region;
})
.entries(data);
var children=[];
nest.map( (reg) => {
var child={};
child.name=reg.key; // ** "key" -> "name" 変更
child.children = reg.values; // ** "values" -> "children" 変更
children.push(child);
});
var saitama={};
saitama.name="埼玉県地域別人口マップ";
saitama.children=children; // ** jsonデータ(saitama)への変換完了
// ** 最後にsaitamaとvalueAccessorを設定して、renderします。
chart.nodes(saitama).valueAccessor(size).render();
// ** 階層化されたデータの詳細をリスト表示します。
var list = d3.select("#list");
nest.map( (reg) => {
list.append('h3').text(reg.key);
var ul=list.append('ul');
reg.values.map( (c)=> {
ul.append('li')
.style("display","inline-block")
.style("margin-left","5px")
.text(c.name +"("+c.size+") ");
});
})
});
}
次にこのプログラムの中心部であるTreeMapを設定していきます。データ全体をツリーとみなし、各要素をnodeと表現することがあります。上のプログラムで示したように、nodesにはcsvファイルを変換したjsonデータが入っています。d3.hierarchy(nodes)はnodesをさらに扱いやすく変換し、sum(valueAccessor)でnodeの値(人口であったり市町村数であったり)を計算し、sort(...)でツリー上のnodeをソートします。またroot.leaves()で末端node(葉)のデータのみを抽出できるようになります。
_treemap(root)は各nodeをDOM上に描くときの座標を計算します。TreeMapの根幹的な関数となります。
以下はクラス関数 treemapChart() の中のrenderBody()関数です。
function renderBody(svg) {
if (!_bodyG) {
_bodyG = svg.append("g")
.attr("class", "body");
_treemap = d3.treemap() //<-A
.size([_width, _height])
.padding(1);
}
var root = d3.hierarchy(_nodes) // (参照1) hierarchy()関数でtreemapで扱える階層構造に変換する
.sum(_valueAccessor) // value(size or count)の合計を計算する
.sort(function(a, b) { return b.value - a.value; }); //size合計 or count合計順に並べる
_treemap(root); // ツリーレイアウトのための属性を計算して追加 : x0, x1, y0, y1
var cells = _bodyG.selectAll("g")
// ** root.leaves()はrootの葉オブジェクトをフラットな配列に入れて返す
// ** 葉オブジェクトの描画レイアウトは計算済みなので、階層構造はもはや不要である。
// ** また葉オブジェクトは親オブジェクト(d.parent)も内包している。
.data(root.leaves(), (d)=>{return d.data.name;});
renderCells(cells);
}
(参照1) d3.hierarchy(_nodes)の返すオブジェクト(root)は以下のような属性がセットされています。
- node.data - the associated data, as specified to the constructor.
- node.depth - zero for the root node, and increasing by one for each descendant generation.
- node.height - zero for leaf nodes, and the greatest distance from any descendant leaf for internal nodes.
- node.parent - the parent node, or null for the root node.
- node.children - an array of child nodes, if any; undefined for leaf nodes.
- node.value - the summed value of the node and its descendants; optional, see node.sum and node.count.**
hierarchyのAPI解説は以下を参照
https://github.com/d3/d3-hierarchy/blob/master/README.md#hierarchy
以上でレイアウトが終了したので、実際にボックスを描いていきます。通常のD3.jsと同じやり方になります。以下にRectボックスを描く関数を示します。Text文字の描画は同様なので省きます。cellsは葉ですので、市町村のデータになります。**カラーはcolors(d.parent.data.name)で指定していますので、親(地域名)が同じなら同じカラーが選ばれます。
function renderCells(cells) {
var cellEnter = cells.enter().append("g")
.merge(cells) // update + enter
.attr("class", "cell")
.attr("transform", function (d) {
return "translate(" + d.x0 + "," + d.y0 + ")";
});
renderRect(cellEnter);
renderText(cellEnter);
cells.exit().remove();
}
function renderRect(cellEnter) {
cellEnter.append("rect");
cellEnter
.transition()
.select("rect")
.attr("width", function (d) {
return d.x1 - d.x0;
})
.attr("height", function (d) {
return d.y1 - d.y0;
})
.style("fill", function (d) {
return colors(d.parent.data.name); // 親が同じなら同じ色です
});
}
#3.全ソースコード (treemap)
これまで部分的な説明を行ってきましたが、以下に全ソースコードを掲載します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Treemap</title>
<link rel="stylesheet" type="text/css" href="./styles.css"/>
<script type="text/javascript" src="./d3.js"></script>
</head>
<body>
<script type="text/javascript">
function treemapChart() {
var _chart = {};
//var _width = 1600, _height = 800,
var _width = 1200, _height = 1000,
_colors = d3.scaleOrdinal(d3.schemeCategory20c),
_svg,
_nodes,
_valueAccessor,
_treemap,
_bodyG;
_chart.render = function () {
if (!_svg) {
_svg = d3.select("div#svg").append("svg")
.attr("height", _height)
.attr("width", _width);
}
renderBody(_svg);
};
function renderBody(svg) {
if (!_bodyG) {
_bodyG = svg.append("g")
.attr("class", "body");
_treemap = d3.treemap() //<-A
.size([_width, _height])
//.round(true)
.padding(1);
}
// https://github.com/d3/d3-hierarchy/blob/master/README.md#hierarchy
var root = d3.hierarchy(_nodes) // (参照1) hierarchy()関数でtreemapで扱える階層構造に変換する
.sum(_valueAccessor) // value(size or count)の合計を計算する
.sort(function(a, b) { return b.value - a.value; }); //size合計 or count合計順に並べる
_treemap(root); // ツリーレイアウトのための属性を計算して追加 : x0, x1, y0, y1
var cells = _bodyG.selectAll("g")
.data(root.leaves(), (d)=>{return d.data.name;}); // 葉だけを扱う。hierarchy()関数の成果。
renderCells(cells);
}
function renderCells(cells) {
var cellEnter = cells.enter().append("g")
.merge(cells) // update + enter
.attr("class", "cell")
.attr("transform", function (d) {
return "translate(" + d.x0 + "," + d.y0 + ")";
});
renderRect(cellEnter, cells);
renderText(cellEnter, cells);
cells.exit().remove();
}
function renderRect(cellEnter, cells) {
cellEnter.append("rect");
cellEnter
.transition()
.select("rect")
.attr("width", function (d) {
return d.x1 - d.x0;
})
.attr("height", function (d) {
return d.y1 - d.y0;
})
.style("fill", function (d) {
return _colors(d.parent.data.name); //親が同じなら同じ色です
});
}
function renderText(cellEnter, cells) {
cellEnter.append("text");
cellEnter
.select("text") //<-H
.style("font-size", 11)
.attr("x", function (d) {
return (d.x1 - d.x0) / 2;
})
.attr("y", function (d) {
return (d.y1 - d.y0) / 2;
})
.attr("text-anchor", "middle")
.text(function (d) {
return d.data.name;
})
.style("opacity", function (d) {
d.w = this.getComputedTextLength();
return d.w < (d.x1 - d.x0) ? 1 : 0;
});
}
_chart.width = function (w) {
if (!arguments.length) return _width;
_width = w;
return _chart;
};
_chart.height = function (h) {
if (!arguments.length) return _height;
_height = h;
return _chart;
};
_chart.colors = function (c) {
if (!arguments.length) return _colors;
_colors = c;
return _chart;
};
_chart.nodes = function (n) {
if (!arguments.length) return _nodes;
_nodes = n;
return _chart;
};
_chart.valueAccessor = function (fn) {
if (!arguments.length) return _valueAccessor;
_valueAccessor = fn;
return _chart;
};
return _chart;
}
var chart = treemapChart();
function size(d) { // 人口を数えます
return d.size;
}
function count() { // 単に市町村数の数を数えます
return 1;
}
function flip(chart) { // valueAccessorを交換します
chart.valueAccessor(chart.valueAccessor() == size ? count : size).render();
}
function pop() {
d3.csv("saitama201710.csv", function (error,data) {
//console.log(JSON.stringify(data));
var nest = d3.nest()
.key(function(d) {
return d.region;
})
.entries(data);
var children=[];
nest.map( (reg) => {
var child={};
child.name=reg.key;
child.children = reg.values;
children.push(child);
});
var saitama={};
saitama.name="埼玉県地域別人口マップ";
saitama.children=children;
console.log(JSON.stringify(saitama));
chart.nodes(saitama).valueAccessor(size).render();
// ** 階層化されたデータの詳細をリスト表示します。
var list = d3.select("#list");
nest.map( (reg) => {
list.append('h3').text(reg.key);
var ul=list.append('ul');
reg.values.map( (c)=> {
ul.append('li').style("display","inline-block").style("margin-left","5px").text(c.name +"("+c.size+") ");
});
})
});
}
pop();
</script>
<h1>埼玉県地域別人口ツリーマップ</h1>
<div class="control-group">
<button onclick="pop()">市町村の人口</button>
<button onclick="flip(chart)">市町村数</button>
</div>
<div id="svg">
</div>
<div id="list">
</div>
</body>
</html>
#4.全ソースコード (tree)
埼玉県市町村ツリー
https://s3-ap-northeast-1.amazonaws.com/kuki-app-bucket/saitama/tree-saitama.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Tree</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script type="text/javascript" src="d3.js"></script>
<style type="text/css">
.node circle {
cursor: pointer;
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font-size: 11px;
}
path.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
</style>
</head>
<body>
<script type="text/javascript">
function tree() {
var _chart = {};
//var _width = 1600, _height = 1600,
var _width = 1200, _height = 1000,
//_margins = {top: 30, left: 120, right: 30, bottom: 30},
_margins = {top: 30, left: 120, right: 100, bottom: 30},
_svg,
_nodes,
_i = 0,
_duration = 300,
_bodyG,
_root;
_chart.render = function () {
if (!_svg) {
_svg = d3.select("div#svg").append("svg")
.attr("height", _height)
.attr("width", _width);
}
renderBody(_svg);
};
function renderBody(svg) {
if (!_bodyG) {
_bodyG = svg.append("g")
.attr("class", "body")
.attr("transform", function (d) {
return "translate(" + _margins.left
+ "," + _margins.top + ")";
});
}
_root = d3.hierarchy(_nodes); // (参照1) hierarchy()関数でtreeで扱える階層構造に変換する
render(_root);
}
function render(root) {
var tree = d3.tree() //
.size([
(_height - _margins.top - _margins.bottom),
(_width - _margins.left - _margins.right)
]);
tree(root); // ** 各要素のx,y座標を計算する
renderNodes(root); //
renderLinks(root); //
}
// ★renderNodes(root)
function renderNodes(root) {
// ** 階層化されたrootデータの子孫をフラットな配列として集める
var nodes = root.descendants();
var nodeElements = _bodyG.selectAll("g.node")
.data(nodes, function (d) {
return d.id || (d.id = ++_i); // バインドし、要素にidを振る
});
// 新要素(enter)
var nodeEnter = nodeElements.enter().append("g") // 新要素を配置して、toggleハンドラを設置する
.attr("class", "node")
.attr("transform", function (d) { // (1)まず新要素を配置する(下でtransitionを使っているので必要)
return "translate(" + d.y
+ "," + d.x + ")";
})
.on("click", function (d) { //
toggle(d); // d.children=null をトグルする
render(_root); // toggle後の座標を計算し直して描き直す
});
nodeEnter.append("circle") // 新要素の○を描く
.attr("r", 4);
// 新・既存要素(enter+update)
var nodeUpdate = nodeEnter.merge(nodeElements) // 新・既存要素を配置し直す
.transition().duration(_duration) // 古い座標から新座標への遷移が描かれる。(1)によって新要素が(0,0)からの遷移になるのを防いでいる。
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")"; // <-I
});
nodeUpdate.select('circle')
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff"; // トグル状態で○の色を変える
});
var nodeExit = nodeElements.exit().remove();
nodeExit.select("circle")
.attr("r", 1e-6) //○は十分小さくしてから削除
.remove();
renderLabels(nodeEnter, nodeUpdate, nodeExit);
}
function renderLabels(nodeEnter, nodeUpdate, nodeExit) {
nodeEnter.append("text")
.attr("x", function (d) {
return d.children || d._children ? -10 : 10; //
})
.attr("dy", ".35em")
//.attr("dy", ".70em")
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start"; //
})
.text(function (d) {
return d.data.name;
})
.style("fill-opacity", 1e-6);
nodeUpdate.select("text")
.style("fill-opacity", 1);
nodeExit.select("text")
.style("fill-opacity", 1e-6)
.remove();
}
// ★renderLinks(root)
function renderLinks(root) {
var nodes = root.descendants().slice(1);//大元のrootは削除して置く
var link = _bodyG.selectAll("path.link")
.data(nodes, function (d) {
return d.id || (d.id = ++_i);
});
link.enter().insert("path", "g") //
.attr("class", "link")
.merge(link)
.transition().duration(_duration)
.attr("d", function (d) { //pathを描く
return generateLinkPath(d, d.parent);
});
link.exit().remove();
}
function generateLinkPath(target, source) {
var path = d3.path();
path.moveTo(target.y, target.x);
path.bezierCurveTo((target.y + source.y) / 2, target.x,
(target.y + source.y) / 2, source.x, source.y, source.x);
return path.toString();
//path=M362.5,565.3698630136986C181.25,565.3698630136986,181.25,624.4383561643835,0,624.4383561643835
}
function toggle(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
_chart.width = function (w) {
if (!arguments.length) return _width;
_width = w;
return _chart;
};
_chart.height = function (h) {
if (!arguments.length) return _height;
_height = h;
return _chart;
};
_chart.margins = function (m) {
if (!arguments.length) return _margins;
_margins = m;
return _chart;
};
_chart.nodes = function (n) {
if (!arguments.length) return _nodes;
_nodes = n;
return _chart;
};
return _chart;
}
var chart = tree();
function pop() {
d3.csv("saitama201710.csv", function (error,data) {
var nest = d3.nest()
.key(function(d) {
return d.region;
})
.entries(data);
var children=[];
nest.map( (reg) => {
var child={};
child.name=reg.key;
child.children = reg.values;
children.push(child);
});
var saitama={};
saitama.name="埼玉県地域別人口マップ";
saitama.name="埼玉県";
saitama.children=children;
console.log(JSON.stringify(saitama));
chart.nodes(saitama).render();
});
}
pop();
</script>
<div class="control-group">
<button onclick="flare()">埼玉県市町村ツリー</button>
</div>
<div id="svg">
</div>
</body>
</html>
#5.全ソースコード (pack)
埼玉県市町村パックチャート
https://s3-ap-northeast-1.amazonaws.com/kuki-app-bucket/saitama/pack-saitama.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Pack</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<script type="text/javascript" src="d3.js"></script>
<style type="text/css">
text {
font-size: 11px;
pointer-events: none;
}
text.parent {
fill: steelblue;
}
text.child {
display: none;
}
circle {
fill: #ccc;
stroke: #999;
pointer-events: all;
}
circle.parent {
fill: steelblue;
fill-opacity: .1;
stroke: steelblue;
}
circle.parent:hover {
stroke-width: .5px;
}
circle.child {
pointer-events: none;
}
</style>
</head>
<body>
<script type="text/javascript">
function pack() {
var _chart = {};
var _width = 1280, _height = 800,
_svg,
_valueAccessor,
_nodes,
_bodyG;
_chart.render = function () {
if (!_svg) {
_svg = d3.select("body").append("svg")
.attr("height", _height)
.attr("width", _width);
}
renderBody(_svg);
};
function renderBody(svg) {
if (!_bodyG) {
_bodyG = svg.append("g")
.attr("class", "body");
}
var pack = d3.pack() //
.size([_width, _height]);
var root = d3.hierarchy(_nodes) // (参照1) hierarchy()関数でtreemapで扱える階層構造に変換する
.sum(_valueAccessor)
.sort(function(a, b) { return b.value - a.value; });
pack(root); // <-C
renderCircles(root.descendants());
renderLabels(root.descendants());
}
function renderCircles(nodes) { // <-C
var circles = _bodyG.selectAll("circle")
.data(nodes);
circles.enter().append("circle")
.merge(circles)
.transition()
.attr("class", function (d) {
return d.children ? "parent" : "child";
})
.attr("cx", function (d) {return d.x;}) //
.attr("cy", function (d) {return d.y;})
.attr("r", function (d) {return d.r;});
circles.exit().transition()
.attr("r", 0)
.remove();
}
function renderLabels(nodes) {
var labels = _bodyG.selectAll("text")
.data(nodes);
labels.enter().append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.merge(labels).transition()
.attr("class", function (d) {
return d.children ? "parent" : "child";
})
.attr("x", function (d) {return d.x;})
.attr("y", function (d) {return d.y;})
.text(function (d) {return d.data.name;});
labels.exit().remove();
}
_chart.width = function (w) {
if (!arguments.length) return _width;
_width = w;
return _chart;
};
_chart.height = function (h) {
if (!arguments.length) return _height;
_height = h;
return _chart;
};
_chart.nodes = function (n) {
if (!arguments.length) return _nodes;
_nodes = n;
return _chart;
};
_chart.valueAccessor = function (fn) {
if (!arguments.length) return _valueAccessor;
_valueAccessor = fn;
return _chart;
};
return _chart;
}
var chart = pack();
function pop() {
d3.csv("saitama201710.csv", function (error,data) {
//console.log(JSON.stringify(data));
var nest = d3.nest()
.key(function(d) {
return d.region;
})
.entries(data);
var children=[];
nest.map( (reg) => {
var child={};
child.name=reg.key;
child.children = reg.values;
children.push(child);
});
var saitama={};
saitama.name="埼玉県地域別人口マップ";
saitama.name="埼玉県";
saitama.children=children;
console.log(JSON.stringify(saitama));
chart.nodes(saitama).valueAccessor(size).render();
});
}
function size(d) {
return d.size;
}
function count() {
return 1;
}
function flip(chart) {
chart.valueAccessor(chart.valueAccessor() == size ? count : size).render();
}
pop();
</script>
<div class="control-group">
<button onclick="flare()">市町村人口</button>
<button onclick="flip(chart)">市町村数</button>
</div>
</body>
</html>
最後にTreeMapの解説記事は以下のものがわかりやすいと感じました。
http://deep-blend.com/jp/2014/05/d3-js-layout-treemap/