【D3.js】ブラウザで日本地図を県別に描画・制御する

この記事では、以下のように D3.js を使用して描画した地図に対して、**インタラクティブに制御する方法**を共有します。

[<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/883b3404-6419-1327-4c6e-d37dacefff45.gif">](https://hazard.westa.io/)

地図を自在に描画・制御できると、色々と**幅が広がりそうでワクワク**します。

## 1. D3.js とは

D3.js はデータに基づいてドキュメント(SVG や DOM など)を操作するための JavaScript ライブラリです。
今回は D3.js を使い**地図情報(GeoJson データ)から SVG を生成してブラウザに描画**します。

![2020-03-19_13h39_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/0be0196c-b093-b347-09cc-3daa0265dc8c.png)

## 2. 地図データの準備

D3.js を使ってブラウザで地図を描画するためには **GeoJson 形式のファイルを用意**する必要があります。

ネット上を探してみると直ぐに利用できる状態の GeoJson 形式ファイルもありますが、ここでは信頼性のありそうでライセンス的に扱いが簡単な **Natural Earth からデータを取得して加工する**方法を解説します。

(手っ取り早く**加工済みの GeoJson データが欲しい方**は [こちら](https://hazard.westa.io/japan.geo.json) からどうぞ)

### (1) [Natural Earth](http://www.naturalearthdata.com/) / 世界地図のデータを取得
![NEV-Logo-color_sm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/4e77ae91-f2a2-3e81-f269-421997c43a67.png)

Natural Earth から世界地図をダウンロードします(次の工程で日本地図のみになるように加工します)。

サイト上の `Downloads` => `Large scale data, 1:10m` => `Cultural` => `Admin 1 – States, Provinces` => `Download states and provinces` から取得しておけば**日本の都道府県別情報を含むデータを取得**できます。
![2020-03-19_11h48_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/608fdf23-ec02-633e-b186-b4e12f259dfe.png)

なお、地図データのライセンスはかなり緩いようです。以下の場所から原文を確認できます。

> Natural Earth の利用規約
> https://www.naturalearthdata.com/about/terms-of-use/

### (2) [QGIS v3.12](https://www.qgis.org/ja/site/) / 地図を加工・ファイル形式変換ソフト
- **Windows, macOS, Linux などで動作**するフリーソフトです
- 地形のデータとなる点を**移動したり不要な地形を削除したり加工**ができます
- Natural Earth から取得した `.shp` 形式のファイル から **GeoJson 形式に変換**できます

#### a) `レイヤ` => `レイヤの追加` => `ベクタレイヤの追加` を選択
![2020-03-19_11h06_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/baf94a8b-81a9-810f-a900-5fd649f842bd.png)

#### b) Natural Earth で取得した shp ファイルを`ベクタデータセット` に指定して `追加` ボタンをクリック
![2020-03-19_12h09_42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/4576affc-b666-ba09-eb3c-50e573c6e51f.png)

#### c) `① 編集モードに切り替え` =>  `② 地物の選択に切り替え` => `③ 不要な地形を削除`
![2020-03-19_12h12_49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/4c2f659e-c5df-f32a-5f1b-d0fca554852b.png)

#### d) 編集メニューから地物ポリゴンを移動させたり、新たにポリゴンを追加も可能
![2020-03-19_12h18_57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/5f5460b0-9ce2-b939-a9d7-19815ed25f92.png)

#### e) GeoJson データ形式としてエクスポート
![2020-03-19_12h59_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/46c3cf57-0ded-bf04-f539-4a0a1383a917.png)

### (3)  [mapshaper](https://mapshaper.org/) / 地図データの軽量化サービス
- 地形のデータとなる点情報をイイ感じに削減してデータを軽量化できます
    - (QGIS からでも軽量化できますが使いやすいのでこのサービスを利用しました)

`Simplify` ボタンから表示されるスライダーで軽量化、`Export` ボタンで出力できます。
![2020-03-19_13h18_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/cf01ddab-a3e3-b0a7-cc19-897efcba594f.png)


## 3. 地図データを D3.js で描画

TypeScript のコードになりますが、JavaScript が読めれば問題ないと思います。
解説はコード内のコメントをご確認ください。

なお、最後の方に動作確認デモへのリンクも載せてあります。

```terminal:d3パッケージをインストール
> npm install d3@5.15.0
```

```ts:地図データをD3.jsで描画するコード
import * as d3 from "d3";

// GeoJsonファイルを読み込み
import geoJson from "~/assets/japan.geo.json";

async function main() {
  const width = 400; // 描画サイズ: 幅
  const height = 400; // 描画サイズ: 高さ
  const centerPos = [137.0, 38.2]; // 地図のセンター位置
  const scale = 1000; // 地図のスケール

  // 地図の投影設定
  const projection = d3
    .geoMercator()
    .center(centerPos)
    .translate([width / 2, height / 2])
    .scale(scale);

  // 地図をpathに投影(変換)
  const path = d3.geoPath().projection(projection);

  // SVG要素を追加
  const svg = d3
    .select(`#map-container`)
    .append(`svg`)
    .attr(`viewBox`, `0 0 ${width} ${height}`)
    .attr(`width`, `100%`)
    .attr(`height`, `100%`);

  // 
  // [ メモ ]
  // 動的にGeoJsonファイルを読み込む場合は以下のコードを使用
  // const geoJson = await d3.json(`/japan.geo.json`);
  // 

  // 都道府県の領域データをpathで描画
  svg
    .selectAll(`path`)
    .data(geoJson.features)
    .enter()
    .append(`path`)
    .attr(`d`, path)
    .attr(`stroke`, `#666`)
    .attr(`stroke-width`, 0.25)
    .attr(`fill`, `#2566CC`)
    .attr(`fill-opacity`, (item: any) => {
      // メモ
      // item.properties.name_ja に都道府県名が入っている

      // 透明度をランダムに指定する (0.0 - 1.0)
      return Math.random();
    })

    /**
     * 都道府県領域の MouseOver イベントハンドラ
     */
    .on(`mouseover`, function(item: any) {
      // ラベル用のグループ
      const group = svg.append(`g`).attr(`id`, `label-group`);

      // 地図データから都道府県名を取得する
      const label = item.properties.name_ja;

      // 矩形を追加: テキストの枠
      const rectElement = group
        .append(`rect`)
        .attr(`id`, `label-rect`)
        .attr(`stroke`, `#666`)
        .attr(`stroke-width`, 0.5)
        .attr(`fill`, `#fff`);

      // テキストを追加
      const textElement = group
        .append(`text`)
        .attr(`id`, `label-text`)
        .text(label);

      // テキストのサイズから矩形のサイズを調整
      const padding = { x: 5, y: 0 };
      const textSize = textElement.node().getBBox();
      rectElement
        .attr(`x`, textSize.x - padding.x)
        .attr(`y`, textSize.y - padding.y)
        .attr(`width`, textSize.width + padding.x * 2)
        .attr(`height`, textSize.height + padding.y * 2);

      // マウス位置の都道府県領域を赤色に変更
      d3.select(this).attr(`fill`, `#CC4C39`);
      d3.select(this).attr(`stroke-width`, `1`);
    })

    /**
     * 都道府県領域の MouseMove イベントハンドラ
     */
    .on("mousemove", function(item: any) {
      // テキストのサイズ情報を取得
      const textSize = svg
        .select("#label-text")
        .node()
        .getBBox();

      // マウス位置からラベルの位置を指定
      const labelPos = {
        x: d3.event.offsetX - textSize.width,
        y: d3.event.offsetY - textSize.height
      };

      // ラベルの位置を移動
      svg
        .select("#label-group")
        .attr(`transform`, `translate(${labelPos.x}, ${labelPos.y})`);
    })

    /**
     * 都道府県領域の MouseOut イベントハンドラ
     */
    .on(`mouseout`, function(item: any) {
      // ラベルグループを削除
      svg.select("#label-group").remove();

      // マウス位置の都道府県領域を青色に戻す
      d3.select(this).attr(`fill`, `#2566CC`);
      d3.select(this).attr(`stroke-width`, `0.25`);
    });
}

main();
```

以下のリンクに D3.js を使用して地図を描画する**デモを用意**しました。

> 動作デモ (StackBlitz)
> https://stackblitz.com/edit/d3map


## 4. 最後に

勢いで書いてしまったので適当な部分もありそうですが、`D3.js` ＋ `地図描画` いかがでしょうか。
記事の内容が『役に立った』『面白かった』と思ったら、ぜひ **[LGTM(いいね)](https://blog.qiita.com/like-to-lgtm/) を頂けると嬉しい**限りでございます！