地図とグラフで2025~2050年全国都道府県の将来推計人口変化を見てみた

# 完成画面
初期画面では、右下に全国の都道府県の人口指数を示すバーチャートが表示されます。ここでは、2020年の人口総数を基準に、2025年から2050年までの人口増減が表現されています。
![人口指数.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916958/31108e47-5409-4613-8360-7525fabc84f9.gif)

また、左側のマップ上の都道府県ポリゴンをクリックすると、該当する都道府県の2025年～2050年の人口総数を示す折れ線グラフが表示されます。
![人口総数.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916958/0f2da406-ff57-47fa-a440-f52cd30fcec0.gif)


# 将来推計人口データについて
今回使用したデータは、国立社会保障・人口問題研究所が発表している「将来推計人口」をもとに作成した「未来人口データ」です。2020年を基準年とし、2025年から2050年までを5年ごとに、5歳階級別の推計人口データとなっています。

https://www.mapmarketing.co.jp/data/tokei/future_population.html

今回使用した全国都道府県ポリゴンおよび未来人口推計データは、TerraMap APIからレスポンスされたものです。

https://www.mapmarketing.co.jp/terramap-api?grid=qiita&utm_source=qiita.com&utm_medium=referral&utm_campaign=qiita_80fc0e21961e46cf0bcc

# コードについて
## 地図表示
地図はOSMを使用し、地図ライブラリはLeafletを用いて、TerraMap APIから取得したGeoJSONファイルを読み込み、マップ上にポリゴンを表示しています。TerraMap APIにリクエストする部分は、area.jsonにリクエストすることで疑似的に表現しています。

```js
let map;
let geojsonData;

async function initMap() {
    map = L.map('map').setView([35.68437, 139.75247], 7);

    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 13,
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    }).addTo(map);

    try {
        const response = await fetch("area.json");
        geojsonData = await response.json();     

        L.geoJSON(geojsonData, {
            style: {
                fillColor: "#2196F3",
                fillOpacity: 0.4,
                color: "#1565C0",
                weight: 2
            },
            onEachFeature: (feature, layer) => {
                layer.on('click', () => {
                    const districtName = feature.properties.points[0][0];
                    createLineChart(districtName, feature.properties.data);
                });
            }
        }).addTo(map);    

    } catch (error) {
        console.error("GeoJSONの読み込みに失敗しました:", error);
    }
}

initMap();
```

JSONファイルの抜粋は以下になります。

```json
{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"area":{"area":78124.56292457819,"ratio_area":[78124.56292457819],"ratio":[1.0]},"data":[{"is_authorized":true,"ratio_value":["5224614"],"stat_item_id":21142,"stat_id":"029002300","value":"5224614"},{"is_authorized":true,"ratio_value":["5007066"],"stat_item_id":21143,"stat_id":"029002300","value":"5007066"},{"is_authorized":true,"ratio_value":["4791556"],"stat_item_id":21144,"stat_id":"029002300","value":"4791556"},{"is_authorized":true,"ratio_value":["4562362"],"stat_item_id":21145,"stat_id":"029002300","value":"4562362"},{"is_authorized":true,"ratio_value":["4319217"],"stat_item_id":21146,"stat_id":"029002300","value":"4319217"},{"is_authorized":true,"ratio_value":["4067642"],"stat_item_id":21147,"stat_id":"029002300","value":"4067642"},{"is_authorized":true,"ratio_value":["3820016"],"stat_item_id":21148,"stat_id":"029002300","value":"3820016"}],"point_coordinates":[143.33075,43.46036],"geocode":"01","points":[["北海道"]]},"geometry":{"type":"MultiPolygon","coordinates":[[[[140.218914647,41.419258644],
```

## チャートの表示にはChart.jsを使用します

### バーチャートの生成と表示設定
未来人口の人口指数は、2020年国勢調査の人口を基準（100）として、未来人口の増減率を示す値です。

```例：2050年総人口指数 = (2050年人口総数 / 2020年人口総数) * 100```

また、クリックしたボタンに応じて、その年の人口指数が表示されます。

```js
/**
 * 年度に応じた stat_item_id を返す関数
 */
function getStatId(year) {
    switch(year) {
        case 2025: return 21143;
        case 2030: return 21144;
        case 2035: return 21145;
        case 2040: return 21146;
        case 2045: return 21147;
        case 2050: return 21148;
        default: return 21143;
    }
}

/**
 * 各地区の人口指数のバーチャートを作成する関数
 * @param {number} year 対象の年（2025, 2030, ...）
 */
function createBarChart(year) {
    // 対象の stat_item_id を取得
    const statId = getStatId(year);

    const districtNames = [];
    const populationIndices = [];

    // 全地区のデータをループして、x軸のラベルと対象年の人口指数を計算
    geojsonData.features.forEach(feature => {
        const districtName = feature.properties.points[0][0];
        const data = feature.properties.data;
        const pop2020 = data.find(entry => entry.stat_item_id === 21142)?.value || 0;
        const popYear = data.find(entry => entry.stat_item_id === statId)?.value || 0;
        let index = 0;
        if (pop2020 > 0) {
            index = (popYear / pop2020) * 100;
        }
        districtNames.push(districtName);
        populationIndices.push(index);
    });

    const ctx = document.getElementById('barChart').getContext('2d');

    // 既にチャートがある場合は破棄
    if (window.barChart instanceof Chart) {
        window.barChart.destroy();
    }

    window.barChart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: districtNames,
            datasets: [
                {
                    label: year + "年の人口指数（増加）",
                    data: populationIndices.map(value => value >= 100 ? value - 100 : 0), 
                    backgroundColor: 'rgba(54, 162, 235, 0.5)',
                    borderColor: 'rgba(54, 162, 235, 1)',
                    borderWidth: 1
                },
                {
                    label: year + "年の人口指数（減少）",
                    data: populationIndices.map(value => value < 100 ? value - 100 : 0),
                    backgroundColor: 'rgba(255, 99, 132, 0.5)',
                    borderColor: 'rgba(255, 99, 132, 1)',
                    borderWidth: 1
                }
            ]
        },
        options: {
            responsive: true,
            scales: {
                x: {
                    stacked: true,
                    title: {
                        display: true,
                        text: '地区'
                    },
                    ticks: {
                        autoSkip: false,
                        maxRotation: 90, // 最大回転角度
                        minRotation: 90, // 最小回転角度（90なら縦書きになる）
                        font: {
                          size: 10
                        }
                    }
                },
                y: {
                    stacked: true,
                    beginAtZero: false,
                    suggestedMin: 0, // 適宜調整
                    suggestedMax: 10, // 適宜調整
                    title: {
                        display: true,
                        text: '人口指数'
                    }
                }
            }
        }
    });        
}

/**
 * ボタンから呼ばれる、チャート更新用の関数
 * @param {number} year
 */
function updateChart(year) {
    createBarChart(year);
}
```

### 折り線グラフの生成と表示設定
グラフのY軸は、全データ中の最大の人口総数に合わせて設定しています。

```js  
function createLineChart(districtName, data) {
    const ctx = document.getElementById('lineChart').getContext('2d');
    const population2020 = data.find(entry => entry.stat_item_id === 21142)?.value || 0; // (基準)2020年人口総数
    const population2025 = data.find(entry => entry.stat_item_id === 21143)?.value || 0; // 2025年人口総数
    const population2030 = data.find(entry => entry.stat_item_id === 21144)?.value || 0; // 2030年人口総数
    const population2035 = data.find(entry => entry.stat_item_id === 21145)?.value || 0; // 2035年人口総数
    const population2040 = data.find(entry => entry.stat_item_id === 21146)?.value || 0; // 2040年人口総数
    const population2045 = data.find(entry => entry.stat_item_id === 21147)?.value || 0; // 2045年人口総数
    const population2050 = data.find(entry => entry.stat_item_id === 21148)?.value || 0; // 2050年人口総数
   
    const chartData = {
        labels: ['2020年', '2025年', '2030年', '2035年', '2040年', '2045年', '2050年'],
        datasets: [{
            label: districtName,
            data: [population2020, population2025, population2030, population2035, population2040, population2045, population2050],
            backgroundColor: 'rgba(255, 99, 132, 0.2)',
            borderColor: 'rgba(255, 99, 132, 1)',
            borderWidth: 1,
            fill: false
        }]
    };

    if (window.lineChart instanceof Chart) {
        window.lineChart.destroy();
    }

    window.lineChart = new Chart(ctx, {
        type: 'line',
        data: chartData,
        options: {
            scales: {
                x: {
                    display: true,
                    title: {
                        display: true,
                        text: '年次'
                    }
                },
                y: {
                    beginAtZero: true,
                    max: unifiedMax,
                    display: true,
                    title: {
                        display: true,
                        text: '人口総数'
                    }
                }
            }
        }
    });

    // チャートを表示
    document.getElementById('lineChart').style.display = 'block';
}
```

全体のコードは以下のファイルに記載しています。
<details><summary>index.html</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <title>Leaflet Map</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="container">
    <div id="map"></div>
    <div id="charts">
      <canvas id="lineChart"></canvas>
      <hr style="border: 1px solid #ccc; margin: 0;">
      <canvas id="barChart"></canvas>
      <div id="buttons">
        <button onclick="updateChart(2025)">2025年</button>
        <button onclick="updateChart(2030)">2030年</button>
        <button onclick="updateChart(2035)">2035年</button>
        <button onclick="updateChart(2040)">2040年</button>
        <button onclick="updateChart(2045)">2045年</button>
        <button onclick="updateChart(2050)">2050年</button>
      </div>
    </div>    
  </div>  
  <script src="index.js"></script>
</body>
</html>

```

</details>

<details><summary>style.css</summary>

```css
body, html {
    margin: 0;
    padding: 0;
    height: 100%;
    overflow: hidden;
  }
  
  .container {
    display: flex;
    height: 100vh;
  }
  
  #map {
    width: 50%;
    height: 100%;
  }
  
  #charts {
    width: 50%;
    height: 45%;
    display: flex;
    flex-direction: column;
  }
  
  #lineChart {
    flex: 1;
    display: block;
  }

  #buttons {
    text-align: center;
    padding: 10px;
    background-color: #f5f5f5;
  }

  #buttons button {
    margin: 0 5px;
    padding: 5px 10px;
  }
```

</details>

<details><summary>index.js</summary>

```js
let map;
let geojsonData;
let unifiedMax = 0;

async function initMap() {
    map = L.map('map').setView([35.68437, 139.75247], 7);

    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 13,
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    }).addTo(map);

    try {
        const response = await fetch("area.json");
        geojsonData = await response.json();

        geojsonData.features.forEach(feature => {
            const data = feature.properties.data;
            const population2020 = data.find(entry => entry.stat_item_id === 21142)?.value || 0;
            const population2025 = data.find(entry => entry.stat_item_id === 21143)?.value || 0;
            const population2030 = data.find(entry => entry.stat_item_id === 21144)?.value || 0;
            const population2035 = data.find(entry => entry.stat_item_id === 21145)?.value || 0;
            const population2040 = data.find(entry => entry.stat_item_id === 21146)?.value || 0;
            const population2045 = data.find(entry => entry.stat_item_id === 21147)?.value || 0;
            const population2050 = data.find(entry => entry.stat_item_id === 21148)?.value || 0;
            const maxForFeature = Math.max(population2020, population2025, population2030, population2035, population2040, population2045, population2050);
            if (maxForFeature > unifiedMax) {
                unifiedMax = maxForFeature;
            }
        });        

        L.geoJSON(geojsonData, {
            style: {
                fillColor: "#2196F3",
                fillOpacity: 0.4,
                color: "#1565C0",
                weight: 2
            },
            onEachFeature: (feature, layer) => {
                layer.on('click', () => {
                    const districtName = feature.properties.points[0][0];
                    createLineChart(districtName, feature.properties.data);
                });
            }
        }).addTo(map);

        // ページ読み込み時にデフォルト（2025年）のバーチャートを描画
        createBarChart(2025);        

    } catch (error) {
        console.error("GeoJSONの読み込みに失敗しました:", error);
    }
}

/**
 * 年度に応じた stat_item_id を返す関数
 */
function getStatId(year) {
    switch(year) {
        case 2025: return 21143;
        case 2030: return 21144;
        case 2035: return 21145;
        case 2040: return 21146;
        case 2045: return 21147;
        case 2050: return 21148;
        default: return 21143;
    }
}

/**
 * 各地区の人口指数のバーチャートを作成する関数
 * @param {number} year 対象の年（2025, 2030, ...）
 */
function createBarChart(year) {
    // 対象の stat_item_id を取得
    const statId = getStatId(year);

    const districtNames = [];
    const populationIndices = [];

    // 全地区のデータをループして、x軸のラベルと対象年の人口指数を計算
    geojsonData.features.forEach(feature => {
        const districtName = feature.properties.points[0][0];
        const data = feature.properties.data;
        const pop2020 = data.find(entry => entry.stat_item_id === 21142)?.value || 0;
        const popYear = data.find(entry => entry.stat_item_id === statId)?.value || 0;
        let index = 0;
        if (pop2020 > 0) {
            index = (popYear / pop2020) * 100;
        }
        districtNames.push(districtName);
        populationIndices.push(index);
    });

    const ctx = document.getElementById('barChart').getContext('2d');

    // 既にチャートがある場合は破棄
    if (window.barChart instanceof Chart) {
        window.barChart.destroy();
    }

    window.barChart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: districtNames,
            datasets: [
                {
                    label: year + "年の人口指数（増加）",
                    data: populationIndices.map(value => value >= 100 ? value - 100 : 0), 
                    backgroundColor: 'rgba(54, 162, 235, 0.5)',
                    borderColor: 'rgba(54, 162, 235, 1)',
                    borderWidth: 1
                },
                {
                    label: year + "年の人口指数（減少）",
                    data: populationIndices.map(value => value < 100 ? value - 100 : 0),
                    backgroundColor: 'rgba(255, 99, 132, 0.5)',
                    borderColor: 'rgba(255, 99, 132, 1)',
                    borderWidth: 1
                }
            ]
        },
        options: {
            responsive: true,
            scales: {
                x: {
                    stacked: true,
                    title: {
                        display: true,
                        text: '地区'
                    },
                    ticks: {
                        autoSkip: false,
                        maxRotation: 90, // 最大回転角度
                        minRotation: 90, // 最小回転角度（90なら縦書きになる）
                        font: {
                          size: 10
                        }
                    }
                },
                y: {
                    stacked: true,
                    beginAtZero: false,
                    suggestedMin: 0, // 適宜調整
                    suggestedMax: 10, // 適宜調整
                    title: {
                        display: true,
                        text: '人口指数'
                    }
                }
            }
        }
    });        
}

/**
 * ボタンから呼ばれる、チャート更新用の関数
 * @param {number} year
 */
function updateChart(year) {
    createBarChart(year);
}

function createLineChart(districtName, data) {
    const ctx = document.getElementById('lineChart').getContext('2d');
    const population2020 = data.find(entry => entry.stat_item_id === 21142)?.value || 0; // (基準)2020年人口総数
    const population2025 = data.find(entry => entry.stat_item_id === 21143)?.value || 0; // 2025年人口総数
    const population2030 = data.find(entry => entry.stat_item_id === 21144)?.value || 0; // 2030年人口総数
    const population2035 = data.find(entry => entry.stat_item_id === 21145)?.value || 0; // 2035年人口総数
    const population2040 = data.find(entry => entry.stat_item_id === 21146)?.value || 0; // 2040年人口総数
    const population2045 = data.find(entry => entry.stat_item_id === 21147)?.value || 0; // 2045年人口総数
    const population2050 = data.find(entry => entry.stat_item_id === 21148)?.value || 0; // 2050年人口総数
   
    const chartData = {
        labels: ['2020年', '2025年', '2030年', '2035年', '2040年', '2045年', '2050年'],
        datasets: [{
            label: districtName,
            data: [population2020, population2025, population2030, population2035, population2040, population2045, population2050],
            backgroundColor: 'rgba(255, 99, 132, 0.2)',
            borderColor: 'rgba(255, 99, 132, 1)',
            borderWidth: 1,
            fill: false
        }]
    };

    if (window.lineChart instanceof Chart) {
        window.lineChart.destroy();
    }

    window.lineChart = new Chart(ctx, {
        type: 'line',
        data: chartData,
        options: {
            scales: {
                x: {
                    display: true,
                    title: {
                        display: true,
                        text: '年次'
                    }
                },
                y: {
                    beginAtZero: true,
                    max: unifiedMax,
                    display: true,
                    title: {
                        display: true,
                        text: '人口総数'
                    }
                }
            }
        }
    });

    // チャートを表示
    document.getElementById('lineChart').style.display = 'block';
}

initMap();

```

</details>