studio haneya

Work1: コロプレス図について

はじめに

皆さんはどんなチャートが好きですか?
僕はコロプレス図が一番好きです

というわけで作ってみたのがこちら
/works/japan-choropleth/

この記事ではどんな風に作ったか設計面を解説したいと思います


技術スタック

描画: D3.js(d3-geo / d3-scale / d3-scale-chromatic)

地図の描画はD3.jsを使いました
使っているモジュールは3つです

地図というだけならleafletやMapLibreが定番ですが、今回はズームやパンをしないのでデザインも整えやすいD3にしています。google maps的な地図を見せる場合には地図タイルのハンドリングやズームやパン機能が必要になるのでleafletやMapLibreを選ぶと思いますが今回は動かさないですからD3.js一択だと思います

D3.jsはデータを展開していくような書き方になるのも好きですし、コード量自体はちょっと増えますがコードの構造はシンプル 、フロントエンドでの扱いやすさが優れていると思うんですよね。よく習熟難度が高いなんて言われるようですが、フロントエンドを普通に書けるならむしろ他のライブラリより習熟は早いと思います

地理データ: TopoJSON + topojson-client

今回のコロプレス図は県の範囲をsvg pathでポリゴン描画していますが、この座標情報は自前で持ってくる必要があります。これはd3-geo, leaflet, MapLibreのどれでも同じです

地図を扱う時にややこしいのが緯度経度が距離とそのまま対応しないことですが、地図系ライブラリにGeoJSONとして放り込んでやればメルカトル図法で適切に変換して使ってくれます。ただし、GeoJSONは文字列で書いていくので結構なデータ容量になってしまいますから、容量を小さくするためにTopoJSONで転送し、topojson-clientでGeoJSONに変換してd3-geoに渡す、という処理にしています

TopoJSONの仕様を読んでもらうと分かるんですがかなりの涙ぐましいデータ節約をやっていて、量子化しつつ前の点との差で書くことで文字数を減らしています。そこまでするぐらいなら普通にバイナリにした方が容量が小さくなるんじゃないかと思って計算してみたらfloat64でやる方が容量は大きくなっちゃうんですよね。計算時間としても現代のPCなら一瞬だし、JavaScriptに渡すときには文字列にしないといけないので結局TopoJSONが一番いい選択になるようです

今回データ元は定番のdataofjapan/landを利用しています。国土地理院の地理情報をデータ化した無料で利用できるリポジトリで、営利利用の場合は著作権者への利用報告義務があるようですが、非営利利用なら出典明記するだけで無料で使えます。リポジトリにTopoJSONも置いてあるのでこれをそのまま利用しました。ブラウザ側では topojson-clientfeature() 関数で TopoJSON を GeoJSON に変換し、d3-geo に渡しています。変換コストはほぼゼロで、初回レンダリングへの影響もありません。昔、国土地理院のデータをそのままGeoJSONにしたことがあるんですが、こちらのリポジトリのデータよりはるかに大きな容量になってしまったのを思い出します。測量データとしては非常に精密なものがあるのですが、コロプレス図には過剰品質なので適当にはしょったデータが必要になるわけです

UI フレームワーク: React + Astro Islands

このサイトはSSGブログがメインなのでAstroで作成していますが、コロプレス図コンポーネントはCSRでそこそこリッチな書き方になるので、Reactで実装してIslands Architectureで埋め込む形で実装しています。AstroというとSSGというイメージですが、Islandで入れればReactもVueも普通に組み込めるのが嬉しいところです

<ChoroplethMap client:visible />

このコンポーネントはD3や統計JSONデータによりバンドルサイズが大きい為に表示遅延が発生しますが、アイランドにしておくとAstroがコンポーネント部分だけハイドレーションしてくれるので、画面全体が表示遅延するのを防いでくれます。ついでにclient:visible ディレクティブにより、地図がビューポートに入るまで JavaScript のロード・ハイドレーションが遅延するようにしていますが、現状だと最初からコンポーネントが画面内にあるのであまり関係ないですが、これを付けたせいで遅くなることもないし一応付けてあります

Reactを選んだ理由はReactが好きだからですが、今回で言うとホバー時のランキングの同期やテーマ切替のような状態管理を宣言的に書けるのが選定理由として挙げられると思います


アーキテクチャ: ビルド前 ETL + 静的 JSON

データパイプラインの全体像

e-Stat API ──┐
             ├─→ Python ETL ─→ 統合 JSON ─→ Astro ビルド ─→ 静的サイト
内閣府 Excel ─┘     (etl_estat.py)    (choropleth-themes.json)

データはe-Stat APIとウェブ上にある内閣府ExcelをPython ETLで処理しています
手動で事前に取得してJSONをリポジトリに書き出しておいて、Astroビルドする際にViteでインラインバンドルしています
配信時にCDNが効いて最速で届く筈です

なぜETLしているか

ブラウザから直接e-Stat APIを叩く方式でも書けますが、以下の理由で採用しませんでした

ETLはPythonでpandasとopenpyxlを使っています
機能的にも手軽さとしても一番良いと思います

統合 JSON の構造

ETLの出力は src/data/choropleth-themes.json 1ファイル28KBにまとめています
以下のような書式です

{
  "prefecture_names": {"01": "北海道", "02": "青森", ...},
  "themes": [
    {
      "key": "gdp",
      "label": "県内総生産(GDP)",
      "colorScheme": "GnBu",
      "variants": [
        {
          "mode": "total",
          "modeLabel": "県内総額",
          "metric": "県内総生産(名目)",
          "unit": "億円",
          "valueFormat": "oku",
          "scale": "sqrt",
          "year": 2022,
          "source": "内閣府「県民経済計算」...",
          "source_url": "https://...",
          "note": "GDPの都道府県版(生産側)。",
          "values": {"01": {"v": 208892}, "02": {"v": 44391}, ...}
        },
        ...
      ]
    },
    ...
  ]
}

GDPなどのテーマ別に統計値がぶら下がる構造で、scaleはコロプレス図の色分けスケール用、source, source_urlをつけてデータ出典表示に使う為です
このJSONをフロントエンド側で TypeScript の型定義と合わせてimportして、テーマ切替やランキング表示に使っています

分母設計(total / percapita / perworker)

県内総生産や製造品出荷額のような総額は、人口が多い県が大きくなるのは当然なので、県民1人あたり、就業者1人あたりで割った値も表示できるようにしています。出典がそれぞれ少し違ったりするので論文に書くような数字にはなっていないですが、カジュアルに眺めて楽しむためのものという事で許してください

それで、やってもらうと分かると思いますが1人あたりにすると色の分布が全然違ってきます。製造業を例にとると総額では愛知県が圧倒的に濃いですが、就業者1人あたりに切り替えると大分県や山口県が上位に来るんですよね。何故かっていうと大分、山口には石油化学コンビナートや製鉄工場などの大規模設備を少人数で動かす設備があって、しかも他の産業が少ないので、県内就業者1人あたりにすると大きくなるわけです。大阪、兵庫、広島も大規模設備産業がありますが、中小製造業やサービス業も規模が大きいので、ここでやっているようなざっくり就業人口比でみると薄まってしまう

一方で、静岡、三重も人口あたりにすると色が濃くなり、東海一円に自動車などの工業集積がされていることも分かったりします

色スケールの使い分け

指標ごとに色スケールを変えています

スケール対象理由
平方根(sqrt)製造品出荷額、農業産出額、県内総生産(総額)分布の偏りが極端(最大と最小で数十〜百倍)。線形だと最大県だけが濃く、他が全部薄くなる
線形(linear)最低賃金、失業率、1 人あたり指標範囲が狭く、値の差を素直に見せられる

平方根スケールは値の大きい側を圧縮して中位県の差も見えるようにするためのものです。対数(log)スケールも選択肢ですが、0の処理で困るのと圧縮が強すぎてイマイチなので平方根にしています


データソース

統計データの取得元

8テーマのデータは、大きく3系統の公的統計から取得しています

1. e-Stat API

e-Stat は日本の政府統計ポータルで、REST API でデータを取得できます。

テーマ統計調査年次
製造業(出荷額)経済センサス - 活動調査2021
農業(産出額)生産農業所得統計2020
サービス業(売上)経済センサス - 活動調査(産業横断的集計)2021
最低賃金社会・人口統計体系2024
完全失業率社会・人口統計体系(国勢調査ベース)2020
従業者 1 人あたり指標社会・人口統計体系各年

e-Stat API は無料で使えますが、appId(API キー)の登録が必要です。ETL スクリプトでは環境変数 ESTAT_APP_ID から読み込み、生成 JSON や公開バンドルには含めていません

2. 内閣府「県民経済計算」Excel

県内総生産(GDP の都道府県版)、県民所得、雇用者報酬(給与所得)は、内閣府が公開している Excel ファイルから取得しています

テーマExcel ファイル
県内総生産soukatu1.xlsx(生産側・名目)
県民所得soukatu5.xlsx / soukatu7.xlsx(1 人あたり)
雇用者報酬soukatu6.xlsx
総人口soukatu9.xlsx
就業者数soukatu10.xlsx

これらは e-Stat API では提供されていない(または粒度が異なる)ため、直接 Excel をダウンロードして pandas で解析しています。Excel のフォーマットは年度によって微妙に異なるため、ETL スクリプトでは列位置をハードコードせず、年度の値を検索して動的に特定しています

3. 地理データ

都道府県の境界 TopoJSON は dataofjapan/land から取得しています。GitHubで公開されている日本の都道府県境界データとしては最も広く使われているもので、ライセンスも明確です

JIS コードによる統合

データソースが異なると、地域の識別コードも異なります

ETL スクリプトでは、すべてを JIS 都道府県コード(01〜47) に正規化しています。TopoJSON 側も取得時に id を JIS コードに書き換えているため、フロント側では単純なキーマッチでデータと地図を結合できます。

年次の不一致について

各テーマのデータ年次は 2020〜2024 年とばらつきがあります。これは統計調査の実施サイクルが異なるためです(経済センサスは 5 年周期、県民経済計算は毎年、国勢調査は 5 年周期)。テーマ間の年次を揃えるために古いデータに合わせるよりも、各テーマで利用可能な最新データを使う方針にしています。年次は各バリアントの year フィールドに保持し、UI 上でも出典と合わせて表示しています。


まとめ

選定判断理由
描画D3.js(d3-geo / scale / chromatic)色スケール・投影の細かい制御
地理データTopoJSON + topojson-clientサイズ圧縮、JIS コード正規化
UIReact + Astro Islands(client:visible状態管理の宣言性 + 遅延ハイドレーション
データ取得Python ETL(pandas / openpyxl)API + Excel の統合処理
データ形式統合 JSON(テーマ → バリアント → 県別値)ブラウザ側の処理を最小化
データソースe-Stat API + 内閣府 Excel + dataofjapan TopoJSON公的統計、API キー非公開

設計の軸は「ビルド前にデータの取得・加工を完結させ、ブラウザには描画だけをやらせる」ことです。公的統計のコード体系の違いや Excel フォーマットのばらつきを ETL で吸収し、フロント側には綺麗な JSON だけを渡す。この分離により、新しい指標の追加は JSON の構造に 1 テーマ分を足すだけで済みます。


お仕事募集しています
案件相談はX DM (@studio_haneya) まで


← 記事一覧に戻る