d3.jsで描いた地図をGoogleMapの上に載せる方法

はじめに

先日、以下の記事を描いたが、毎回ここで終わってしまうが、きっちりとしたサイトを作るにおいて、地図の描画をユーザのインタラクティブ(マウス操作等)に追従させるには一苦労するコードを書かないといけなくて悩ましかった。

d3.js (v6) でブラウザに地図を描く方法

おそらく、GoogleMapなどと連携させれば、この面倒な操作系の処理はGoogleMap側に任せて、D3.jsの地図描画は自動的にそれに合わせて更新(拡縮、移動)されるはずと思って調べると、やはりそういうことが可能で解説記事見つかったので自分でも実験してみる。

GoogleMapの利用

GoogleMap JavaScript SDK ( Google Maps Platform )を利用する。準備として Google Cloud Platform へのアカウント登録&支払い方法の登録(十分な無料利用枠は存在するが基本的に有料Platform)が必要。今回はこの説明は省略する。 GoogleMap以外にも、leafletMapBox でも同様のことが可能だとは思う。

d3.js on Google Map

前回の記事のコードを流用してUpdateするため、手順をコピーしたい方は前回の記事を写経してから本記事に進むことをおすすめします。

前回のコード中のd3.js での地図表示のメイン処理である、D3Map.ts を以下に改造する。改造の前にGoogleMapSDKのTypeScript用の型定義をインストールしておく

% npm install -D @types/google.maps
import * as d3 from "d3"
import * as topojson from "topojson-client";

export default class D3Map {

    projection: any
    path: any
    map: google.maps.Map;
    overlay: google.maps.OverlayView
    
    static readonly JAPAN_GEOJSON = "./tokyo.topojson"

    constructor(width: number, height: number, elm: string = "main") {

        const center: google.maps.LatLngLiteral = {lat: 35.7, lng: 139.5};
        this.map = new google.maps.Map(document.getElementById(elm) as HTMLElement, {
            center,
            zoom: 10
          });

        this.overlay = new google.maps.OverlayView();

        this.overlay.onAdd = () => {
            var layer = d3.select(this.overlay.getPanes()!.overlayMouseTarget).append("div").attr("class", "SvgOverlay");
            var overlayProjection = this.overlay.getProjection()
            this.projection = d3.geoTransform({point: function(x, y) {
                var d = new google.maps.LatLng(y, x);
                var dd = overlayProjection.fromLatLngToDivPixel(d);
                this.stream.point(dd!.x + 4000, dd!.y + 4000);
            }});
            this.path = d3.geoPath().projection(this.projection);


            d3.json(D3Map.JAPAN_GEOJSON).then( (data: any) => {                
                var svg = layer.append("svg").append("g");
                this.overlay.draw = () => {
                    svg.selectAll("path")
                    .data((topojson.feature(data, "tokyo") as any).features)
                    .attr("d", this.path)
                    .enter()
                    .append("path")
                    .attr("stroke", "#333")
                    .attr("fill", "#fff")
                    .attr("fill-opacity", "0.6")
                    .on("mouseover", (e: any, n: any)  => {
                        d3.select(e.currentTarget)
                            .attr("fill", "red")
                        d3.select("#info")
                            .html(n.properties.JCODE + ": " + n.properties.KEN + " " + n.properties.SIKUCHOSON)
                    })
                    .on("mouseout", (e: any, n: any) => {
                        d3.select(e.currentTarget)
                            .attr("fill", "#fff")
                    })
                    .on("click", (e: any, n: any)  => {
                        console.log("hoge")
                        d3.select(e.currentTarget)
                            .attr("fill", "red")
                        d3.select("#info")
                            .html(n.properties.JCODE + ": " + n.properties.KEN + " " + n.properties.SIKUCHOSON)
                    })
                  }
            })
        }
        this.overlay.setMap(this.map)
        setTimeout(() => {
            const center2: google.maps.LatLngLiteral = {lat: 35.7, lng: 139.7};
            this.map.setCenter(center2);    
        }, 500)
    }
}

解説

基本的には、GoogleMapを描画し、GoogleMapのLayer機能を用いる。Layerのoverlayにsvgを広げそこにD3.jsで地図を描画するのが流れ。this.overlay.onAdd が overlayのカスタマイズ(地図描画)であり、最後に作成したoverlayをGoogleMapに被せている。

var layer = d3.select(this.overlay.getPanes()!.overlayMouseTarget)....

上のコードでoverlayMouseTarget というoverlayを選択してそこにsvgをappendしている。他の解説記事ではこのレイヤの選択をoverlayLayerとする記事が多かったが、それを選択すると地図は描画できたが on(マウスイベント)が一切反応せず、これを overlayMouseTarget とすることで解決できた。

また、今回の肝な部分はProjection/Pathの定義の部分は以下であり、GoogleMapのProjectionにD3のpathをマッピングしている。 4000 という数値があるが、これはGoogleマップが地図中心原点であることによる原点補正の処理である。

var overlayProjection = this.overlay.getProjection()
this.projection = d3.geoTransform({point: function(x, y) {
    var d = new google.maps.LatLng(y, x);
    var dd = overlayProjection.fromLatLngToDivPixel(d);
    this.stream.point(dd!.x + 4000, dd!.y + 4000);
}});
this.path = d3.geoPath().projection(this.projection);

また、追加として CSS での定義が追加で必要である

.SvgOverlay svg {
      position: absolute;
      top: -4000px;
      left: -4000px;
      width: 8000px;
      height: 8000px;
}

以上により、実行するとGoogleMapにD3.jsのレイヤーが追加され、GoogleMapの地図操作に連動してD3.jsのレイヤが追従してくれることが確認できる

おわり

まあ、出来るんだろうなと思いながら確認せずに過ごしてきましたが、実際に実現を確認できてよかった。今後はD3.jsで地図を扱うときには積極的にこの方法を用いていきたい。