1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

地図クリックで2km範囲内の将来推計人口指数をインタラクティブに可視化

Last updated at Posted at 2025-04-23

完成画面

ユーザーがクリックしたポイントを中心に、半径2km以内の町丁目ポリゴンを抽出し、2020年人口データおよび将来推計人口データを集計しました。
地図上には該当範囲のポリゴンを表示し、集計結果は表形式で示しています。
集計対象の人口データ指標は以下のとおりです。

  • 総人口
  • 19歳以下人口
  • 労働者人口(15~64歳)
  • 高齢者人口(65歳以上)
  • ミドル女性人口(40~50代の女性)

レーダーチャートでは、各指標の「人口指数」を可視化しています。
人口指数は、2020年国勢調査の人口を基準値(100)とし、将来推計人口の増減率を示す値です。
例:2050年総人口指数 = (2050年総人口 / 2020年総人口) * 100

未来人口指数0423.gif

*地図の移動操作は省略しています

データについて

今回使用した町丁目ポリゴンおよび将来推計人口データは、TerraMap APIから取得したGeoJSONレスポンスをもとにしています。

コードについて

  • 地図:OSM
  • 地図ライブラリ:Leaflet
  • データ取得:TerraMap API(GeoJSON形式)
  • レーダーチャート:Chart.js

Leafletで取得したGeoJSONを読み込み、マップ上にポリゴンを描画するとともに、集計データを表やレーダーチャートに反映しています。
*バックエンドシステムから行うTerraMap APIへのリクエストの詳細については、こちらの記事を参考してください

*本番の時はサーバープログラムにAPIキーをセットして下さい

// —— 初期設定 ——
const API_URL = 'TerraMap APIにリクエストするバックエンドのシステムのエンドポイント';  // ← 実際のエンドポイントに置き換えてください

// Leaflet 地図を作成
const map = L.map('map').setView([35.681236, 139.767125], 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  attribution: '© OpenStreetMap contributors'
}).addTo(map);

// レイヤーグループ(円・ポリゴンを管理)
const circleLayer = L.layerGroup().addTo(map);
const polygonLayer = L.geoJSON(null, {
  style: { color: 'blue', weight: 2, fillOpacity: 0.2 }
}).addTo(map);

// Chart.js 用変数
let radarChart = null;
const ctx = document.getElementById('radarChart').getContext('2d');
const labels = ['総人口', '19歳以下人口', '労働力人口', '高齢者人口', 'ミドル女性人口'];

// —— クリック時の処理 ——
map.on('click', async (e) => {
  // 1) 既存レイヤーをクリア
  circleLayer.clearLayers();
  polygonLayer.clearLayers();

  // 2) クリック地点に半径1000mの円を描画
  L.circle(e.latlng, { radius: 2000, color: 'red', weight: 1, fillOpacity: 0.1 })
    .addTo(circleLayer);

  // 3) API リクエスト
  const body = {
    layer_id: "00104",
    area_type: "circle",
    center_lat: e.latlng.lat,
    center_lng: e.latlng.lng,
    radius: [2000],
    stats: [
      { stat_id: "001012000", stat_item_id:[15776,15781,15782,15783,15784,15804,15805,15847,15848,15849,15850] },
      { stat_id: "029002300", stat_item_id:[22611,22615,22708,22709,22710,22711,23020,23021,23022,23023,22730,23042,22731,23043,22768,22769,22770,22771,23080,23081,23082,23083] }
    ],
    output: "polygon,data,point,area_ratio"
  };

  const res = await fetch(API_URL, {
    method: 'POST',
    headers: {
        'Content-Type':'application/json',
        'X-API-KEY': 'APIキー'
    },
    body: JSON.stringify(body)
  });
  const geojson = await res.json();

  // 4) ポリゴンを地図に追加
  polygonLayer.addData(geojson);

  // 5) 表とチャートを更新
  updateTableAndChart(geojson.features);
});

// —— 表とチャート更新関数 ——
function updateTableAndChart(features) {
    // 1) 全ポリゴンをまたいで stat_item_id ごとに合計を作る
    const mapVal = {};
    features.forEach(f => {
      f.properties.data.forEach(d => {
        const id = d.stat_item_id;
        const val = Number(d.value);
        mapVal[id] = (mapVal[id] || 0) + val;
      });
    });
  
    // 2) 各年・各指標の値を計算
    const sum = ids => ids.reduce((s,i)=>s + (mapVal[i]||0), 0);
    const v2020 = [
      mapVal[15776],
      sum([15781,15782,15783,15784]),
      mapVal[15804],
      mapVal[15805],
      sum([15847,15848,15849,15850])
    ];
    const v2030 = [
      mapVal[22611],
      sum([22708,22709,22710,22711]),
      mapVal[22730],
      mapVal[22731],
      sum([22768,22769,22770,22771])
    ];
    const v2050 = [
      mapVal[22615],
      sum([23020,23021,23022,23023]),
      mapVal[23042],
      mapVal[23043],
      sum([23080,23081,23082,23083])
    ];
  
    // 3) 指数を算出
    const idx2030 = v2030.map((v,i)=>(v2020[i] ? (v/v2020[i]).toFixed(2) : ''));
    const idx2050 = v2050.map((v,i)=>(v2020[i] ? (v/v2020[i]).toFixed(2) : ''));
  
    // 4) 表を再描画
    const tbody = document.getElementById('data-table-body');
    const names = ['総人口','19歳以下人口','労働力人口','高齢者人口','ミドル女性人口'];
    let html = '';
    names.forEach((nm, i) => {
      html += `
        <tr>
          <td>${nm}</td>
          <td>${v2020[i]}</td><td>1.00</td>
          <td>${v2030[i]}</td><td>${idx2030[i]}</td>
          <td>${v2050[i]}</td><td>${idx2050[i]}</td>
        </tr>`;
    });
    tbody.innerHTML = html;

  // 5) チャート更新
  if (radarChart) radarChart.destroy();
  radarChart = new Chart(ctx, {
    type: 'radar',
    data: {
      labels,
      datasets: [
        {
          label: '2050年 指数',
          data: idx2050,
          fill: true
        },
        {
          label: '2030年 指数',
          data: idx2030,
          fill: true
        }
      ]
    },
    options: {
      scales: {
        r: {
          // 目盛りの範囲を 0 〜 2 に固定
          min: 0,
          max: 2,
          // 目盛りの間隔を 0.5 に
          ticks: {
            stepSize: 0.5,
            // 表示ラベルを 2 桁小数に
            callback: value => value.toFixed(2)
          },
          beginAtZero: true
        }
      },
      plugins: {
        legend: {
          position: 'top'
        }
      }
    }
  });
}

// 初期表示のセットアップ
window.addEventListener('DOMContentLoaded', () => {
    // 1) 表に空行を入れる
    const tbody = document.getElementById('data-table-body');
    const rowNames = ['総人口','19歳以下人口','労働力人口','高齢者人口','ミドル女性人口'];
    let html = '';
    rowNames.forEach(name => {
      html += `
        <tr>
          <td>${name}</td>
          <td>—</td><td>—</td>
          <td>—</td><td>—</td>
          <td>—</td><td>—</td>
        </tr>`;
    });
    tbody.innerHTML = html;
  
    // 2) 空チャートを初期化
    radarChart = new Chart(ctx, {
      type: 'radar',
      data: {
        labels,
        datasets: []  // データなし
      },
      options: {
        scales: {
          r: {
            min: 0,
            max: 2,
            ticks: {
              stepSize: 0.5,
              callback: v => v.toFixed(2)
            }
          }
        },
        plugins: {
          legend: {
            position: 'top'
          }
        }
      }
    });
  });
  

htmlとcssは以下のファイルに記載しています。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>Leaflet + Chart.js Map App</title>
  <link
    rel="stylesheet"
    href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
  />
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div id="container">
    <!-- 上半分:地図 -->
    <div id="map"></div>

    <!-- 下左:表 -->
    <div id="table-wrap">
        <table>
            <thead>
            <tr>
                <th rowspan="2"></th>
                <th colspan="2">2020年</th>
                <th colspan="2">2030年</th>
                <th colspan="2">2050年</th>
            </tr>
            <tr>
                <th>人口</th><th>指数</th>
                <th>人口</th><th>指数</th>
                <th>人口</th><th>指数</th>
            </tr>
            </thead>
            <tbody id="data-table-body">
            </tbody>
        </table>
    </div>

    <!-- 下右:レーダーチャート -->
    <div id="chart-wrap">
      <canvas id="radarChart"></canvas>
    </div>
  </div>

  <script
    src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
  ></script>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <script src="script.js"></script>
</body>
</html>
style.css
#container {
    display: grid;
    grid-template-rows: 50vh 50vh;
    grid-template-columns: 1fr 1fr;
    height: 100vh;
    margin: 0;
}

#map {
    grid-row: 1 / 2;
    grid-column: 1 / 3;
    width: 100%;
    height: 100%;
}

#table-wrap {
    grid-row: 2 / 3;
    grid-column: 1 / 2;
    padding: 10px;
    overflow: auto;
}

#chart-wrap {
    grid-row: 2 / 3;
    grid-column: 2 / 3;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 10px;
    height: 100%;
}
  
#chart-wrap canvas {
    max-width: 90%;
    max-height: 90%;
}

table {
    width: 100%;
    border-collapse: collapse;
    text-align: center;
}
th, td {
    border: 1px solid #ccc;
    padding: 4px;
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?