完成画面
ユーザーがクリックしたポイントを中心に、半径2km以内の町丁目ポリゴンを抽出し、2020年人口データおよび将来推計人口データを集計しました。
地図上には該当範囲のポリゴンを表示し、集計結果は表形式で示しています。
集計対象の人口データ指標は以下のとおりです。
- 総人口
- 19歳以下人口
- 労働者人口(15~64歳)
- 高齢者人口(65歳以上)
- ミドル女性人口(40~50代の女性)
レーダーチャートでは、各指標の「人口指数」を可視化しています。
人口指数は、2020年国勢調査の人口を基準値(100)とし、将来推計人口の増減率を示す値です。
例:2050年総人口指数 = (2050年総人口 / 2020年総人口) * 100
*地図の移動操作は省略しています
データについて
今回使用した町丁目ポリゴンおよび将来推計人口データは、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;
}