leafletのbasemap機能を応用して作る切り替え可能な統計色分けマップ（コロプレス図）

# 作ったもの
[世界の国別統計色別マップ- World Chroths Map](https://co-place.com/chroths/jp/)
世界の様々な種類の国別統計、例えばGNI(国民所得)、人口密度、人間開発指数、幸福度指数、インターネット浸透率といったものを、一つのマップ上で切り替えてみることができるウェブアプリケーションです。
こうした統計の色分けマップはコロプレス図（Choropleth Map）と呼ばれています。
<img width="500" alt="sample2.png" src="https://qiita-image-store.s3.amazonaws.com/0/122865/918faada-ac62-7669-e1e5-6e6bddf36eb3.png">

# 作成手順

ざっくりと下記の流れで作成しています。

1. ウェブ上で入手できる各種統計データをCSVで整理
2. 国境線ポリゴンデータをGISで作成し、統計データCSVと結合して、そのデータをGeoJson形式で出力
3. leafletをベースにHTML/CSS/JavaScriptファイルを構築


# leaflet.jsについて
これを作成するにあたって、ウェブマップ作成に使用されるJavaScriptライブラリの一つである[leaflet](https://leafletjs.com)を使いました。
leafletはウクライナ人のVladimir Agafonkin氏によって開発・公開されたオープンソースのウェブマッピング用ライブラリです。
leafletには「ベースマップ（下図）を登録して切り替えられるようにする」基本機能があるのですが、今回はこれを応用して、basemapに各種統計レイヤを登録することでサクサク切り替え可能な統計マップを作成しました。

leafletのスタンダードな使い方については[公式チュートリアル](https://leafletjs.com/examples.html)等で確認してもらえればと思います。

また、このアプリケーションにはleafletのプラグインも大いに活用させてもらっています。開発者の皆さんに感謝です。


# 構成
アプリケーションの構成は下記の通りです。

-index.html ・・・HTMLファイル：ライブラリの読込みや各ファイルへのリンクを記述
-Application/
　├ css/
　｜　　└ 各種cssファイル
　├ data/
　｜　　└ data.js ・・・geojson形式で作成したデータ（拡張子を.jsに変更）
　└ js/
　　　　├ main.js ・・・JavaScriptの記述
　　　　└ その他leafletプラグイン等のjsファイル


# 1. 統計データの整理
統計データはウェブ上で入手可能な様々な情報源からデータをダウンロードし、csvで整理しました。
API経由とかで一気にデータ取得できたら良いのですが、公開データがPDF形式しかなかったり、あとは統計の作成元によって国名の表記とかバラバラなので、その辺りをほぼ手作業で統一しています。
国名を名寄せする作業が一番大変だったかもしれません。
（国名ってこんなに表記の揺れがあるんだなと、あらためて驚きました。作成作業中にスワジランドがエスワティニ/Eswatiniという国名に変わったり。。）

また、アプリケーション上の【 i 】アイコンから出典とそのリンクは参照できるようにしています。

# 2. 国情報のポリゴンデータをgeojson形式で出力
leaflet.jsで描画するポリゴンデータはgeojson形式で準備します。データの加工には[QGIS](https://www.qgis.org/ja/site/)を使用しました。

geojsonとはjson形式のファイルに地理情報（座標系や各点のXY座標値などの情報）を持たせたデータ形式です。
また、jsonとはJavaScript Object Notationの略で、JavaScriptでオブジェクトとして読み込める形状にしたデータ形式のことです。

geojsonファイルは、まずQGIS上で各国の国境線をポリゴンデータで作成し、それに各種統計をまとめたcsvファイルを国名（正確には各国に対してあらかじめ設定しておいた国ID）をキーとして結合し、QGISの[別名保存（save as）]でgeojson形式を指定して出力しています。

データの内容はこんな感じです。CRS(座標参照系)やフィーチャのジオメトリタイプ（この場合はマルチポリゴン）といった地理情報が盛り込まれています。

```javascript:data.geojson
{"type": "FeatureCollection",
"name": "data","crs": { "type": "name", "properties": { "name":"urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
 { "type": "Feature", 
  "properties": {
  //各データの属性値の情報
  }, 
  "geometry": { "type": "MultiPolygon", "coordinates": [
  //各データの座標値の情報
  ]}
 }]
}
```

今回のケースでは、geojsonで出力する際に文字コードをUTF-8に、CRS(座標参照系)をWGS84(EPSG:4326)に設定することが注意点です。
このWGS84(EPSG:4326)はウェブ地図でよく見る世界地図の表現方法です。ウェブマッピングの際、特に世界地図を全体表示するようなアプリケーションではこのCRS（もしくはEPSG:3857）に設定するのが基本かと思います。
また、EPSGのコードはウェブマッピングではSRID(Spatial Reference ID)の設定値としても使われます。

# 3. JavaScriptの記述
## マップの基本情報の設定
下記のコードでウェブマップの初期表示と、”本当の”ベースマップを設定します。
ここではベースマップとして[OpenStreetMap](https://www.openstreetmap.org/)を読み込みます。

```javascript:main.js
const map = L.map('map', {
                zoomControl: true, maxZoom:15, minZoom:1
                }).setView([21.0000, 36.0000], 2);

const basemap_main = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
                               {attribution: '&copy; <a href="https://openstreetmap.org">OpenStreetMap</a> contributors,<a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',maxZoom: 15});

const mapBounds = [[90,-360],[-90,360]];
map.setMaxBounds(mapBounds)
```

マップのmaxZoomを15に設定しているのは、あまりマップを拡大されると国境線の粗が見えてしまうためです。
今回、国境線のポリゴンデータはそこまで正確なものは作成していませんし、そもそも国境線はセンシティブな問題ですので、その粗さが気にならない程度のズームレベルに敢えて制限しています。

##統計情報レイヤの設定
次に、各種の国別統計情報レイヤを設定します。
下記は人口密度のレイヤについて設定したものです。記述は[leafletの公式チュートリアルのコロプレス図作成方法](https://leafletjs.com/examples/choropleth/)に倣っていますが、各国の情報をツールチップで表示するあたりは適宜アレンジしています。

```javascript:main.js
//Population Density
let Country_Stats_PPD; //レイヤ名を設定（中身は後から代入）

//数値による色分けを設定。今回は事前に各統計で国順位を6分位にしてあるので、その順位グループの値を参照する。
function getColor_PPD(d){
    return d == 6 ? '#d53e4f':
    d == 5 ? '#fc8d59':
    d == 4 ? '#fee08b':
    d == 3 ? '#e6f598':
    d == 2 ? '#99d594':
    d == 1 ? '#3288bd':
    '#999999';
}

//レイヤの基本表示設定。geojsonデータの[ feature.properties.d_ppd_gr ]の値が色分けの際に参照される。
function stats_PPD_style(feature){
    return{
    fillColor: getColor_PPD(feature.properties.d_ppd_gr),
    weight: 0.7,
    opacity: 1,
    color: 'white',
    dashArray: '',
    fillOpacity: 0.6
    };
}

//マウスオーバー時のハイライト表示設定
function highlightFeature_PPD(e){
    const stats_layer = e.target;
    stats_layer.setStyle({
                         weight: 2,
                         color: 'white',
                         dashArray: '',
                         fillOpacity: 0.7
                         });
    if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge){
        stats_layer.bringToFront();
    }
}

//マウスオーバー解除時のハイライト表示リセット
function resetHighlight_PPD(e){
    Country_Stats_PPD.resetStyle(e.target);
}

//モバイルブラウザでのタップ時のハイライト設定
function infoFeature_PPD(e){
    const stats_layer = e.target;
    if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge){
        stats_layer.bringToFront();
    }
}

//各国の表示設定（ハイライトやツールチップ）のコントロール
function stats_PPD_onEachFeature(feature, layer){
    if (L.Browser.mobile){
        layer.on({
                 click: infoFeature_PPD
                 });
    }else{
        layer.on({
                 mouseover: highlightFeature_PPD,
                 mouseout: resetHighlight_PPD
                 });
    };
    layer.bindTooltip('<p class="tipstyle02">人口密度: '+feature.properties.d_ppd+' 人/平方km<br>人口: '+feature.properties.d_pop/1000+' 百万人<br>261 か国中 '+feature.properties.d_ppd_rk+' 位（グループ '+feature.properties.d_ppd_gr+')</p><hr><p class="tipstyle01">'+feature.properties.d_Name_jp+'</p>',{className: 'tipstyle', sticky: 'true', direction:'top', offset:[0,-15], opacity: 0.9});
}

//geojsonデータの読込み設定
Country_Stats_PPD = new L.geoJson(json_country, {style: stats_PPD_style, onEachFeature: stats_PPD_onEachFeature});
```

他の統計情報レイヤも同様に設定します。各レイヤのオブジェクト名は下記の要領で設定しています。
Country_Stats_GNI：国民1人あたり所得（GNI per capita）
Country_Stats_HDI：国連人間開発指数（Human Development Index）
Country_Stats_GEN：ジェンダー格差指数（Gender Inequality Index）
・・・

もっと簡素な記述方法があるかもしれませんが、各統計情報レイヤごとにツールチップに表示する単位などが微妙に違ったりするので、統計の種類の分だけ上記のコードを愚直に追加しています。


##geojsonデータの修正

geojsonデータは読み込み用に拡張子を.jsに変更し、下記のように参照用のオブジェクト名[ json_country ]を追記しています。

```javascript:data.js
const json_country={
"type": "FeatureCollection",
"name": "data",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
//各国のデータ
]
}
```
## 初期表記レイヤの設定

初期状態で表示するレイヤを設定します。

```javascript:main.js
//Add default layers
basemap_main.addTo(map); //最初に設定した”本当の”ベースマップレイヤ。下図として常に表示する。
Country_Stats_HDI.addTo(map); //初期状態で表示する統計情報レイヤ
```

今回のケースではHuman Development Index (国連人間開発指数)を初期表示に設定しています。レイヤのオブジェクト名は Country_Stats_HDI としました。
前述の人口密度レイヤを初期表示にしたい場合は"Country_Stats_HDI"の部分を"Country_Stats_PPD"に変更すればOKです。

## 凡例の設定

最後に凡例（レジェンド）を設定します。
baseMapsのほうが切り替え可能なレイヤセットで、通常はこっちにOpenStreetMapやGoogleMapなどのベースマップレイヤを設定します。今回は各種統計のレイヤをひたすら追記します。
一方、overlayMapsのほうは重ね描画用のレイヤです。今回はこちらには何も入れません。

```javascript:main.js
//Legend
const baseMaps = {
    '<i class="fas fa-users fa-fw" style="color:black"></i><i class="fa fa-caret-right fa-fw" style="color:grey"></i> 人口密度': Country_Stats_PPD,
//統計の種類分追記
};
const overlayMaps = {
    //今回はオーバーレイ用のレイヤはなし
};

//レイヤコントロールパネルの設定。モバイルブラウザでは畳んで表示する。
if (L.Browser.mobile) {
    L.control.layers(baseMaps, overlayMaps,{collapsed:true}).addTo(map)}
else{
    L.control.layers(baseMaps, overlayMaps,{collapsed:false}).addTo(map);
}
```

ベースマップのレイヤ名表示には[fontawesome](https://fontawesome.com)のアイコンを使っています。


#おわりに

以上の要領で、一画面で切り替え可能な統計色分けマップ（コロプレス図）が作成できます。色分けの方法については、閾値が統計指標によってバラバラだと直感的に分かりづらいので、国順位を6分位ごとにグループ化して色分けしました。

HTML/CSSとJavaScriptのみで構成しているので、実際のコードはブラウザ上のソース表示で見れますし、一応[Github](https://github.com/sanskruthiya/world-chroths-map/tree/master/JP)にもあげています。
私はSEやプログラマではないため、もしかしたらコードの書き方などでおかしな部分があるかもしれません。その点はアドバイスいただければ助かります。