top of page
MSL

住民基本台帳データを地図で可視化する (2) Webページへのデータ表示編

前回の「データ準備編」では、政府統計の住民基本台帳デーと国土交通省のデータをGoogleスプレッドシート上で加工し、地図に表示させるための準備が整いました。


今回は、その加工済みデータを実際にウェブページに表示させ、動的な地図を完成させます。

市区町村別人口データ(GIS)
市区町村別人口データ(GIS)

※上の地図には、私の実家も載っています。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. ウェブアプリとして公開


コードの準備ができたら、いよいよウェブアプリとして公開します。

  1. Apps Scriptエディタ右上の「デプロイ」>「新しいデプロイ」を選択します。

  2. 「種類」を「ウェブアプリ」に設定します。

  3. 「アクセスできるユーザー」を「全員」に変更します。

  4. 「デプロイ」ボタンをクリックし、表示されたURLにアクセスします。

おめでとうございます!これで、市区町村の境界線が地図上に表示され、クリックすると人口データが表示されるウェブアプリが完成しました。

デプロイの設定画面
デプロイの


3. コードの解説

index.htmlのポイント


  • Promise.all()

    • 人口データと地図データの両方が取得できるまで待機します。これにより、両方のデータが揃った状態で地図の描画が始まります。

  • populationMap

    • スプレッドシートのデータをキーと値のペア({"01101": { ... }})に変換し、地図データと素早く紐づけられるようにしています。

  • L.geoJson()

    • Leafletの機能で、geojsonデータを地図上に描画します。

  • onEachFeature

    • 各市区町村の領域が描画されるたびに実行される関数です。ここで、先ほど作成したpopulationMapを使って人口データを取得し、ポップアップに表示しています。

コード.gsの開発・編集画面
コード

コード.gsのポイント


  • doGet()

    • ウェブアプリにアクセスしたときに最初に実行される関数です。index.htmlを読み込んで表示しています。

  • getPopulationData()

    • スプレッドシートから人口データを読み込み、JSON形式で返します。

  • getGeoJsonData()

    • Google Driveに保存した軽量なgeojsonデータを読み込み、JSON形式で返します。


次回は、今回作成した地図をさらに見やすくするためのカスタマイズ方法(塗り分け、カラーリングなど)を解説します。

コメント


(C) 株式会社マーケティングサイエンスラボ

(C) MSL,2020-2025

bottom of page