この記事は Data Visualization Advent Calendar 2015 の 5 日目の記事です。
JavaScript でタイルマップを扱うライブラリの一つである leaflet.js
と、データや DOM の操作が充実したライブラリである d3.js
を使って、地図上にデータヴィジュアライゼーションを作成する際に最低限必要な情報をまとめた技術解説です。
この記事で解説していることを bl.ocks.org にまとめています。コピペで動くサンプルコードはそちらをご参照ください。
geojson
の表示については記事があったのですが、動的にデータを追加したり、緯度経度だけわかっているデータをちょっと表示するだけの場合に、すぐ使えそうなサンプルが見当たらなかったので作ってみました。
leaflet.js
で地図表示
leaflet.js
を使って OpenStreetMap
のタイルマップを表示するには、例えば以下のようにします。
<!DOCTYPE html>
<meta charset='utf-8'>
<script src='//d3js.org/d3.v3.min.js'></script>
<link rel='stylesheet' href='http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css' />
<script src='http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js'></script>
<body>
<div style='width:960px;height:500px'></div>
<script>
var map = new L.Map(d3.select('div').node()).setView([35.678707, 139.739143], 12);
var tile = L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution : '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
</script>
body
要素のすぐ下の div
要素にタイルマップを作成します。地図のサイズは div
要素のサイズと同じになります。今回はインラインのstyle
属性で大きさを指定しています。タイルマップの生成は、実質2行で済みます。
L.Map
関数でマップオブジェクト を生成します。引数は id
文字列や、DOM
エレメントを渡すことができます。ソースではd3
で取得したセレクションからDOM
エレメントにアクセスしています。第二引数にオプションを指定することもできます。
L.tileLayer
関数で タイルレイヤを生成します。タイルはURLテンプレートによって指定します。ソースではOpenStreetMap
を使っているので、attribution
を追加します。最後にaddTo
関数でマップオブジェクトに追加します。
SVG
レイヤを追加する
var svgLayer = d3.select(map.getPanes().overlayPane).append('svg').attr('class', 'leaflet-zoom-hide').attr({width:960,height:500});
var plotLayer = svgLayer.append('g');
svg
要素を追加するだけなら上の行だけで済みますが、タイルマップのズーム前後の座標位置を合わせるのにg
要素を使います。
描画する要素はg
要素の下にぶら下げていきます。svg
につけているleaflet-zoom-hide
クラスは、タイルマップのズーム開始からズーム完了までの間、レイヤを非表示にするクラスだそうです。
緯度経度とSVG
座標の相互変換
leaflet
で緯度経度情報は L.LatLng
オブジェクトとして扱います。L.LatLng
オブジェクトから、レイヤ座標への変換には map.latLngToLayerPoint
関数を使います(map
はマップオブジェクト)。引数には L.LatLng
オブジェクトを渡します。
d.pos = map.latLngToLayerPoint(new L.LatLng(d.latitude, d.longitude));
返り値は x
と y
という属性を持った L.point
オブジェクトです。
L.point
から L.LatLng
への変換は map.layerPointToLatLng
関数を使います(今回のソースには登場しません)。クリックした位置の緯度経度を取得するといった使い方ができます。
データを地図上にプロットする
緯度経度から座標変換ができるようになったので、あとは通常のD3での描画と同じようなコードになります。描画する要素は何でもいいのですが、今回は山手線の駅の位置を使います。各要素に latitude
属性と longitude
属性を持った points
という配列の中身を circle
要素でプロットしてきます。
points.forEach(function(d){
d.pos = map.latLngToLayerPoint(new L.LatLng(d.latitude, d.longitude));
});
plotLayer.selectAll('circle').data(points).enter().append('circle')
.attr({r:10, fill:'steelblue', stroke: 'white', 'stroke-width': 3})
.attr('cx', function(d){return d.pos.x;})
.attr('cy', function(d){return d.pos.y;});
前半3行で pos
属性に、変換後の座標を入れています。
後半4行で plotLayer
下に circle
要素を追加し、各属性を設定しています。
ズーム/スクロールに対応する
マップの表示が固定であれば、ここまでの内容で十分なのですが、最後に一つ、ズームやタイルのスクロールに対応するのにもう少しコードを追加する必要があります。
タイルレイヤ上のスクロールは、マップ全体の平行移動で表現されているので、svg
要素もそれに引っ張られて一緒に平行移動していきます。
一方で、svg
要素の描画領域から外れたオブジェクトはクリッピングされて見えなくなってしまうので、svg
要素は常に表示領域全体を覆っているようにしなくてはなりません。
対応の仕方はいくつかあると思いますが、筆者が使っている方法を以下で紹介します。考え方は以下の通りです。
- 表示されている地図の北西、南東の端点の緯度経度をそれぞれ取得する
- その緯度経度をレイヤ座標系に変換する
-
svg
要素の原点を平行移動で北西に合わせ、右下の点を南東に合うようにwidth
とheight
を設定する -
g
要素を平行移動して、svg
要素の平行移動をキャンセルする
コードで書くと以下のようになります。
var reset = function()
{
var bounds = map.getBounds();
var topLeft = map.latLngToLayerPoint(bounds.getNorthWest());
var bottomRight = map.latLngToLayerPoint(bounds.getSouthEast());
svgLayer.attr("width", bottomRight.x - topLeft.x)
.attr("height", bottomRight.y - topLeft.y)
.style("left", topLeft.x + "px")
.style("top", topLeft.y + "px");
plotLayer.attr('transform', 'translate('+ -topLeft.x + ',' + -topLeft.y + ')');
}
この関数をマップオブジェクトの move
イベントにバインドしておきます。
map.on("move", reset);
描画する要素のレイヤ座標も再取得する必要があるため、その処理も追加します。まとめると下記のようになります。
var updatePosition = function(d)
{
d.pos = map.latLngToLayerPoint(new L.LatLng(d.latitude, d.longitude));
d3.select(this).attr( {cx: d.pos.x, cy: d.pos.y } );
}
map.on('move', function()
{
plotLayer.selectAll('circle').each(updatePosition);
});
まとめ
この記事では leaflet.js
に SVG
をオーバーレイして表示する際に必要なことを書きました。冒頭でも書きましたが、上記のことをまとめて、少し整理したコードを bl.ocks に置いてあります。合わせてご参照ください。