2013年3月12日

D3.js と GeoJSON でポリゴンを描く

こちらの記事を参考にして、東京都のポリゴンを描いてみます。

違いは2点です。

  • イギリスではなく「東京都」のポリゴンを使う
  • TopoJSON に変換せず GeoJSON を使う

作成したものは下記のページに置いてみました。

「23区」と「島しょ部除く」のデータはすんなり表示されると思いますが、 「全域」のデータは大きいのでかなり時間がかかります。 これは TopoJSON を使う動機付けになりそうです。

データを探す

まずは境界線を持つポリゴンデータを探します。 国土交通省の国土政策局という部署が「国土数値情報ダウンロードサービス」を提供しています。 このサービスでは、平成24年4月から GML と SHAPE 形式のデータをダウンロードできます。

国土数値情報ダウンロードサービス > JPGIS2.1(GML)準拠及びSHAPE形式データのダウンロード > 国土数値情報 行政区域データ と追いかけていくと、行政の境界線を含むポリゴンデータを入手できます。 その他にも「点」「線」「面」のデータが色々とありますので、頑張れば何かが見つかるはずです。

同様のものとしては、esriジャパンが公開している「 全国市区町村界データ 」もあります。 こちらには人口や世帯数もあるのでデータ内容としてはリッチだと思います。 行政が生データを提供し、企業が加工データで提供してくれる、という流れは嬉しいですね。 もちろん、冒頭の記事で紹介されている Natural Earth もひとつの選択肢です。 それ以外にも使えるデータはたくさんあると思いますので、「 公開データまとめ 」が取っ掛かりとしては良いでしょう。

この記事の以下の部分では国土数値情報の「行政区域界」を使います。 これは、行政界及び海岸線で囲まれる行政区のポリゴンデータです。 都道府県別のデータセットとしてダウンロードできますので、東京都の部分だけを使います。

ダウンロードページの説明では、以下の属性情報があるとのことです。

  • 範囲 : 行政区として定義された領域。[曲面型](GM_Surface)
  • 都道府県名 : 当該区域を含む都道府県名称。[文字列型]
  • 支庁名 : 当該都道府県が「北海道」の場合、該当する支庁の名称。[文字列型]
  • 郡・政令市名 : 当該行政区の郡又は政令市の名称。[文字列型]
  • 市区町村名 : 当該行政区の市区町村の名称。[文字列型]
  • 行政区域コード : 都道府県コードと市区町村コードからなる、行政区を特定するためのコード。コードリスト「行政コード」に対応。

ZIP ファイルをダウンロードして展開すると、下記のファイルがあるはずです。

$ ls -1 N03-120401_13_GML/
KS-META-N03-12_13_120401.xml    # 説明ファイル
N03-12_13_120401.dbf            # Shape 形式の補助ファイル
N03-12_13_120401.shp            # Shape 形式の本体ファイル
N03-12_13_120401.shx            # Shape 形式の補助ファイル
N03-12_13_120401.xml            # GML 形式のファイル

Shape も GML も JavaScript で処理するには不向きなので、これらを JSON、 特に GeoJSON 形式に変換するのが次のステップになります。 GeoJSON に関してはこちらのエントリで触れています。

ツール類のインストール

Shape ファイルを扱えるようにするために、 GDAL を使えるようにします。 Homebrew を使うかパッケージ用のインストーラーを使います。 Mac OSX の場合は下記のページから「GDAL Complete」パッケージを利用可能です。 手元の環境では「GDAL 1.9 Complete」を使っています。

GDAL, OGR に関しては「入門Webマッピング ―自分で作るオリジナルのデジタル地図」という書籍に書かれています。 2006年発行なので細かい記述が古びてしまった感じは否めませんが、 GIS+Web の入門としてはとても分かりやすい1冊だと思います。 Unix 系のツールの使い方も基本的な部分から説明が書かれていますので、何はともあれ読んでおくと良いでしょう。

インストールが終わったら ogr2ogrogrinfo コマンドが使えることを確認します。 .bash_profile に下記の1行を追加してから環境設定を再読み込みします。 (Mac OSX の場合。Windows/Linux の場合はそれぞれでパスが異なります)

export PATH=/Library/Frameworks/GDAL.framework/Programs:$PATH
$ source .bash_profile
$ ogr2ogr --version
GDAL 1.9.2, released 2012/10/08
$ ogrinfo --version
GDAL 1.9.2, released 2012/10/08

TopoJSON も使いたい場合は npm install -g topojson でツールをインストールしましょう。 ここでは TopoJSON には変換しないので、続けて変換ステップに進みます。

もしも自力で Shape ファイルを解析したい場合は、 pyshp というパッケージがあります。 "Pure Python read/write support for ESRI Shapefile format" という説明にある通り、 Python で Shape ファイルを読み書きできるパッケージです。 単一ファイルなので、ソースコードを読んでみても良さそうです。

データの変換

ogr2ogr を使って Shape ファイルを GeoJSON に変換します。 その前に ogrinfo を使って Shape ファイルの情報を見ておきましょう。

$ ogrinfo N03-12_13_120401.shp
INFO: Open of `N03-12_13_120401.shp'
      using driver `ESRI Shapefile' successful.
1: N03-12_13_120401 (3D Polygon)

ESRI Shapefile のドライバを使って読み込みが成功しました。 これだけではどんなデータが含まれているか分からないので、 -al オプションを付けます。 全てのフィーチャーを出力するようになりますので、 head で先頭をちょっとだけ眺めます。

$ ogrinfo -al N03-12_13_120401.shp | head -n 20
INFO: Open of `N03-12_13_120401.shp'
      using driver `ESRI Shapefile' successful.

Layer name: N03-12_13_120401
Geometry: 3D Polygon
Feature Count: 3297
Extent: (136.069482, 20.425119) - (153.986898, 35.898424)
Layer SRS WKT:
(unknown)
N03_001: String (10.0)
N03_002: String (20.0)
N03_003: String (20.0)
N03_004: String (20.0)
N03_007: String (5.0)
OGRFeature(N03-12_13_120401):0
  N03_001(String) = �����s
  N03_002 (String) = (null)
  N03_003 (String) = �������S
  N03_004 (String) = ��������
  N03_007 (String) = 13308

Feature Count: 3297 と分かりますので、結構な量のデータが含まれているようです。 フィーチャーの属性としては N03_001, N03_002, N03_003, N03_004, N03_007 があります。 これらが都道府県名、支庁名、郡・政令市名、市区町村名、行政区域コードに当たります。 手元の環境では N03_001, N03_003, N03_004 で文字化けしています。 これはデータの文字コードが Shift-JIS だからです。 ファイルにリダイレクトして別のエディタで開くか nkf などで変換すると次のように確認できます。

N03_001 (String) = 東京都
N03_002 (String) = (null)
N03_003 (String) = 西多摩郡
N03_004 (String) = 奥多摩町
N03_007 (String) = 13308

ダウンロードページの説明にあった属性情報と照らし合わせると以下の関係であることが分かりますね。

  • N03_001 : 都道府県名
  • N03_002 : 支庁名 (当該都道府県が「北海道」の場合のみ)
  • N03_003 : 郡・政令市名
  • N03_004 : 市区町村名
  • N03_007 : 行政区域コード

OGR SQL を使うと属性情報でフィルタできます。 たとえば、23区は N03_003 に名称が含まれていて N03_004 が null になっていますので、 N03_004 IS NULL でフィルタできます。 ogr2ogr-where オプション (※ハイフンはひとつなので注意) で OGR SQL を受け付けますので、 下記のコマンドで23区のポリゴンデータだけを GeoJSON 形式に変換できます。

$ ogr2ogr \
    -f GeoJSON \                # 変換形式
    -where "N03_004 IS NULL" \  # フィルタ条件
    tokyo23.json \             # 出力ファイル名
    N03-120401_13_120401.shp    # 入力ファイル名

tokyo23.json が出来上がった GeoJSON ファイルです。 文字コードが Shift-JIS になっていると思いますので、テキストエディタで開いて文字コードを変更して保存し直すか、 nkf などのツールを使って UTF-8 にしておきます。

フィルタ条件を未指定の場合は八丈島なども含んだ東京全域のポリゴンデータになります。 GeoJSON だと結構なデータ量 (6.9MB) になりますので、ブラウザに読み込ませるときは気をつけましょう。 TopoJSON に変換すると 200KB 以下になると思います。 その他、フィルタ条件を調整することで好みのポリゴンを生成できます。 機械的に処理するときは「行政区域コード」を指定すると良いでしょう。

Shape ファイルを GeoJSON に変換して d3.js で表示する流れは、 英語ですが、こちらの記事も参考になるはずです。 Quantum GIS を使ったときの操作を紹介してくれています。

データの読み込み

HTML を作成し、d3.js のコードを読み込める環境を整えます。 冒頭で参照しているページの内容で十分だと思いますので詳細は割愛します。

Yeoman を使う場合は下記のように進めます。

注意:ここに記載した手順は Yeoman 1 以前のバージョンに基づいています。 Yeoman 1 以降の手順は末尾に追記したセクションで確認してください。
$ yeoman init
$ yeoman install d3
$ cp components/d3/d3.min.js app/scripts/vendor/
$ yeoman server

これでテストサーバが立ち上がってブラウザのページも開きます。 d3.js は AMD 非対応なので、require.config で shim を設定します。

注意:d3.js は バージョン 3.4 から AMD に対応しました。

main.js は次のようにしておきます。

require.config({
  shim: {
    d3: {
      exports: 'd3'
    }
  },

  paths: {
    // hm: 'vendor/hm',
    // esprima: 'vendor/esprima',
    // jquery: 'vendor/jquery.min',
    d3: 'vendor/d3.min'
  }
});

require(['d3'], function(d3) {
  'use strict';

  // PART 1: 後で埋めます

  d3.json('tokyo23.json', function(err, collection) {
    console.log(collection);

    // PART 2: 後で埋めます

  });
});

先ほど作成した tokyo23.json を app ディレクトリに置くと読み込まれるはずです。 JavaScript コンソールに GeoJSON の内容が表示されれば成功です。

ポリゴンの表示

おそらく、ここからが注力すべき、難しい部分になります。 理想的には SVG と投影法の両方の知識が欲しいところですが、d3.js がこの層を吸収してくれます。 d3.geo のパッケージを使いましょう。

まずは先ほどのスクリプトの PART 1 の部分に下記のコードを挿入します。

var width = 960,
    height = 560;

var g = d3.select('#main').append('svg')
              .attr('width', width)
              .attr('height', height)
              .append('g');

なお、HTML の index.html の好きなところに id="main" の DIV タグを用意しておきます。 width, height は好きに調整した方が良いでしょう。 ここでは画面いっぱいを使うような比率にしています。

次に PART 2 の部分に下記のコードを挿入します。

var projection = d3.geo.mercator()
                   .scale(450000)
                   .center(d3.geo.centroid(collection))
                   .translate([width / 2, height / 2]);
var path = d3.geo.path().projection(projection);
g.selectAll('path')
  .data(collection.features)
  .enter()
  .append('path')
  .attr('d', path)

  // PART 3: 後で埋めます

  ;

ぱっと見で何をやっているのかよくわからないコードですが、順番に追いかけてみます。

まず、Web マッピングにおける基本的な事項として、緯度経度の場所を画面にどのように対応付けるか? という課題があります。 緯度経度 (0, 0) を画面の中心にするのか、任意の地点、たとえば緯度経度 (35.39, 139.44)、を中心にするかを考える必要があります。 また、どこまでを画面に表示させるか、地球の丸みをどこまで考慮して緯度1度を画面に対応させるかも考えるべきかもしれません。 それはそれで面白いトピックかもしれませんが、d3.geo パッケージがこれらを抽象化してくれます。

上のコードでは、メルカトル図法 (よく見る平面の四角形の地図) で縮尺は 1:450000 に設定し、 中心は GeoJSON のデータの中心点に設定しています。 d3.js では GeoJSON のデータを簡単に扱えるようになっており、 d3.geo.centroid に FeatureCollection を渡すと、その中心を計算してくれるのです(たぶんバウンディングボックスを見てるんだと思います)。 ポリゴンデータの個々の緯度経度は、画面に表示されるときに translate() で指定したルールに沿って変換されます。 これらを d3.geo.path() で生成されるパスオブジェクトの投影法(projection)として設定します。

次に、ポリゴンデータを d3 のお作法に沿って割り当てます。 d3 ではループ処理をあまり使わず、Array データを与えて個別のコールバックを追加します。 data() で Array データを与え、 enter() の戻り値に対してコールバックを指定するのです。 collectionFeatureCollection なので、 collection.featuresFeature の集合になっています。 したがって、 Feature ごとに path 要素が追加されます。 最後に、 "d" という仮引数に投影された緯度経度を割り当てておきます。 ここが d3 の便利なところであり、よく分からない部分なのではないかと思います。

ポリゴンのスタイル設定

HTML/JavaScript/CSS の構成で実装する場合、色情報などは CSS で設定します。 SVG を使った場合も例外ではありませんので、JavaScript で HTML (DOM) にクラスを割り当て、CSS でクラスごとの描画情報を設定します。

国土数値情報の「行政区域界」では、東京23区は N03_004 が null になっていますので条件を判断してクラス名を変更できます。 たとえば、すべてのポリゴンに area というクラスを割り当て、23区は area23、それ以外には areaCity を追加することが考えられます。 この場合、上記の PART 3 の部分に下記のコードを挿入します。

.attr('class', function(d) {
  return 'area area' + (d.properties.N03_004 ? 'City' : '23');
})

これとは別に CSS で次のように色情報を設定します。

.area23 { fill: #ddc; }
.areaCity { fill: #cdc; }
.area { stroke: #000000; }
.area:hover { fill: #00ffff; }

インタラクションの追加

せっかくの Web マッピングならば、クリックしたときに動きがあって欲しいものです。 d3 のコールバックチェーンに下記の属性とイベントを割り当てることで、クリックした都市名を表示できます。 ID が "cityname", "citycode" の DOM は別途用意しておいてください。

.attr('data-cityname', function(d) {
  return d.properties.N03_003 ? d.properties.N03_003 : '' +
            d.properties.N03_004 ? d.properties.N03_004 : '';
})
.attr('data-citycode', function(d) {
  return d.properties.N03_007;
})
.on('click', function() {
  var self = d3.select(this);
  d3.select('#cityname').text(self.attr('data-cityname'));
  d3.select('#citycode').text(self.attr('data-citycode'));
})

終わりに

d3.js と GeoJSON を組み合わせて東京都のポリゴンを描いてみました。 参考元の記事にあった「境界線の表示」、「都市名の表示」、「国ラベルの表示」の3つは割愛しました。 テキストを落とす位置の調整も難しいトピックのひとつなので、時間があればチャレンジしたいですね。

Mike Bostock さんのサイトには Leaflet を使ったサンプル( D3 + Leaflet )もあります。 Leaflet で GeoJSON を扱うには L.GeoJSON も使えますが、 測地系や投影法の変換や、ほどほどな大きさのデータ処理も必要なときは d3.js が役に立つでしょう。 データ容量を抑えるために TopoJSON を導入してみることも興味深いトピックと言えます。

何はともあれ、東京って結構広いんだなぁ、ということがポリゴンのデータサイズから分かりました。 島嶼部には「所属未定地」という場所もあるようです。 現実世界の多様なデータを表現するときには「地図」が役に立つことも多々ありますので、 データ視覚化のひとつの手段として d3.js には期待が大きいですね。

EDIT1: 東京の時価公示データを眺める 記事を追加。(2013/07/15)

EDIT2: Yeoman 1.1 での手順を追加。(2014/02/05)

Yeoman 1 以降での手順

主な変更点

  • Yeoman の構成が大きく変わりました。
  • サーバーを起動するタスク名が "server" から "serve" になりました。
  • 基本的なジェネレーターでは RequireJS を使わなくなりました。
  • GeoJSON ファイルの拡張子を ".json" から ".geojson" に変更しました。

まずは nvm を使って Node をインストールします。

$ nvm install v0.10.25
$ nvm alias default 0.10
$ nvm use 0.10

Yeoman と基本的な Web アプリのジェネレーターをインストールします。

$ npm install -g yo
$ npm install -g generator-webapp

プロジェクトのディレクトリを作成します。

$ mkdir tokyo-polygon && cd $_

Yeoman で雛形を生成し、d3.js をインストールします。

$ yo webapp
$ bower install d3

サーバーを起動してブラウザを立ち上げます。

$ grunt serve

HTML (app/index.html) の JavaScript を読み込む部分を修正します。

<!-- build:js scripts/vendor.js -->
<!-- bower:js -->
<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/d3/d3.min.js"></script>  <!-- ADD -->
<!-- endbower -->
<!-- endbuild -->

JavaScript (app/scripts/main.js) を修正して d3.js が読み込めていることを確認します。 Google Chrome の開発者ツールなどで表示を確認してください。

/*global d3*/
'use strict';
console.log(d3);

正常に JavaScript が読み込めていることを確認したら、再び HTML を修正します。 Yeoman のジェネレーターで生成した雛形の "container" クラスのブロックを以下のように書き換えます。

<div class="container">
    <div class="header">
        <ul class="nav nav-pills pull-right">
            <li class="active"><a href="#">東京都 23区</a></li>
            <li><a href="#">東京都 島しょ部除く</a></li>
            <li><a href="#">東京都 全域</a></li>
        </ul>
        <h3 class="text-muted">東京ポリゴン</h3>
    </div>

    <div class="row">
        <h3>
            <span id="cityname">名称</span>
            <span id="citycode" class="small">JISコード</span>
        </h3>
    </div>
    <div class="row">
        <div id="main"></div>
    </div>

    <div class="footer">
        <p>
        </p>
    </div>

</div>

JavaScript と CSS (app/styles/main.scss) も修正します。 出来上がったソースファイルは gist に貼っておきました。

最後に、デプロイ用にビルドします。 Gruntfile.js を編集して、GeoJSON ファイルもリリース物として認識するようにします。 該当部分は以下のようになります。

// Copies remaining files to places other tasks can use
copy: {
    dist: {
        files: [{
            expand: true,
            dot: true,
            cwd: '<%= yeoman.app %>',
            dest: '<%= yeoman.dist %>',
            src: [
                '*.{ico,png,txt,geojson}',    // ← ここに拡張子を追加
                '.htaccess',
                'images/{,*/}*.webp',
                '{,*/}*.html',
                'styles/fonts/{,*/}*.*',
                'bower_components/' + (this.includeCompass ? 'sass-' : '') + 'bootstrap/' + (this.includeCompass ? 'fonts/' : 'dist/fonts/') +'*.*'
            ]
        }]
    },
    styles: {
        expand: true,
        dot: true,
        cwd: '<%= yeoman.app %>/styles',
        dest: '.tmp/styles/',
        src: '{,*/}*.css'
    }
},

Grunt のタスクを起動すると dist/ ディレクトリに配布用ファイルが生成されます。 これをサーバーにコピーすれば動くはずです。

$ grunt
コメントを投稿