D3.jsで日本地図を描くときの基本(geojson)
D3.jsで埼玉県地図を描くときの基本(topojson)
D3.jsで埼玉県の地図上に市町村ラベルを描く
前回はTopoJSONのデータを使って埼玉県の地図を描きました。今回はその地図上に市役所・町役場の所在地とラベルを追加して描きます。所在地はマル(D3.jsのcircle)で表し、名前はテキスト(市町村名、D3.jsのtext)で表します。
今回作成したページです。前回同様ズームやドラッグができ、拡大するとテキストが見やすくなります。
https://s3-ap-northeast-1.amazonaws.com/kuki-app-bucket/japanmap/saitama2.html
1.データの入手
地図データは前回、埼玉県の地図を描いた時に利用したTopoJSONデータをそのまま用います。それに加えて新たに市役所の名前と経度緯度のデータが必要になります。これは以下の国土地理院のサイトから入手します。
http://www.gsi.go.jp/KOKUJYOHO/CENTER/kendata/saitama_heso.htm
HTMLページなので、ページをコピペしてテキストファイルに保存し、簡単な変換プログラム(nodeスクリプト)を自作し、CSVファイルに変換しました。
label,x,y
埼玉県,139.64888888888888,35.856944444444444
さいたま市(浦和区),139.64527777777778,35.861666666666665
さいたま市西区,139.57972222222222,35.925
さいたま市北区,139.62,35.93083333333333
さいたま市大宮区,139.62861111111113,35.90611111111111
さいたま市見沼区,139.65444444444444,35.93527777777778
さいたま市中央区,139.62611111111113,35.88388888888889
さいたま市桜区,139.61027777777778,35.85583333333334
さいたま市南区,139.64555555555555,35.84527777777778
さいたま市緑区,139.6838888888889,35.87111111111111
さいたま市岩槻区,139.69416666666666,35.94972222222222
川越市,139.48583333333332,35.925
熊谷市,139.38861111111112,36.14722222222222
川口市,139.72416666666666,35.80777777777777
行田市,139.45583333333332,36.138888888888886
秩父市,139.08527777777778,35.99166666666667
所沢市,139.4688888888889,35.79944444444444
飯能市,139.32777777777778,35.85583333333334
加須市,139.60194444444443,36.13138888888889
本庄市,139.1902777777778,36.24388888888889
---
2.Promiseで2ファイルを同時に読み込む
今回のテーマに直接関係ありませんが、TopoJSONファイルとCSVファイルを読み込むのにES6のPromiseを使います。以前、Promiseの記事を書いた時に、PromiseはCallBackヘル(入れ子が深すぎるCallBack)を解決する以外にも便利な機能があると書きました。
ES6のPromiseの基本を考えてみた
TopoJSONファイル(地図データ)とCSVファイル(市役所データ)を、Promise.all()を使って同時に読み込み、両ファイルの読み込みが成功・終了した時にのみthen()が呼ばれます。PromiseWrapperはd3.jsonとd3.csvの非同期読み込みをラッピングしてPromiseを返すための関数です。両方の読み込みが成功した時にcreateMap(resolve[0], resolve[1])が呼ばれます。resolve[0]にはsaitama.topojsonの内容が含まれ、resolve[1]にはsaitama_cities.csvの内容が含まれます。
var PromiseWrapper = (xhr, d) => new Promise(resolve => xhr(d, (p) => resolve(p)))
Promise.all([PromiseWrapper(d3.json, "./saitama.topojson"),PromiseWrapper(d3.csv, "./saitama_cities.csv")])
.then( resolve => { createMap(resolve[0], resolve[1])} )
3.D3.jsのプログラム
以下に全ソースコードを示します。コードの骨格はシンプルな埼玉県の地図を描いた前回のものと全く同じです。それに市役所のcircleとラベルのtextを追加しただけです。この追加した部分を以下に解説します。
<!doctype html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.12.0/d3.min.js" type="text/JavaScript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js"></script>
</head>
<body>
<svg></svg>
<script>
var width = 1600,
height = 1200;
var scale = 80000;
var PromiseWrapper = (xhr, d) => new Promise(resolve => xhr(d, (p) => resolve(p)))
Promise.all([PromiseWrapper(d3.json, "./saitama.topojson"),PromiseWrapper(d3.csv, "./saitama_cities.csv")])
.then( resolve => { createMap(resolve[0], resolve[1])} )
function createMap(topoSaitama, cities) {
var geoSaitama = topojson.feature(topoSaitama, topoSaitama.objects[11]);
var aProjection = d3.geoMercator()
.center([ 139.5, 35.9 ])
.translate([width/2, height/2])
.scale(scale);
var geoPath = d3.geoPath().projection(aProjection);
var svg = d3.select("svg").attr("width",width).attr("height",height);
//マップの描画
var map = svg.selectAll("path").data(geoSaitama.features)
.enter()
.append("path")
.attr("d", geoPath)
.style("stroke", "#ffffff")
.style("stroke-width", 0.1)
.style("fill", "#5EAFC6");
//市町村ポイントの描画
var map2 = svg.selectAll("circle").data(cities)
.enter()
.append("circle")
.attr("r", 3)
.attr("cx", d => {return aProjection([d.x,d.y])[0]})
.attr("cy", d => {return aProjection([d.x,d.y])[1]})
.style("stroke", "red")
.style("stroke-width", 0.5)
.style("fill", "none");
//ラベルの描画
var map3 = svg.selectAll("text").data(cities)
.enter()
.append("text")
.text( (d) => { return d.label } )
.attr("x", d => {return aProjection([d.x,d.y])[0]})
.attr("y", d => {return (aProjection([d.x,d.y])[1]-10)})
.attr("text-anchor","middle")
.attr('font-size','8pt');
//ズームイベント設定
var zoom = d3.zoom().on('zoom', function(){
aProjection.scale(scale * d3.event.transform.k);
map.attr('d', geoPath);
map2.attr("r", 3)
.attr("cx", d => {return aProjection([d.x,d.y])[0]})
.attr("cy", d => {return aProjection([d.x,d.y])[1]});
map3.attr("x", d => {return aProjection([d.x,d.y])[0]})
.attr("y", d => {return (aProjection([d.x,d.y])[1]-10)})
.attr("text-anchor","middle")
.attr('font-size','8pt');
});
svg.call(zoom);
//ドラッグイベント設定
var drag = d3.drag().on('drag', function(){
var tl = aProjection.translate();
aProjection.translate([tl[0] + d3.event.dx, tl[1] + d3.event.dy]);
map.attr('d', geoPath);
map2.attr("r", 3)
.attr("cx", d => {return aProjection([d.x,d.y])[0]})
.attr("cy", d => {return aProjection([d.x,d.y])[1]});
map3.attr("x", d => {return aProjection([d.x,d.y])[0]})
.attr("y", d => {return (aProjection([d.x,d.y])[1]-10)})
.attr("text-anchor","middle")
.attr('font-size','8pt');
});
map.call(drag);
}
</script>
</body>
</html>
マップ描画は前回と同じものですが、あまり説明をしていませんでした。今回は市町村ポイントの描画についてD3.jsの見地から少し説明を加えたいと思います。まず以下のコードで市役所の場所にマルを描きます。地図を描くときはdata(geoSaitama.features)でしたが、今回はcircle要素にdata(cities)をバインドしています。既にバインド済みのcircle要素はありませんから、data(cities)のデータは全て新要素となり、enter()でマッチします。つまり続くappend("circle")でdata(cities)の全要素はcircle要素となります。今回のプログラムではdata(cities)の追加や削除はありませんから、D3.jsのenter-updata-exit パタンのenterのみを考慮すればいいわけです。これはマップ描画やラベルの描画についても全く同じことが言えます。
D3.jsの enter-updata-exit パタンについて
マルを描くときの中心はcx=aProjection([d.x,d.y])[0], cy=aProjection([d.x,d.y])[1]で得られることに注意してください。
//市町村ポイントの描画
var map2 = svg.selectAll("circle").data(cities)
.enter()
.append("circle")
.attr("r", 3)
.attr("cx", d => {return aProjection([d.x,d.y])[0]})
.attr("cy", d => {return aProjection([d.x,d.y])[1]})
.style("stroke", "red")
.style("stroke-width", 0.5)
.style("fill", "none");
上の市町村ポイントの描画と全く同じようにラベルの描画も行えます。circle要素でなくtext要素になっていることに注意してください。テキストラベルの描画の時に、マルと重ならないようにy座標を-10しています。
//ラベルの描画
var map3 = svg.selectAll("text").data(cities)
.enter()
.append("text")
.text( (d) => { return d.label } )
.attr("x", d => {return aProjection([d.x,d.y])[0]})
.attr("y", d => {return (aProjection([d.x,d.y])[1]-10)})
.attr("text-anchor","middle")
.attr('font-size','8pt');
ズームイベントの設定は以下のコードになります。ズームイベントでdataセットの追加・削除は生じません。mapやmap2、map3でマッチさせたenterのDOM要素の属性を変えてやる必要があるだけです。aProjection.scale()の変更に伴って、座標が変わるので関連のある属性を再設定します。この再設定を反映させるために、自動的に再描画されます。
//ズームイベント設定
var zoom = d3.zoom().on('zoom', function(){
aProjection.scale(scale * d3.event.transform.k);
map.attr('d', geoPath);
map2.attr("r", 3)
.attr("cx", d => {return aProjection([d.x,d.y])[0]})
.attr("cy", d => {return aProjection([d.x,d.y])[1]});
map3.attr("x", d => {return aProjection([d.x,d.y])[0]})
.attr("y", d => {return (aProjection([d.x,d.y])[1]-10)})
.attr("text-anchor","middle")
.attr('font-size','8pt');
});
svg.call(zoom);
ドラッグイベント設定はズームイベント設定と全く同じです。svg.call(zoom)とmap.call(drag)の違いがあるだけです。