【D3.jsで日本地図描画①】Natural Earth からGeoJSONを作成するスクリプト

Natural Earth からダウンロードした Shapefile データを使用して、D3.js で表示可能な日本地図データを作成する手順を説明する。私の実行環境はNext.js v15.3.3だが、NuxtやVueでも動作する汎用的な内容になっている。

全部で2記事構成になっており、当記事ではデータの作成について解説し、次の記事ではd3.jsを使った地図の描画方法を解説する。最終的には、以下のような日本地図をWEB上で描画できるようになる。

GeoJSONとは何か

GeoJSONは地理空間データを表現するための軽量なデータ形式である。JSONをベースとしており、点、線、面といった地理的な図形と、それに関連する属性情報を一つのファイルで管理できる。例えば、都道府県の境界線データなら、各都道府県の形状(座標の集合)と名前や人口といった属性を組み合わせて記録する。WebブラウザでもNode.jsでも簡単に扱えるため、Web地図アプリケーションの開発で広く使われている。従来のShapefileと比べて、単一ファイルで完結し、テキスト形式なので人間が読みやすく、JavaScriptで直接操作できる点が大きな利点。特にLeafletやD3といったライブラリとの相性が良く、データの可視化や分析が簡単に行える。

手順 1: Natural Earth データのダウンロード

  1. Natural Earth 公式サイトから Admin 1 データをダウンロード
  2. データの配置
    • ダウンロードした ZIP ファイルを解凍
    • 以下の 4 つのファイルを public/natural-earth/ ディレクトリに配置:
      • ne_10m_admin_1_states_provinces.shp (20MB) – 地理形状データ
      • ne_10m_admin_1_states_provinces.shx (36KB) – インデックスファイル
      • ne_10m_admin_1_states_provinces.dbf (14MB) – 属性データ
      • ne_10m_admin_1_states_provinces.prj (145B) – 投影法定義

手順 2: 必要な依存関係のインストール

npm install shapefile

Shapefile から日本の都道府県データのみを抽出し、D3.js で使用可能な軽量な GeoJSON ファイルを作成するために使用する。※node.js スクリプト実行時に ES モジュール構文をそのまま使用するため、package.json に "type": "module" が記載されていることを確認する。

手順 3: データ変換

3-1. 変換コードの実装

tools/shapefile-converter.js を作成し、以下のスクリプトを実装する。

import shapefile from "shapefile";
import fs from "fs";
import path from "path";

/**
 * ShapefileをGeoJSONに変換するツール
 */

// コマンドライン引数を解析
const args = process.argv.slice(2);

if (args.length < 2) {
    process.exit(1);
}

const inputPath = args[0];
const outputPath = args[1];

// オプション解析
const options = {
    filterJapan: args.includes("--filter-japan"),
    simplify: args.includes("--simplify"),
    encoding: "utf8",
};

// エンコーディング指定
const encodingIndex = args.indexOf("--encoding");
if (encodingIndex !== -1 && encodingIndex + 1 < args.length) {
    options.encoding = args[encodingIndex + 1];
}

/**
 * 日本のデータのみをフィルタリング
 */
function filterJapanFeatures(features) {
    return features.filter((feature) => {
        const props = feature.properties;

        // Natural Earthの正しいプロパティ名で日本を判定
        if (
            props.iso_a2 === "JP" ||
            props.adm0_a3 === "JPN" ||
            props.sov_a3 === "JPN" ||
            props.admin === "Japan" ||
            props.geonunit === "Japan"
        ) {
            return true;
        }

        // 念のため、adminやgeonunitにJapanが含まれているかもチェック
        if (
            (props.admin && props.admin.includes("Japan")) ||
            (props.geonunit && props.geonunit.includes("Japan"))
        ) {
            return true;
        }

        return false;
    });
}

/**
 * 座標の簡略化(Douglas-Peucker アルゴリズムの簡易版)
 */
function simplifyCoordinates(coordinates, tolerance = 0.01) {
    if (!Array.isArray(coordinates[0])) {
        return coordinates;
    }

    if (Array.isArray(coordinates[0][0])) {
        // ポリゴンの場合
        return coordinates.map((ring) => simplifyRing(ring, tolerance));
    } else {
        // LineStringの場合
        return simplifyRing(coordinates, tolerance);
    }
}

function simplifyRing(ring) {
    if (ring.length <= 2) return ring;

    // 簡易的な間引き処理
    const simplified = [ring[0]]; // 最初の点は必ず含める

    for (let i = 1; i < ring.length - 1; i += 2) {
        // 2点おきに取得
        simplified.push(ring[i]);
    }

    simplified.push(ring[ring.length - 1]); // 最後の点も必ず含める
    return simplified;
}

/**
 * メイン変換処理
 */
async function convertShapefileToGeoJSON() {
    try {
        // Shapefileを読み込み
        const source = await shapefile.open(inputPath, undefined, {
            encoding: options.encoding,
        });

        const features = [];
        let result;

        // すべてのフィーチャーを読み込み
        while (!(result = await source.read()).done) {
            if (result.value) {
                features.push(result.value);
            }
        }

        // 日本のデータのみに絞り込み
        let filteredFeatures = features;
        if (options.filterJapan) {
            filteredFeatures = filterJapanFeatures(features);
        }

        // 座標の簡略化
        if (options.simplify) {
            filteredFeatures.forEach((feature) => {
                if (feature.geometry && feature.geometry.coordinates) {
                    feature.geometry.coordinates = simplifyCoordinates(
                        feature.geometry.coordinates
                    );
                }
            });
        }

        // GeoJSONオブジェクトを作成
        const geoJSON = {
            type: "FeatureCollection",
            name: path.basename(inputPath, ".shp"),
            crs: {
                type: "name",
                properties: {
                    name: "urn:ogc:def:crs:OGC:1.3:CRS84",
                },
            },
            features: filteredFeatures,
        };

        // 出力ディレクトリを作成
        const outputDir = path.dirname(outputPath);
        if (!fs.existsSync(outputDir)) {
            fs.mkdirSync(outputDir, { recursive: true });
        }

        // ファイルに書き込み
        fs.writeFileSync(outputPath, JSON.stringify(geoJSON, null, 2));
    } catch (error) {
        console.error("❌ エラーが発生しました:", error);
        process.exit(1);
    }
}

// メイン処理を実行
convertShapefileToGeoJSON();

3-2. 変換コマンドの実行

以下のコマンドを実行し、ne_10m_admin_1_states_provinces.shp ファイルをインプットに、GeoJSONデータを出力する。

node tools/shapefile-converter.js public/natural-earth/ne_10m_admin_1_states_provinces.shp public/natural-earth/ne_10m_admin_1_states_provinces.json --filter-japan --simplify
  • 第 1 引数: 入力 Shapefile のパス
  • 第 2 引数: 出力 GeoJSON ファイルのパス
  • --filter-japan: 全世界データから日本のデータのみを抽出
  • --simplify: 座標を間引いてファイルサイズを削減

コマンド実行後、第 2 引数で指定した場所に json が出力されれば OK。

3-3. 出力データの構造

出力されたJSONデータの構造を栃木県のデータを例に解説する。ne_10m_admin_1_states_provinces.jsonファイルが、以下のような構成で正しく出力さてるかをチェックする。

全体

{
  "type": "FeatureCollection",
  "name": "ne_10m_admin_1_states_provinces", 
  "crs": {
    "type": "name",
    "properties": {
      "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
    }
  },
  "features": [
    // 各都道府県のFeatureオブジェクトが配列で格納される
    { /* 鹿児島県 */ },
    { /* 栃木県 */ },
    { /* その他45の都道府県 */ }
  ]
}

各都道府県(Feature)の構造

{
  "type": "Feature",
  "properties": {
    // === 基本情報 ===
    "name": "Tochigi",           // 英語名(ヌル文字パディング付き)
    "name_local": "栃木県",       // 現地語名(日本語)
    "name_ja": "栃木県",         // 日本語名
    "name_en": "Tochigi Prefecture", // 英語正式名
    
    // === 識別コード ===
    "iso_3166_2": "JP-09",       // ISO 3166-2コード
    "iso_a2": "JP",              // 国コード
    "adm1_code": "JPN-1859",     // 行政区分コード
    "code_hasc": "JP.TC",        // HASCコード
    "fips": "JA38",              // FIPSコード
    
    // === 分類・タイプ ===
    "type": "Ken",               // 都道府県種別(県)
    "type_en": "Prefecture",     // 英語での種別
    "region": "Kanto",           // 地方名(関東)
    "region_cod": "JPN-KNT",     // 地方コード
    
    // === 地理的情報 ===
    "latitude": 36.6593,         // 中心緯度
    "longitude": 139.786,        // 中心経度
    "area_sqkm": 0,              // 面積(km²)
    
    // === 多言語名称 ===
    "name_de": "Präfektur Tochigi",     // ドイツ語
    "name_fr": "Tochigi",               // フランス語
    "name_ko": "도치기현",               // 韓国語
    "name_zh": "栃木县",                // 中国語(簡体字)
    "name_zht": "栃木縣",               // 中国語(繁体字)
    // その他20以上の言語での名称...
    
    // === メタデータ ===
    "scalerank": 6,              // 表示優先度ランク
    "labelrank": 6,              // ラベル表示ランク  
    "datarank": 2,               // データ品質ランク
    "min_zoom": 3,               // 最小ズームレベル
    "wikidataid": "Q44843",      // Wikidata ID
    "ne_id": 1159312195          // Natural Earth ID
  },
  "geometry": {
    "type": "Polygon",
    "coordinates": [
      [
        // 栃木県の境界線を構成する座標点の配列
        // [経度, 緯度] の形式で点が並ぶ
        [139.44051232258562, 36.268484199086515],  // 開始点
        [139.42356245284486, 36.27910370497199],   // 2番目の点
        [139.3913163597622, 36.31098806374814],    // 3番目の点
        // ... 境界線を形成する多数の座標点 ...
        [139.46826256716372, 36.27300588601281],   // 最後から2番目
        [139.44051232258562, 36.268484199086515]   // 終点(開始点と同じ)
      ]
    ]
  }
}

次のステップ

生成されたGeoJSONデータと、d3.jsを使って地図を描画する。詳しくは次の記事「【D3.jsで日本地図描画②】d3.jsを使って日本列島を描画する方法」を参照のこと。

関連記事