(訳:FoD5 ) このチュートリアルの解説 D3.js 日本語化プロジェクトについて

D3.jsとTopoJSONで地図を作る

このチュートリアルでは D3 TopoJSON を使って、シンプルな地図を一から作る方法について学習します。オンラインでフリーの地理データが得られるサイトを いくつか紹介し、そのデータを、画面表示効率がよくて使いやすいフォーマットに変換する方法を解説します。このチュートリアルでは thematic mapping(主題図)については触れません。 しかし製作過程で居住地域へのラベルの付け方も説明しますので、そのテクニックを応用すれば graduated symbol maps(等級別記号図)choropleths(コロプレス/統計表現地図) 等の地図的なデータ視覚化もできるでしょう。

細かい説明は後回しに、最終的にできあがる地図をお見せします。

表示されているのは、四つの非独立国である スコットランド北アイルランドウェールズイングランド からなるイギリスの地図です。地図としての面白味はあまりありませんが、こうしたシンプルな地図の方が地図製作の 基本を理解しやすいでしょう。

#データを探す

どんな地図を作るにせよジオメトリ()を探すことが最初の仕事となります。行政境界線やその他の公的な データはまず各国の政府機関から入手するものです。たとえばアメリカ合衆国 国勢調査局 (センサス・ビューロー)は、州や群、国政統計区など細分化された区域ごとのジオメトリを発行しています。 政府発行のデータに関しては、一般的には公有扱い、すなわち無償で使えます。イギリスでは同様のベクトルデータを オーディナンス・サーベイ(英国陸地測量部) が提供しています。

残念なことに政府発行のデータは往々にして見つけにくく()、使いにくいものです。 誰か GRIB形式のファイルに取り組んだりアテもなく謎の FTP サーバに出入りしている連中に頼んでみるのもよいでしょう。地図データへの需要が増すにつれこうした状況も改善していくと思いますが、 過去の版の残骸や、乱造された紛らわしいフォーマット規格のなくなることはないでしょう。 Data.govSunlight Foundation のようなプロジェクトは、特にこうした政府提供データの質を向上させることを目的としており、また Social Explorer のようなサイトは、政府発行のデータをよりアクセス しやすいフォーマットにパッケージしています。

クラウド上の選択肢としては、GeoCommons が地理情報データセット共有の プラットフォームとなります。検索とプレビューが統合されており簡単に調べることができます。有益なデータも多数ありますが、 出所不明のデータに関しては、 少なくとも報道目的に使うような場合は、扱いに慎重さが求められます。 データは直接公的機関もしくは信頼できる入手先から得るのが望ましいでしょう。

フリーの地理データの最も手軽な入手先は間違いなく Natural Earth と言えます。 Natural Earth は Nathaniel Vaughn Kelso氏( やその他の人々)の真に奉仕的な 活動によって運営されており、様々な文化的、自然的、 あるいはラスター形式のデータセットを提供しています。シェイプファイルは一つ一つが複数の解像度ごとに美しく簡略化されており、 自分のアプリケーションに最適の解像度を選ぶことができます。このチュートリアルでは、その Natural Earth から、二つの 1:1e7 データセットを使って地図を作っていきます。以下の二つのファイルです。まずこれをダウンロードしましょう。

前者には国ポリゴン、後者には居住地域の位置と名前が含まれています。両ファイルとも全世界のデータが入っているため、 次に行う作業は、ダウンロードしたデータを適当なフィルターにかけ、必要なサブセットを作ることです。

#ツール類のインストール

通常、地理データファイルは非常にサイズが大きいため、手作業で整理したり変換するのはまず不可能です。 幸いなことにジオ関連には活発なオープンソース・コミュニティが存在しており、 標準フォーマットの操作やフォーマット間の変換を行ってくれる優れたフリーのツール類があります。

最初に憶えておくべきマルチツールが Geospatial Data Abstraction Library (地理空間データ抽出ライブラリ)です。通常 GDAL と呼ばれるこのツールには、 OGR Simple Features Library(OGR シンプル機能ライブラリ) ogr2ogr バイナリが入っています。次の節ではその ogr2ogr を使って Natural Earth のシェイプファイルを処理します。 様々なプラットフォーム向けの公式 GDAL バイナリがあります。もし Mac ユーザなら Homebrew を使ってインストールしましょう。

brew install gdal

次に TopoJSON のリファレンス実装( reference implementation ) が必要です。まず Node.js をインストールします。Node.js も Homebrew でインストールできますが、 公式インストーラでも問題ありません。 Node.js のインストール後に次のコマンドで TopoJSON をインストールします:

npm install -g topojson

インストールが両方とも上手くいったか確かめます:

which ogr2ogr
which topojson

それぞれ /usr/local/bin/ogr2ogr/usr/local/bin/topojson が表示されるはずです。

#データの変換

必要な準備は整いました。これで二つのシェイプファイルを一つの TopoJSON ファイルへと合成できます。 最初にシェイプファイルにフィルターをかけ、必要となるイギリスのフィーチャー()だけを残すようにします。 次にそのシェイプファイルを中間フォーマットの GeoJSON に変換し、最後に TopoJSON を生成します。

なぜ JSON フォーマットが二種類あるのでしょうか?実のところ両者は兄弟関係にあります。 TopoJSON() は GeoJSON の拡張形式であり、トポロジーをエンコードしたものです。座標計算に固定精度エンコーディングを用いることにより TopoJSON ファイルのサイズは GeoJSON よりずっと小さくなります。今作っている地図は、GeoJSON では 536KB ですが、TopoJSON ではたったの 80KB、つまり 85% も削減されています(gzip圧縮後の比較でもずっと小さくなります)。単にディスク必要量を 削減するだけではなく、TopoJSON に含まれるトポロジー情報を使えば、境界線の自動計算や こうした面白いアプリケーションを作ることもできます。

最初に、ダウンロードした ne_10m_admin_0_map_subunits.shp を引数に、 ogr2ogrコマンドを使って subunits.json という名前の GeoJSON ファイルを生成します。

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

引数 -where がフィルターとして働きます。ここでは adm0_a3 プロパティが "GBR" もしくは "IRL"に等しいフィーチャーだけが GeoJSON に出力されます。adm0 は Admin-0 すなわちトップレベルの 行政境界線のことを指し、a3 ISO 3166-1 alpha-3 の定める国コードを指しています。作成するのはイギリス(国コード 'GBR' )の地図だけですが、地形を正確に描くためには アイルランド (国コード 'IRL' )も含める必要があります。さもなければ北アイルランドだけで一つの島であるかのような印象を与えかねないでしょう。

次に、居住地域の GeoJSON ファイル、places.json を作成します。再度 ne_10m_admin_0_map_subunits.shp を 引数にして居住地域をフィルターします。 場所のプロパティが結構まちまちであるため、ここでは where 節で iso_a2 を指定しています。 さらに SCALERANK に大都市だけラベル付けするようにフィルターをかけます。

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

最後に、出来上がった subunits.jsonplaces.json の二つの GeoJSON ファイルを結合して一つの TopoJSON ファイル、uk.json にします。このステップではソースの細かい不一致の修正も行います。 NAME プロパティを name に改め、また、オブジェクトID に su_a3 プロパティを使うようにさせます。

topojson \
   --id-property su_a3 \
   -p NAME=name \
   -p name \
   -o uk.json \
   subunits.json \
   places.json

今回のシンプルな地図作成には使いませんが、ogr2ogr には他にも多くの、今後役立つ強力な機能があります。 たとえば引数 -clipdst は、シェイプファイルから四角形の バウンディング・ボックスを切り抜きます。あるフィーチャーの一部分だけを表示させたいときに便利な機能です。シェイプファイルに ( UTM のような)経緯度格子が必要な場合は -t_srs EPSG:4326を使って背景を定型的な緯度・経度線に変更できます。その他のオプションについては ogr2ogr マニュアル をご覧ください。

#データの読込み

コマンドラインでの地理データ処理はここまでで終わりです。ここからは WEB 開発の世界に戻ります。 以前に解説したことを再度繰り返すことはせず、 HTML や JavaScript の知識があるものという前提で話を進めていきます。不安のある方は スコット・マレイの D3 入門 (日本語版)に目を通してください。uk.jsonファイルと同じディレクトリに 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>

/* JavaScript goes here. */

</script>

そしてローカルの Web サーバを起動してこの index.html を表示させます。Web サーバは何でも構いません。私は Python を使っています。

python -m SimpleHTTPServer 8008 &

ブラウザの URL に http://localhost:8008 と入力すれば以下のような真っさらのブランクページが表示されるはずです。

ソースコードの表示: step-1.html

これは期待外れの表示結果かと思いますが、すぐに改良していきますので、少しのあいだ辛抱してください。 スクリプトタグ内("JavaScript goes here"と書いてある箇所)に、TopoJSON ファイルを読み込むための d3.json 呼び出しスクリプトを書き加えます。

d3.json("uk.json", function(error, uk) {
   console.log(uk);
});

これで JavaScript コンソール 内に、イギリスの行政境界線と居住地域を表す topology オブジェクトが表示されるはずです。Google Chrome ですと こんな風に表示されます( Object をクリックして展開した状態)。



画面上には何も表示されていませんが、TopoJSON のデータはすでに読み込まれていることがわかります。

#ポリゴンの表示

二次元ジオメトリをブラウザに表示するには様々な方法がありますが、主要な二つは SVG Canvas の両標準です。D3 3.0 はその両方をサポートしていますが、 今回の例では SVG を用います。SVG は CSS を使ってスタイルを設定でき、 そうした宣言的スタイリングの方が設定が簡単だからです。次のコードでルート SVG 要素を生成します。

var width = 960,
    height = 1160;

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

SVG は、d3.json コールバック関数内ではなく、このようにメインスクリプトの先頭で定義することをお薦めします。 理由は d3.json が非同期処理だからです。こうすることで、TopoJSON ファイルのダウンロードを待っている間に、 ページの残りのレンダリングを進められます。また、ページ生成時に空のルート SVG を作って置けば、 ジオメトリ取得時の画面再描写によるチラつきも回避できます。

ジオメトリのレンダリングに必要なメソッドが二つあります。 projectionpath ジェネレータです。 projection は、その名前が示す通り球面座標をデカルト平面に投影(project)します。 投影は球面ジオメトリを二次元スクリーンに表示するために必要なものですので、もし将来、 3次元ホログラフィックディスプレイを使うようになればこのメソッドは不要になるでしょう。path ジェネレータは投影されたされた二次元ジオメトリを受取り、 SVG や Canvas 向けに適切にフォーマットします。

さぁ準備完了です! d3.json コールバック内のコードを次のように書き換えてください。

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

小さくて黒い、しかしどこかで見たような形のシミが表示されているはずです。

ソースコードの表示: step-2.html

適当な地図屋さんならこれで上出来だとのたまい、さっさとうちに帰ってビールでもひっかけるところでしょう。 でも適当でない私たちは、降参せずにもう少しこの地図に磨きをかけていきます。 理解の一助になるよう、ここで、 書き換えた三行の呪文のようなコードについて解説しておきます。

少し前の節で、近縁関係にある二つの JSON 地理データフォーマット、GeoJSON と TopoJSON について触れたのを思い出してください。 私たちの扱うデータは TopoJSON フォーマットにより保管は効率よく行えますが、表示の際には、それを一度 GeoJSON フォーマットに戻す必要があります。このステップを分かりやすく噛み砕いてみます。

まず svg.append( "path" ).datum() の引数部分を変数にします。ここでは topojson.object メソッドが与えられた TopoJson ファイル中のジオメトリオブジェクトの配列( uk.objects.subunits )をGeoJSONに変換して変数 subunits に返しています。

var subunits = topojson.object(uk, uk.objects.subunits);

同様にプロジェクションの定義も抜き出してコードをわかりやすくします。d3.geo.mercator() は、メルカトール投影法による プロジェクションを返します。あわせて scaletranslate の二つのメソッドを適用します。scale は指定した数値にプロジェクションをスケールし、translate は 引数で指定したオフセット( x, y の値からなる配列)分、プロジェクションを移動させます。

var projection = d3.geo.mercator()
   .scale(500)
   .translate([width / 2, height / 2]);

path ジェネレータも同様に変数 path に代入しておきます。d3.geo.path() で生成した path に、上で生成した変数 projection のプロジェクションを適用します。

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

最後に path 要素( svg.append( "path" ) )に対して datumメソッドで GeoJSON データ( subunits )を bind し、その "d" 属性に selection .attr を使ってフォーマットした path データをセットします。ここで"d"とは、datum() の返すセレクションの各要素に、ループして path を適用するための仮引数です ()。

svg.append("path")
   .datum(subunits)
   .attr("d", path);

ここまでに組んだコードて、ようやくプロジェクション(投影法)をより「連合王国(United Kingdom)」 の名にふさわしいものに変更する準備が整いました。 緯度線(parallel)を 標準的な北緯50°と60°にしたアルバーズ円錐正積図法 が良いでしょう。+4.4°ほど 経度を回転させ、 中心( center ) を西経0°、北緯 55.4°にセットして、実際の原点を西経4.4°北緯55.4° にします。これはスコットランドのどこかを指しています。

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

形を整えた地図:

ソースコードの表示: step-3.html

#ポリゴンのスタイル設定

前述したように、SVG を使うことによる利点の一つは CSS によるスタイル設定ができることです。 fill プロパティ にスタイルルールを適用することで、イギリスを構成する各国(※1)の色分けが可能になります。 しかしそのためにはまず、パス要素を(共有するのではなく)各国ごとに持たせる必要があります(※2)。 パス要素を別々にしなければ、別々の色を割り当てる方法がないからです。

TopoJSON ファイル uk.json 内部では、 ジオメトリコレクションが Admin-0 map subunits を表しています。この geometries 配列を取り出すことで、 data を結合し、それぞれのジオメトリオブジェクトごとに一つのパス要素を生成できるようになります。

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);

四行目( .attr( "class", function… の行)で関数を使ってクラス属性を設定しています。 元のクラス名( subunit )に、それぞれの国に対応する3文字の国コード( ISO-3166 alpha-3 ) をクラス名として付け加えています。データの変換で、TopoJSON 生成時にオブジェクトID に su_a3 プロパティを設定したのを思い出してください。四行目の d.id で、そのオブジェクトIDを取り出しています。 こうして設定した国コードクラス名に対して、以下のスタイルシートで、それぞれの fill スタイルを設定します。

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

このスタイルシートではアイルランドが一時的に見えなくなりますが、次のステップで境界線を表示したときにまた復活します。 ここまでで地図はこのように表示されます。

ソースコードの表示: step-4.html

#境界線の表示

ポリゴンの仕上げに数本の線を書き加える必要があります。イングランドとスコットランド、ウェールズとの間の各国境、 およびアイルランドの海岸線の二種類の境界線です。

トポロジーから境界線を計算するのには topojson.mesh を使います。このメソッドにはトポロジーと構成要素のジオメトリオブジェクトの二つの引数が必要です ()。ここでは uk ( uk.json )がトポロジー、uk.objects.subunits がジオメトリオブジェクトになります。 オプションの引数のフィルタを設定すれば、戻り値の境界線を減らすことができます。このフィルタ関数は二つの引数 ab をとり、それぞれ境界線の両サイドのフィーチャーを表しています。もし海岸線のような外部境界線の場合は ab は同じです。 したがって、a === b または a !== b のフィルタで、それぞれ外部境界線、あるいは 内部境界線を取得できるのです。

イングランドとスコットランド、およびイングランドとウェールズの国境は内部境界線になります。 アイルランドと北アイルランドの国境線は id にフィルタをかけることで除外できます。

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");

スタイルを設定します。

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

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

こうなります。

ソースコードの表示step-5.html

#都市名の表示

国ポリゴンと同様に居住地域もジオメトリコレクションですので、ここでも TopoJSON から GeoJSON に変換し(戻し)、d3.geo.path を使ってレンダリングします。

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

このコードでそれぞれの都市の位置に小さな円マークが描かれます。この円の半径は path.pointRadius を使って調整でき、また円(placeクラス)のスタイルは、CSS で設定できます。しかしここではまだラベルは描かれていません。 テキスト要素を生成するためには、データの結合が必要です。また、各都市の座標を投影することによって transform プロパティを計算し、ラベルを所定の位置に表示させることができます。円マーク同様、ラベルに設定したクラス( place-label )の スタイルも CSS で設定することができます( step-6.htmll内のコード参照 )。

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("dy", ".35em")
    .text(function(d) { return d.properties.name; });

地図に上手にラベルを付けるのはなかなか難しいものです。特にラベルの位置を自動的に設定させるのは骨の折れる仕事です。 今回のシンプルな地図では、初めに SCALERANK で若干フィルターをかけた程度で、この問題をほとんど避けてきました。 手軽な解決策の一つは、地図の左側ではマークの左側に配置し、右側では右側に配置するという方法です。次のコードでは東経1° を区切りに配置を逆転しています。

svg.selectAll(".place-label")
    .attr("x", function(d) { return d.coordinates[0] > -1 ? 6 : -6; })
    .style("text-anchor", function(d) { return d.coordinates[0] > -1 ? "start" : "end"; });

ご覧のとおり、下の地図ではおおむねうまく表示できているものの、重なってしまっているラベルも数都市あります( Nottingham と Coventry など)。どうしても気になる場合はそこだけ個別に処理して別の配置方法を設定するか、あるいは単に邪魔なラベルを削除してしまいます。 simulated annealing force-directed layout を使う手もあります。そのうち私も、 自動ラベル配置( automatic label placement ) に取り組んでみたいと思っています。

ソースコードの表示:step-6.html

#国ラベルの表示

この地図にはまだ重要なパーツが欠けています。そう、まだ国名ラベルを付けていません。 国名ラベルを作成するにはラベルの位置とテキストを指定する必要があります。位置の計算には Natural Earth の Admin-0 label points を使うこともできますが、 centroid メソッドでも同じくらい簡単に計算できます。

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; });

国名ラベルは市名ラベルと見分けやすいよう大きな文字スタイルを設定します。また、国名ラベルの透明度を下げて後退させ、 市名ラベルを読みやすくします。

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

以上で完成です。

ソースコードの表示: step-7.html

この続きは…

このチュートリアルをお楽しみいただいた方は、ぜひ私の事例集もお試しください。 D3 3.0 リリースノート(日本語) APIリファレンス(翻訳準備中)にも地理視覚化のサンプルが入っています。 リクエストがあれば Twitterからどうぞ。
Happy New Year!