d3.jsを用いた地図の描画とプロット：気象庁降灰予報を描画してみる

気づいたらd3.jsのバージョンがv5になっていて、v4も大して触れなかったし多少リハビリ変えて前やったことを再現してみるか、と思い立ったので

# 何をするか

d3.jsというとデータのビジュアライズに長けたライブラリですが、自分はひたすらに地図の描画に使っていました。
d3での地図描画と、そこにいろいろとプロットしてみるという点で、桜島の降灰の予報データを用いて降灰予報をプロットしてみます。
完成図は下記
![ashfall.png](https://qiita-image-store.s3.amazonaws.com/0/140947/f6f41e3f-0380-343b-eb55-f2cd246111f7.png)

実際に動いているのは以下
https://miyawa-tarou.github.io/draw_ash_fall_d3/index.html

d3.jsの地図描画部分と、SVGの図形描画を合わせたようなことを行っています。

## 降灰予報データについて

気象庁のXMLのサンプルデータと仕様は以下に置いてあります。

- 気象庁防災情報XMLフォーマット　技術資料：http://xml.kishou.go.jp/tec_material.html
- XMLの仕様：http://www.data.jma.go.jp/add/suishin/jyouhou/pdf/409.pdf

一式入ったzipなので探すの手間ですが、降灰予報はVFVO[53-55]になります。

下記ページのような気象庁が出している図が書けるように、XMLの中にポリゴン用の座標が示されています。
http://www.data.jma.go.jp/svd/vois/data/kouhai/jishin/ashfall.html

### 追記（2019/1/7）

上でサンプルだけ貼りましたが、今はネット上でデータを取得できるようです。
http://xml.kishou.go.jp/xmlpull.html
Atomで地震と火山の情報を合わせたリストになっており、その中に降灰予報XMLのURLが記されているため、そこから取得できます。


# 手順

下記手順はブランチに分けて書きレポジトリに入れています。
https://github.com/miyawa-tarou/draw_ash_fall_d3

### 0, 地図を用意する

d3.jsでは地図データがあれば地図を簡単に描画できます。
地図データはgeojsonやtopojsonの形式になります。

そのデータの探し方・作り方は今回と関係ないですが、自分は国土交通省などshapeファイルを配っているところから、QGISなどを利用して変換しています。
（コマンドラインでもできるものがありますが、付加情報などをつけるためにQGISを用いている）

今回は面倒なので、あまり考えずに昔作ったtopojsonをコピペして利用します。
https://github.com/miyawa-tarou/japan_map_json

### 1, 地図を描画する

##### d3.jsとtopojsonを読み込む

最初触ったころはv3で、気づいたらv5まで上がっていて、細かいメソッドの呼び出し方が変わっている。
今回触るところは呼び出し方が変わったくらいなので楽だった
テスト用なので公式で上がっているものを利用

```html
    <script src="https://d3js.org/d3.v5.min.js"></script>
    <script src="https://unpkg.com/topojson@3"></script>
```

##### 地図の描画

d3はタグを指定するのに、select()を使えば取れます
いつも地図の描画部分は半分理解しつつ、半分おまじないだと思ってコピペしています。


```javascript

<svg id="map"></svg>
<script>
    (function() {
        // SVGの大きさ
        var width = 600; // SVGタグ側で指定して、それを取得する形でもよいかもしれない
        var height = 600;

        // 中心座標とスケール
        var projection = d3.geoMercator() // メルカトル図法で描画する
            .center([137, 38]) // 地図の中心になります。経度緯度の順番なのが慣れない
            .scale(1600)       // 拡大率です。割と感覚というか試行錯誤して決めてます。
            .translate([width / 2, height / 2]);

        // おまじない
        var path = d3.geoPath().projection(projection);
        var svg = d3.select("#map")
            .attr("width", width)     // 幅・高さを指定する
            .attr("height", height);
        d3.json("./map/japan.json").then(function(data) { // 非同期で読み込まれる
            svg.selectAll("path")  // おまじない。というか理解できてない。色々変えても動く。
                .data(topojson.feature(data, data.objects.pref).features) // GeoJsonの場合はjsonそのままでdata()の引数と同じものが入るはず
                .enter() // dataで入れた配列を要素ごとにそれぞれ以下のpathにしていく
                .append("path") // これはSVGタグの下のpathタグ
                .attr("d", path) // <path d="">に　path()の返り値を入れていく。これは上で代入したpath変数（中身はメソッド）
                .attr("fill", function(d,i){ return "green";}); // こういうのができるよ的な意味からreturnで返してみるがこれだけだと ("fill", "green")を同じ
        });
    })();
</script>
```

参考資料
- https://github.com/d3/d3/blob/master/API.md#geographies-d3-geo
- https://github.com/d3/d3-geo

![ashfall1.png](https://qiita-image-store.s3.amazonaws.com/0/140947/bf689bbb-20f0-ff33-532a-2d9ca8427634.png)


### 2, 桜島にズームして点をプロットする

ズームの類は前項に書いたように以下をいじれば終わりです。

```javascript
        var projection = d3.geoMercator() // メルカトル図法で描画する
            .center([137, 38]) // 地図の中心になります。経度緯度の順番なのが慣れない
            .scale(1600)       // 拡大率です。割と感覚というか試行錯誤して決めてます。
            .translate([width / 2, height / 2]);
```

図形のプロットはd3の知識というよりは、SVGの知識になります。
座標だけは桜島の緯度経度をもとにSVG上の座標に変える必要性はあるので、d3を用いることになります

##### 円プロット

山っぽく三角形にしたかったけどかなり手間だったので円でごまかします。
SVGにはcircleタグがありそれで座標と半径を指定すれば置けます
https://developer.mozilla.org/ja/docs/Web/SVG/Element/circle

今回「桜島の位置」に配置したいので、緯度経度をSVG上の座標に変換する必要があります。
ズームや地図の投影法、中心緯度経度によって座標は変わりますが、それの指定をしたprojectionを使えば変換できます。

```javascript
            var coordinate = projection([130.65, 31.6]); // 経度、緯度の並びになるので注意
```

あとはそのままX,Y座標として入れるのみ

```javascript
            svg.append('g') // SVGのグループ
                .append('circle')
                .attr('cx', coordinate[0])
                .attr('cy', coordinate[1])
                .attr('r', 10)
                .attr('stroke', 'black') // 枠線色
                .attr('fill', 'black'); // 塗りつぶし色
```

![ashfall2.png](https://qiita-image-store.s3.amazonaws.com/0/140947/b8567aba-f5bc-692e-4781-805061f3b956.png)

### 3,XMLを取得する

気象庁サンプルからテキトーに引っ張ってきたXMLの中の降灰予報のポリゴンを取得します。
https://github.com/miyawa-tarou/draw_ash_fall_d3/compare/02_zoom_sakurajima...03_get_xml#diff-eacf331f0ffc35d4b482f1d15a887d3b

コードを読んでください。

```
+31.5979+130.6600/+31.5978+130.6800/+31.5800+130.6978/+31.5604+130.6800/+31.5615+130.6600/+31.5800+130.6421/+31.5979+130.6600/

　↓

[[31.5979, 130.6600],[31.5978,130.6800],[31.5800,130.6978],[31.5604,130.6800],[31.5615,130.6600],[31.5800,130.6421],[31.5979,130.6600]]
```

GeoJsonなどのポリゴンでもそうですが、必ず配列の最初の座標と最後の座標は一致して一周するようになります。
一周しなかったらポリゴンじゃない、ただの線だ。
http://s.kitazaki.name/docs/geojson-spec-ja.html


### 4, 降灰予報を描画する

描画も
d3の機能というより、ほとんどSVGですね。
polygonタグのpoints要素に<polygon points="x,y x,y x,y">という感じで座標を","" "区切りで入れるだけです。

```javascript
                for (var i = 0; i < ashCoordinate.length; i++) {
                    // 緯度経度をSVG上の座標に変換する。またのちの処理のため文字列での,区切りに変える。
                    svgCoordinate.push(projection([ashCoordinate[i][1], ashCoordinate[i][0]]).join(',')); // convertCoordinateToArray内で順番変えたほうが賢い
                }
                svg.append('g')
                    .attr('class', 'ashFall')
                    .append("polygon")
                    .attr("points", svgCoordinate.join(' ')) // 各座標はスペース区切りで、"x,y x,y x,y"
                    .attr("stroke", "black")
                    .attr("stroke-width", 1)
                    .attr("fill", "#333")
                    .attr("fill-opacity", "0.5");
```

初めにGeoJsonやtopoJsonで地図が書けると書きましたが、地図のSVG pathを見るとわかるように、
topojsonはGeoJsonを圧縮したものに近いものですし、GeoJsonも緯度経度が羅列されたものなので（それ以外の情報も含む）
GeoJsonという決まった形式であれば上記処理とほぼ同じことをd3.jsが自動で行ってくれているだけです。

https://github.com/miyawa-tarou/draw_ash_fall_d3/compare/03_get_xml...04_plot_svg

### ex, 鹿児島の色を変えてみる

元データのjsonには都道府県別にいくつかデータが入っています。
桜島ということで、そのデータを使い鹿児島県だけ色を変えてみます。

1で雑な感じで地表の色としてgreenを入れましたが、function(d,i)のdはjsonに一緒に入っている要素・値になります。
今回地図データはほぼ触れませんでしたが、以下のような形で都道府県コード（JIS)が取れます（日本語名なども入れています）

```javascript
.attr("fill", function(d,i){ 
    console.log(d.properties.code)
}
```

これを使って例えば鹿児島県46だけをdarkgreenにしようとするなら

```javascript
.attr("fill", function(d,i){ return d.properties.code === 46 ? "darkgreen" : "green";});
```

こんな感じで変えることができます。

![ashfall.png](https://qiita-image-store.s3.amazonaws.com/0/140947/f6f41e3f-0380-343b-eb55-f2cd246111f7.png)

### 蛇足：描画が遅い

地図がでかいと描画に時間がかかってしまいます。
今回の件だと、九州南部以外はいらないのでそれ以外は読み込まない・描画しないが大事だと思います。

逆に日本全体で表示するには地図が細かすぎるものを使っています。
その場合はもっと簡略化した地図を使うのが良いかと思います。
QGISなどでもなんとかなった気がしますが、一番楽なのは[mapshaper](http://mapshaper.org/)かなと思っています
shapeファイルをそのまま突っ込んで、簡略化しながらGeoJSONやtopoJsonに変換することもできます。