【自分でやってみるシリーズ】D3.jsとTopoJSONで地図をつくる

前置き

自分でやってみるシリーズとは、Web上のチュートリアルなどを実際に自分で動かしてみることによって、理解を深めることを目的とした個人演習シリーズです。

今回の題材

今回はD3.jsとTopoJSONで地図を作るを題材にします。

自分でやってみよう

それでは早速自分でやってみましょう。

データの入手

以下の二つのファイルをダウンロードします。

上記のデータを解凍し、適当なディレクトリに保存します。
今回は以下のように保存しました。

$ mkdir d3js-topojson && cd $_
$ mkdir data && cd $_
$ mkdir ne_10m_admin_0_map_subunits && cd $_
$ unzip ~/Downloads/ne_10m_admin_0_map_subunits.zip
$ cd ../
$ mkdir ne_10m_populated_places && cd $_
$ unzip ~/Downloads/ne_10m_populated_places.zip

ツール類のインストール

gdalとtopojsonをインストールします。
記載の通り、brewnpmを利用し、それぞれインストールします。
gdalのインストールには若干時間を要するでしょう。

具体的な表示のための手順

ここから具体的な作業に入ります。
実際に作業を進める中で元記事の記載をたどりつつ、躓いた部分の補足を入れています。

データの変換

先程ダウンロードしたデータをTopoJSON形式に変換していきます。
まずはsubunits.jsonをGeoJSONとして生成します。

$ cd ne_10m_admin_0_map_subunits
$ ogr2ogr -f GeoJSON -where "adm0_a3 IN('GBR', 'IRL')" subunits.json ne_10m_admin_0_map_subunits.shp

次にplaces.jsonをGeoJSONとして生成します。

$ cd ne_10m_populated_places
$ ogr2ogr -f GeoJSON -where "iso_a2 = 'GB' AND SCALERANK <8" places.json ne_10m_populated_places.shp

最後に上記の二つのGeoJSONファイルを利用してTopoJSONを生成します。
尚、--id-property SU_A3の部分については case sensitiveなようですので、ご注意ください。
元記事中の通り小文字で指定した場合はtopojson内にidが生成されませんでした。
また、-pの指定にあたっては、元記事の記載(-p NAME=name)とは逆に指定(-p name=NAME)する必要があります。
恐らく元記事の誤りです。
topojsonのリファレンスに記載がありますが、-p target=sourceと指定する必要があり、今回のケースではsourceNAMEです。

$ topojson --id-property SU_A3 -p name=NAME -p name -o uk.json ne_10m_admin_0_map_subunits/subunits.json ne_10m_populated_places/places.json

データの読み込み

d3js-topojson直下にindex.htmlを作成します。
尚、D3.jsを利用して開発をする際には何らかのWebサーバーが必要です。
今回は元の記事にならい、pythonのSimpleHTTPServerを利用します。
d3js-topojsonディレクトリに移動し、以下のコマンドを実行します。

$ python -m SimpleHTTPServer 8008

そうすると、http://localhost:8008にアクセスすれば、まっさらな画面が表示されるようになります。
今回はtopojsonをdataディレクトリに保存していますので、以下のようなhtmlを記載することで期待する動作をします。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

/* CSS goes here. */

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

/* JavaScript goes here. */
d3.json("data/uk.json", function(error, uk) {
   console.log(uk);
});

</script>

Sample - step01

今回はMacOSX上でChromeを利用して開発しています。
Command + alt + iを押すことで開発コンソールが起動します。
“Console"上で以下のようなデータを確認することができれば期待通りに動作しています。

Object {type: “Topology”, objects: Object, arcs: Array[70], transform: Object}

ポリゴンの表示

index.htmlを以下のように変更します。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

/* CSS goes here. */

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

var width = 960,
    height = 1160;

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

/* JavaScript goes here. */
d3.json("data/uk.json", function(error, uk) {
  svg.append("path")
      .datum(topojson.object(uk, uk.objects.subunits))
      .attr("d", d3.geo.path().projection(d3.geo.mercator()));
});

</script>

Sample - step02

元の記事のように黒い小さいイギリスの地図が表示されたら成功です!
こちらを元に表示を少しブラッシュアップします。
元記事を参考にしながら、以下のようなhtmlに変更すると、拡大されたイギリスの地図が表示されるはずです。
尚、記事中の記載と異なり、datumにはtopojsonを直接指定しています。
これは、jsonファイルを読み込んだ後にtopojsonのオブジェクトを生成する必要があるためであり、元記事のように変数化するような記載はできないものと思われます。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

/* CSS goes here. */

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

var width = 960,
    height = 1160;

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

var projection = d3.geo.albers()
   .center([0, 55.4])
   .rotate([4.4, 0])
   .parallels([50, 60])
   .scale(6000)
   .translate([width / 2, height / 2]);

var path = d3.geo.path()
   .projection(projection);

d3.json("data/uk.json", function(error, uk) {
  svg.append("path")
      .datum(topojson.object(uk, uk.objects.subunits))
      .attr("d", path);
});

</script>

Sample - step03

ポリゴンのスタイル設定

上記の内容を元に更に表示をブラッシュアップします。
具体的には国コード毎にクラスを設定し、クラス毎の色をCSSで指定することで国コード毎の色分けを行います。
以下のようなコードに変更します。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

    .subunit.SCT { fill: #ddc; }
    .subunit.WLS { fill: #cdd; }
    .subunit.NIR { fill: #cdc; }
    .subunit.ENG { fill: #dcd; }
    .subunit.IRL { display: none; }

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

var width = 960,
    height = 1160;

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

var projection = d3.geo.albers()
   .center([0, 55.4])
   .rotate([4.4, 0])
   .parallels([50, 60])
   .scale(6000)
   .translate([width / 2, height / 2]);

var path = d3.geo.path()
   .projection(projection);

d3.json("data/uk.json", function(error, uk) {
    svg.selectAll(".subunit")
            .data(topojson.object(uk, uk.objects.subunits).geometries)
            .enter().append("path")
            .attr("class", function(d) { return "subunit " + d.id; })
            .attr("d", path);
});


</script>

Sample - step04

境界線の表示

内部境界線であるイングランドとスコットランド、およびイングランドとウェールズの国境を表示します。
アイルランドと北アイルランドの国境線は id にフィルタをかけることで除外します。
また、アイルランドの海岸線は外部境界線として表示します。
境界線の計算には、topojson.mesh関数を利用し、フィルタ関数を利用して境界線を減らします。
最終的に以下のようなコードになります。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

    .subunit.SCT { fill: #ddc; }
    .subunit.WLS { fill: #cdd; }
    .subunit.NIR { fill: #cdc; }
    .subunit.ENG { fill: #dcd; }
    .subunit.IRL { display: none; }

    .subunit-boundary {
      fill: none;
      stroke: #777;
      stroke-dasharray: 2,2;
      stroke-linejoin: round;
    }

    .subunit-boundary.IRL {
      stroke: #aaa;
    }

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

var width = 960,
    height = 1160;

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

var projection = d3.geo.albers()
   .center([0, 55.4])
   .rotate([4.4, 0])
   .parallels([50, 60])
   .scale(6000)
   .translate([width / 2, height / 2]);

var path = d3.geo.path()
   .projection(projection);

d3.json("data/uk.json", function(error, uk) {
    svg.selectAll(".subunit")
            .data(topojson.object(uk, uk.objects.subunits).geometries)
            .enter().append("path")
            .attr("class", function(d) { return "subunit " + d.id; })
            .attr("d", path);

    svg.append("path")
       .datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a !== b && a.id !== "IRL"; }))
       .attr("d", path)
       .attr("class", "subunit-boundary");

    svg.append("path")
       .datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a === b && a.id === "IRL"; }))
       .attr("d", path)
       .attr("class", "subunit-boundary IRL");
});


</script>

Sample - step05

都市名の表示

元記事に記載の通り対応すれば問題ありません。
こちらの対応をすれば、以下のようなコードになります。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

    .subunit.SCT { fill: #ddc; }
    .subunit.WLS { fill: #cdd; }
    .subunit.NIR { fill: #cdc; }
    .subunit.ENG { fill: #dcd; }
    .subunit.IRL { display: none; }

    .subunit-boundary {
      fill: none;
      stroke: #777;
      stroke-dasharray: 2,2;
      stroke-linejoin: round;
    }

    .subunit-boundary.IRL {
      stroke: #aaa;
    }

   .place,
   .place-label {
      fill: #444;
   }

   text {
      font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
      font-size: 10px;
      pointer-events: none;
   }

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

var width = 960,
    height = 1160;

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

var projection = d3.geo.albers()
   .center([0, 55.4])
   .rotate([4.4, 0])
   .parallels([50, 60])
   .scale(6000)
   .translate([width / 2, height / 2]);

var path = d3.geo.path()
   .projection(projection)
   .pointRadius(2);

d3.json("data/uk.json", function(error, uk) {
    svg.selectAll(".subunit")
            .data(topojson.object(uk, uk.objects.subunits).geometries)
            .enter().append("path")
            .attr("class", function(d) { return "subunit " + d.id; })
            .attr("d", path);

    svg.append("path")
       .datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a !== b && a.id !== "IRL"; }))
       .attr("d", path)
       .attr("class", "subunit-boundary");

    svg.append("path")
       .datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a === b && a.id === "IRL"; }))
       .attr("d", path)
       .attr("class", "subunit-boundary IRL");

    svg.append("path")
        .datum(topojson.object(uk, uk.objects.places))
        .attr("d", path)
        .attr("class", "place");

    svg.selectAll(".place-label")
        .data(topojson.object(uk, uk.objects.places).geometries)
        .enter().append("text")
        .attr("class", "place-label")
        .attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; })
        .attr("x", function(d) { return d.coordinates[0] > -1 ? 6 : -6; })
        .attr("dy", ".35em")
        .style("text-anchor", function(d) { return d.coordinates[0] > -1 ? "start" : "end"; })
        .text(function(d) { return d.properties.name; });
});


</script>

Sample - step06

国ラベルの表示

centroid関数を利用して中心位置を計算し、国ラベルを表示します。
コードは以下のようになります。

<!DOCTYPE html>
<meta charset="utf-8">
<style>

    .subunit.SCT { fill: #ddc; }
    .subunit.WLS { fill: #cdd; }
    .subunit.NIR { fill: #cdc; }
    .subunit.ENG { fill: #dcd; }
    .subunit.IRL { display: none; }

    .subunit-boundary {
      fill: none;
      stroke: #777;
      stroke-dasharray: 2,2;
      stroke-linejoin: round;
    }

    .subunit-boundary.IRL {
      stroke: #aaa;
    }

   .place,
   .place-label {
      fill: #444;
   }

   text {
      font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
      font-size: 10px;
      pointer-events: none;
   }

  .subunit-label {
    fill: #777;
    fill-opacity: .5;
    font-size: 20px;
    font-weight: 300;
    text-anchor: middle;
  }

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

var width = 960,
    height = 1160;

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

var projection = d3.geo.albers()
   .center([0, 55.4])
   .rotate([4.4, 0])
   .parallels([50, 60])
   .scale(6000)
   .translate([width / 2, height / 2]);

var path = d3.geo.path()
   .projection(projection)
   .pointRadius(2);

d3.json("data/uk.json", function(error, uk) {
    svg.selectAll(".subunit")
            .data(topojson.object(uk, uk.objects.subunits).geometries)
            .enter().append("path")
            .attr("class", function(d) { return "subunit " + d.id; })
            .attr("d", path);

    svg.append("path")
       .datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a !== b && a.id !== "IRL"; }))
       .attr("d", path)
       .attr("class", "subunit-boundary");

    svg.append("path")
       .datum(topojson.mesh(uk, uk.objects.subunits, function(a, b) { return a === b && a.id === "IRL"; }))
       .attr("d", path)
       .attr("class", "subunit-boundary IRL");

    svg.append("path")
        .datum(topojson.object(uk, uk.objects.places))
        .attr("d", path)
        .attr("class", "place");

    svg.selectAll(".place-label")
        .data(topojson.object(uk, uk.objects.places).geometries)
        .enter().append("text")
        .attr("class", "place-label")
        .attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; })
        .attr("x", function(d) { return d.coordinates[0] > -1 ? 6 : -6; })
        .attr("dy", ".35em")
        .style("text-anchor", function(d) { return d.coordinates[0] > -1 ? "start" : "end"; })
        .text(function(d) { return d.properties.name; });

    svg.selectAll(".subunit-label")
      .data(topojson.object(uk, uk.objects.subunits).geometries)
      .enter().append("text")
      .attr("class", function(d) { return "subunit-label " + d.id; })
      .attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
      .attr("dy", ".35em")
      .text(function(d) { return d.properties.name; });
});


</script>

Sample - step07

終わりに

Shapeファイルのダウンロード→GeoJSON生成→TopoJSON生成→HTMLに表示、という一連の手順を踏みました。
作業に躓いた部分もありましたが、topojsonのリファレンス参照、GeoJSONの構造を垣間見るきっかけとなって結果的によい演習になりました。
また、今回のエントリではあえてソースを全て記載しました。
躓いた際の参照はソースを直接読む方が早いと感じたためです。
次回は今回の事例を応用し、日本地図をベースとして少し表示にカスタマイズをかけていきたいと思います。

尚、今回作成したソースはGithubの以下のレポジトリに置いてあります。
xtatsux/d3js-topojson