はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

d3.js でGeoJSONファイル指定の白地図を表示(地図のズーム、ドラッグ可能)

GeoJSONファイルのd3.jsでの可視化にチャレンジしてみました(という、サンプルコードのセルフリマインダです)。

「GeoJSON 可視化」などでググると、やはりというか、さすがというかd3.jsでさっくり可視化ができるんだね〜ということがわかるのですが、ほぼミニマムなコーディング例で、ただ、できればGoogle Mapsみたいに、ドラッグで移動、マウスホイールでズームぐらいのインタラクティブはできると嬉しいね... ということで、そのようなコーディング例です。

f:id:azotar:20200429144313p:plain

d3.jsでGeoJSONを表示

早速ですが例です。

後述のpref.jsファイルと組み合わせると、pref.jsで定義されているGeoJSONオブジェクトのデータのシェイプを表示できます。

また、ドラッグで地図の移動、マウスホイールでズームができます。

◆sample.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <script src="pref.js"></script>
    <style>
        body {
            background-color: black;
        }
        .mapsvg {
            background-color: white;
        }
    </style>
</head>
<body>
    <script>

        let width = 1000,
            height = 1000

        let svg = d3.select("body").append("svg").attr("width", width).attr("height", height)
            .classed("mapsvg", true)

        // 地図
        let center = [137, 37]
        let scale = 1500
        let geojson = pref // pref.jsで読み込み済み

        let proj = d3.geoMercator()
            .center(center)
            .scale(scale)
            .translate([width / 2, height / 2])

        let render = d3.geoPath().projection(proj)

        let mapg = svg.append("g")
        let map = mapg
            .selectAll("path")
            .data(geojson.features)
            .enter()
            .append("path")
            .attr("d", render)
            .style("stroke", "gray")
            .style("fill", "yellow")

       // ------- ここまでで、地図の表示ができる。

        // svgのズーム対応(イベントはsvgに対応づけるが、実際は地図map(mapg)に作用させる)
        let zoomeffect = () => {
            let t = d3.event.transform
            mapg
                .attr('transform', t)
                .attr('stroke-width', 1 / t.k)
        }

        svg.
            call(d3.zoom().on('zoom', zoomeffect))

    </script>
    </body>
</html>

◆ pref.js

冒頭のサンプル画像は日本全体を含むようなものでしたが、データ量が多くなるので、ここでは、愛知県部分のデータを抜き出し、コツコツと行数を削減した例を直接貼り付けておきます。

let pref =
{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "geometry": {
                "type": "MultiPolygon",
                "coordinates": [
                    [[
                        [137.341, 34.647],
                        [137.074, 34.662],
                        [137.102, 34.632],
                        [137.156, 34.654],
                        [137.237, 34.693],
                        [137.266, 34.708],
                        [137.300, 34.693],
                        [137.315, 34.688],
                        [137.309, 34.700],
                        [137.351, 34.727],
                        [137.311, 34.803],
                        [137.250, 34.805],
                        [137.214, 34.815],
                        [137.186, 34.792],
                        [137.168, 34.787],
                        [137.105, 34.791],
                        [136.996, 34.816],
                        [136.955, 34.854],
                        [136.976, 34.893],
                        [136.971, 34.908],
                        [136.934, 34.874],
                        [136.936, 34.854],
                        [136.921, 34.828],
                        [136.969, 34.725],
                        [136.976, 34.701],
                        [136.851, 34.748],
                        [136.837, 34.875],
                        [136.828, 34.961],
                        [136.869, 35.008],
                        [136.873, 35.071],
                        [136.884, 35.088],
                        [136.857, 35.059],
                        [136.843, 35.041],
                        [136.849, 35.077],
                        [136.838, 35.030],
                        [136.809, 35.049],
                        [136.780, 35.033],
                        [136.769, 35.021],
                        [136.677, 35.128],
                        [136.677, 35.241],
                        [136.864, 35.372],
                        [136.991, 35.424],
                        [137.047, 35.336],
                        [137.067, 35.308],
                        [137.102, 35.301],
                        [137.185, 35.253],
                        [137.345, 35.272],
                        [137.408, 35.234],
                        [137.520, 35.273],
                        [137.570, 35.282],
                        [137.576, 35.220],
                        [137.655, 35.228],
                        [137.759, 35.221],
                        [137.823, 35.211],
                        [137.823, 35.158],
                        [137.802, 35.130],
                        [137.778, 35.099],
                        [137.704, 34.972],
                        [137.639, 34.892],
                        [137.549, 34.843],
                        [137.496, 34.786],
                        [137.483, 34.673],
                        [137.340, 34.647]
                    ]]]

            }
        },
    ]
}

f:id:azotar:20200429135435p:plain

補足

実は後述の参考サイトからほぼ拝借したような例になってしまいましたので、ここでは、この記事である程度固有なところに絞って補足します。

  1. d3.jsはブラウザ上に動的にSVGを描画できる(もちろん他にもいろいろできる)のですが、ほぼその基本のとおりで地図を表示できます。(html中の「 ------- ここまでで、地図の表示ができる。」のあたりまで)
  2. d3.jsの「data joins」に従い、data()メソッドなど、ほぼ基本のとおりなのですが、インプットデータがGeoJSONの場合、180までの数字のデータになることと、平面で表示する都合で、ここではメルカトル図法になるようにd3.geoMercator()というAPIを使って、SVGの出力がよろしくなるように調整しています。(が見てのとおり、ひとまず表示してみるだけなら、理屈から入るよりも、パラメータとして渡している係数の数字を変更してみて肌感覚をつかんだ方が早いです。)
  3. コメントの「// svgのズーム対応」以降が、ズーム、ドラッグの仕掛けです。ここまでの説明の文字面に反して、またhtmlのマークアップ中の変数名・関数名に反して、この記述だけでズームだけでなく、ドラッグによる移動も可能です。少なくとも、v5の現在のバージョン、および私の手元のいくつかのブラウザの範囲であれば。
  4. d3.event.transformを selections.attr('transform')の第2引数で渡してやれば、ドラッグ操作もふくめたイベントをSVGのtransform属性によろしく引き継いでくれるようです。GeoJSONを入力にしていますが、もっと複雑なデータ、あるいは逆にもっとシンプルなデータでもおそらくこのやり方で、ズームとシンプルならドラッグのみであれば対応できるのではないでしょうか。

余談

上記の補足の3つめに関して余談です。

当初、ドラッグで移動→「ドラッグ」という言葉からの思い込みで d3.drag()を使うべきと思って調べていました。

しかし、矩形の地図領域を移動させるという範囲であれば、d3.zoom()をフックさせることで十分そうだったので、これにとどめています。

結果として、他のもっと高度なことをするための例より、シンプルな例に収まりました。

なお、 d3.drag()の場合、「ドラッグする領域(地図でいうと、陸地の部分と海の部分で、海部分をつかんでも地図を移動できるようするか)」の挙動の制御必要になりそうです。あとになってみればですが、「ドラッグ」という位置付けからすると確かにそうですね。

この他、ここでは、よりシンプルな例に止めることから、pref.jsファイルに変数として定義されたGeoJSONオブジェクトを取り扱いましたが、d3.jsには、XHRでWebサイト上の生のGeoJSONファイルを読み込んでよろしく対応できる便利なAPIがあります。

GitHub - d3/d3-fetch at v1.1.2

参考文献

公式リファレンス(公式サイトからリンクされているgithubのドキュメント)のうち、本記事関連のAPIなどの情報

github.com

github.com

d3.jsの基本っぽいところの当ブログの別記事

d3のselectAllとかdataとかenterとかappendとかattrについて簡単な例をまとめてみたもの↓

itdepends.hateblo.jp

※本記事の説明で省略したようなところをまとめてあります。

この文章を書くにあたり勉強させてもらったサイト

qiita.com

qiita.com