住民基本台帳データを地図で可視化する (2) Webページへのデータ表示編
- 本間 充/マーケティングサイエンスラボ所長

- 2025年8月29日
- 読了時間: 5分
前回の「データ準備編」では、政府統計の住民基本台帳デーと国土交通省のデータをGoogleスプレッドシート上で加工し、地図に表示させるための準備が整いました。
今回は、その加工済みデータを実際にウェブページに表示させ、動的な地図を完成させます。

※上の地図には、私の実家も載っています。OpenStreeMapは、本当に素晴らしいですね。
1. ウェブアプリの作成
Google Apps Script (GAS)を使うと、スプレッドシートのデータを活用したウェブアプリを簡単に作成できます。今回は、地図の表示に人気のライブラリ「Leaflet.js」を使用します。
① HTMLファイルの作成
まず、ウェブページの見た目を定義するHTMLファイルを作成します。Apps Scriptエディタで、「ファイル」>「新規作成」>「HTML」を選択し、ファイル名をindex.htmlとして保存します。
以下のコードをindex.htmlに貼り付けます。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<title>市区町村別 人口マップ</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
html, body, #map { height: 100%; width: 100%; margin: 0; padding: 0; }
.popup-content { font-family: sans-serif; line-height: 1.5; max-width: 400px; }
.popup-content b { font-size: 1.1em; }
.popup-content hr { margin: 10px 0; border: 0; border-top: 1px solid #ccc; }
.city-data { background-color: #f7f7f7; padding: 5px; border-radius: 3px; margin-top: 8px; }
</style>
</head>
<body>
<div id="map"></div>
<div id="loader">地図データを読み込み中...</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const map = L.map('map').setView([36, 138], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
function runGas(functionName, ...args)
return new Promise((resolve, reject) => {
google.script.run
.withSuccessHandler(response => {
if (response && response.status === 'error') {
reject(new Error(response.message));
} else {
resolve(response.data);
}
})
.withFailureHandler(error => {
reject(error);
})
[functionName](...args);
});
}
/**
* 増減数をフォーマットする関数
* @param {string | number} value - フォーマットする数値
* @returns {string} - プラス記号とカンマ区切りが付与された文字列
*/
function formatChange(value) {
const num = Number(value);
if (isNaN(num) || value === null || String(value).trim() === '') {
return 'N/A';
}
const sign = num > 0 ? '+' : '';
return sign + num.toLocaleString();
}
Promise.all([
runGas('getPopulationData'),
runGas('getGeoJsonData')
]).then(([populationData, geoJsonString]) => {
const geoJson = JSON.parse(geoJsonString);
const populationMap = new Map();
const dataRows = (populationData.length > 0 && (populationData[0][0] === 'code' || populationData[0][0] === '団体コード')) ? populationData.slice(1) : populationData;
dataRows.forEach(row => {
if (!row[11]) return;
let codeKey = String(row[11]);
while (codeKey.length < 5) {
codeKey = '0' + codeKey;
}
const cityData = {
code: codeKey, year: row[1], prefecture: row[2], city: row[3],
population: row[4], population_male: row[5], population_female: row[6],
households: row[8], pop_per_household: row[9],
full_address: row[10],
parent_code: row[12],
pop_change_total: row[13],
pop_change_male: row[14],
pop_change_female: row[15]
};
populationMap.set(codeKey, cityData);
});
L.geoJson(geoJson, {
style: feature => ({ color: "#555", weight: 1, fillOpacity: 0 }),
onEachFeature: function(feature, layer) {
const props = feature.properties;
if (!props || !props.code) { return; }
const lookupCode = props.code;
const data = populationMap.get(lookupCode);
if (data) {
const totalChange = formatChange(data.pop_change_total);
const maleChange = formatChange(data.pop_change_male);\
const femaleChange = formatChange(data.pop_change_female);
let popupContent = `
<div class="popup-content">
<b>${data.full_address}</b><br>
人口: ${Number(data.population).toLocaleString()}人 (男性:${Number(data.population_male).toLocaleString()}人、女性:${Number(data.population_female).toLocaleString()}人)<br>
増減: <b>${totalChange}人</b> (男性:${maleChange}人、女性:${femaleChange}人)<br>
世帯人口: ${Number(data.pop_per_household).toLocaleString()}人 (世帯数:${Number(data.households).toLocaleString()}世帯)<br>
データ: ${data.year}年
</div>
`;
if (data.parent_code && String(data.parent_code).trim() !== '') {
const parentCodeKey = String(data.parent_code).trim();
const parentData = populationMap.get(parentCodeKey);
if (parentData) {
const parentTotalChange = formatChange(parentData.pop_change_total);
const parentMaleChange = formatChange(parentData.pop_change_male);
const parentFemaleChange = formatChange(parentData.pop_change_female);
popupContent += `
<div class="city-data">
<hr>
<b>${parentData.full_address} (市・区部全体)</b><br>
人口: ${Number(parentData.population).toLocaleString()}人 (男性:${Number(parentData.population_male).toLocaleString()}人、女性:${Number(parentData.population_female).toLocaleString()}人)<br>
増減: <b>${parentTotalChange}人</b> (男性:${parentMaleChange}人、女性:${parentFemaleChange}人)<br>
世帯人口: ${Number(parentData.pop_per_household).toLocaleString()}人 (世帯数:${Number(parentData.households).toLocaleString()}世帯)<br>
データ: ${parentData.year}年
</div>
`;
}
}
layer.bindPopup(popupContent);
} else {
if (props.pref && props.city) {
layer.bindPopup(`<b>${props.pref}${props.city}</b><br>人口データがありません`);
}
}
}
}).addTo(map);
document.getElementById('loader').style.display = 'none';
}).catch(error => {
const loader = document.getElementById('loader');
loader.style.color = 'red';
loader.innerHTML = `<b>エラー: データの読み込みに失敗しました</b><br>詳細: ${error.message || JSON.stringify(error)}`;
});
});
</script>
</body>
</html>② GASコードの変更
次に、コード.gsファイルに以下のコードを追加します。これにより、HTMLからスプレッドシートやGoogle Driveのデータにアクセスできるようになります。
// --- (前回までのコードに追記) ---
// 【!】設定してください
// Mapshaperで作成した、軽量なGeoJSONファイルのIDを入力してください
const ORIGINAL_GEOJSON_FILE_ID = '*****';
// ウェブアプリが使用する、軽量化されたGeoJSONファイルの名前
const SIMPLIFIED_GEOJSON_FILE_NAME = 'simplified_japan.geojson';
/**
* ウェブアプリのメインページを表示します
*/
function doGet() {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('市区町村別 人口マップ')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
/**
* 【ブラウザへ人口データを渡す】
*/
function getPopulationData() {
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TARGET_SHEET_NAME);
if (!sheet) throw new Error(`シート「${TARGET_SHEET_NAME}」が見つかりません。`);
const lastRow = sheet.getLastRow();
if (lastRow < 2) return { status: 'success', data: [] };
// 読み込む列数を16(P列)に変更
const data = sheet.getRange(2, 1, lastRow - 1, 16).getDisplayValues();
return { status: 'success', data: data };
} catch (e) {
return { status: 'error', message: `人口データの取得に失敗しました。詳細: ${e.message}` };
}
}
/**
* ブラウザへ地図データを渡します
*/
function getGeoJsonData() {
try {
const files = DriveApp.getFilesByName(SIMPLIFIED_GEOJSON_FILE_NAME);
if (!files.hasNext()) {
throw new Error(`軽量化されたGeoJSONファイル「${SIMPLIFIED_GEOJSON_FILE_NAME}」が見つかりません。先に手動で作成してください。`);
}
const file = files.next();
const content = file.getBlob().getDataAsString('UTF-8');
return { status: 'success', data: content };
} catch (e) {
return { status: 'error', message: `地図データの取得に失敗しました。詳細: ${e.message}` };
}
}2. ウェブアプリとして公開
コードの準備ができたら、いよいよウェブアプリとして公開します。
Apps Scriptエディタ右上の「デプロイ」>「新しいデプロイ」を選択します。
「種類」を「ウェブアプリ」に設定します。
「アクセスできるユーザー」を「全員」に変更します。
「デプロイ」ボタンをクリックし、表示されたURLにアクセスします。
おめでとうございます!これで、市区町村の境界線が地図上に表示され、クリックすると人口データが表示されるウェブアプリが完成しました。

3. コードの解説
index.htmlのポイント
Promise.all()
人口データと地図データの両方が取得できるまで待機します。これにより、両方のデータが揃った状態で地図の描画が始まります。
populationMap
スプレッドシートのデータをキーと値のペア({"01101": { ... }})に変換し、地図データと素早く紐づけられるようにしています。
L.geoJson()
Leafletの機能で、geojsonデータを地図上に描画します。
onEachFeature
各市区町村の領域が描画されるたびに実行される関数です。ここで、先ほど作成したpopulationMapを使って人口データを取得し、ポップアップに表示しています。

コード.gsのポイント
doGet()
ウェブアプリにアクセスしたときに最初に実行される関数です。index.htmlを読み込んで表示しています。
getPopulationData()
スプレッドシートから人口データを読み込み、JSON形式で返します。
getGeoJsonData()
Google Driveに保存した軽量なgeojsonデータを読み込み、JSON形式で返します。
次回は、今回作成した地図をさらに見やすくするためのカスタマイズ方法(塗り分け、カラーリングなど)を解説します。




コメント