0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【D3.js】公開データ3指標から都道府県別AI利用率推定マップを実装してみた(TopoJSON + WordPress埋め込み)

0
Posted at

ai-usage-map-eyecatch.jpg

この記事でできること

  • 公官庁の公開統計データを組み合わせた推定モデルの実装手順を理解できる
  • D3.js v7 + TopoJSONで日本のコロプレスマップを実装できる
  • WordPress(SWELLテーマ)への静的HTMLインライン埋め込みで詰まるポイントを把握できる
  • スマホ対応の下部固定パネル方式ツールチップを実装できる

実物のツールは都道府県別AI利用率マップ(AI Japan Index)で動作確認できる。

環境・前提

  • D3.js: v7.9.0
  • TopoJSON: v3.0.2(client)
  • JavaScript: ES2020+(Vanilla JS、フレームワーク不使用)
  • 動作確認ブラウザ: Chrome 122+、Firefox 123+、Safari 17+、Edge 122+
  • ホスティング: WordPress 6.4 + SWELLテーマ 2.6.x(インラインHTML埋め込み)
  • 開発OS: Windows 11

完成形

東京約45%・最低約12%と3.7倍の格差を示す、47都道府県のAI利用率推定コロプレスマップ。クリック/タップで都道府県の詳細データ(推定AI利用率・Google Trends値・県民所得・ネット利用率)が表示される。全国平均は総務省「情報通信白書 令和7年版」の公式値26.7%にキャリブレーション済み。

動作確認: https://ai-japan-index.com/ai-usage-map/


手順

Step 1: データの準備と正規化

都道府県別AI利用率の公式統計は存在しないため、以下3指標の公開データを代理変数として使用する。

使用データ(3指標):

  • Google Trends「AI」検索量(重み40%): Google Trends(2025年3月〜2026年3月)
  • 1人当たり県民所得(重み35%): 内閣府「県民経済計算 令和4年度確報」
  • インターネット利用率(重み25%): 総務省「令和6年通信利用動向調査」

「このコードの意味」: 各指標をそのまま加算すると単位・レンジが違うため比較できない。最小最大正規化で全指標を0〜1に統一してから重み付き加算する。

// 最小最大正規化
function normalize(values) {
  const min = Math.min(...values);
  const max = Math.max(...values);
  if (max === min) return values.map(() => 0.5);
  return values.map(v => (v - min) / (max - min));
}

// 重み付き複合スコア算出
function calcCompositeScore(trendsNorm, incomeNorm, netNorm) {
  return 0.40 * trendsNorm + 0.35 * incomeNorm + 0.25 * netNorm;
}

// 全国平均26.7%へのキャリブレーション(総務省公式値)
function calibrateToNationalAvg(scores, populations) {
  const totalPop = populations.reduce((a, b) => a + b, 0);
  const weightedMean = scores.reduce((sum, s, i) => sum + s * populations[i], 0) / totalPop;
  const scaleFactor = 0.267 / weightedMean;
  return scores.map(s => Math.min(s * scaleFactor, 1.0));
}

つまずきポイント: Google Trendsの都道府県別データは「地域内での相対値(最大=100)」として出力されるため、都道府県間の直接比較ができない。全期間の月次平均を使い、都道府県間での相対スケールに再変換する必要がある。


Step 2: TopoJSONのロードとD3プロジェクション設定

「このコードの意味」: geoMercator().fitSize() はSVGのサイズに合わせて日本全体が収まるようにプロジェクション(座標変換)を自動計算してくれる。手動でscale/translateを調整する方法は、SVGリサイズ時に日本が画面外に出る問題が起きやすい。

async function initMap() {
  // TopoJSONのロード
  const japanTopo = await d3.json("./japan-prefectures.topojson");

  // TopoJSON → GeoJSON変換
  const prefFeatures = topojson.feature(
    japanTopo,
    japanTopo.objects.prefectures  // オブジェクト名は境界データによって異なる
  );

  const svg = d3.select("#map-svg");
  const width = svg.node().clientWidth;
  const height = svg.node().clientHeight;

  // SVGサイズに自動フィット(手動scale/translate調整はNG)
  const projection = d3.geoMercator()
    .fitSize([width, height], prefFeatures);

  const path = d3.geoPath().projection(projection);

  // カラースケール(0〜0.45の範囲を5段階に分割)
  const colorScale = d3.scaleQuantize()
    .domain([0.08, 0.45])
    .range(["#1a1a4a", "#2a2a6a", "#3a4a8a", "#4a6aaa", "#ff8906"]);

  // 都道府県ポリゴンの描画
  svg.selectAll("path.pref")
    .data(prefFeatures.features)
    .join("path")
    .attr("class", "pref")
    .attr("d", path)
    .attr("fill", d => {
      const rate = getPrefRate(d.properties.pref_code);
      return (rate !== null) ? colorScale(rate) : "#333";
    })
    .attr("stroke", "#2a2a4a")
    .attr("stroke-width", 0.5);
}

NG例 → OK例:

// NG: 手動のscale/translate調整
const projection = d3.geoMercator()
  .scale(1200)
  .translate([width / 2, height / 2]);
// → SVGサイズが変わると日本が画面外に出る

// OK: fitSizeで自動フィット
const projection = d3.geoMercator()
  .fitSize([width, height], prefFeatures);
// → どのSVGサイズでも日本全体が収まる

Step 3: インタラクション実装(PC + スマホ対応)

「このコードの意味」: PCではホバー+クリックでツールチップ表示、スマホではタッチイベントで画面下部固定パネルを表示する。スマホではマウス追従型ツールチップが画面外にはみ出すため、固定パネル方式が必要だ。

const isMobile = () => window.innerWidth <= 767;

// PC: ホバー時ツールチップ
svg.selectAll("path.pref")
  .on("mouseover", function(event, d) {
    if (isMobile()) return;
    showTooltip(event, getPrefData(d.properties.pref_code));
  })
  .on("mouseout", () => {
    if (isMobile()) return;
    hideTooltip();
  });

// スマホ: タッチで下部固定パネル表示
// e.stopPropagation()必須(省略するとdocumentのタッチハンドラが「パネル外タップ」と誤判定)
svg.selectAll("path.pref")
  .on("touchstart", function(event) {
    if (!isMobile()) return;
    event.preventDefault();
    event.stopPropagation();  // 必須
    const prefCode = this.dataset.prefCode;
    showMobilePanel(getPrefData(prefCode));
  });

// パネル外タップで閉じる
document.addEventListener("touchstart", (e) => {
  const panel = document.getElementById("mobile-panel");
  if (!panel.contains(e.target)) {
    panel.style.display = "none";
  }
});

Step 4: WordPress(SWELLテーマ)への埋め込み

「このコードの意味」: SWELLのCSSがSVG要素のデフォルトスタイルを上書きするため、全スタイルをツール固有クラスでスコープする必要がある。またWordPressの投稿コンテンツ内の外部CDN <script src> タグは無視されるため、D3.jsの動的ロードが必要だ。

<!-- NG: グローバルセレクタはSWELLテーマのスタイルを破壊する -->
<!-- <style>svg { display: block; }</style> -->

<!-- OK: ツール固有クラスでスコープ -->
<style>
.aji-usage-map-wrap svg { display: block; width: 100%; }
.aji-usage-map-wrap * { box-sizing: border-box; }
</style>

<div class="aji-usage-map-wrap">
  <svg id="map-svg"></svg>
  <div id="mobile-panel" style="display:none; position:fixed; bottom:0; left:0; width:100%;
       background:#1a1a2e; border-top:2px solid #ff8906; z-index:10000; max-height:40vh; overflow-y:auto;">
  </div>
</div>

<script>
// D3ブートストラップ: CDN <script src> タグはWordPressが無視するため動的ロード
(function() {
  if (typeof d3 !== "undefined") {
    initMap();
    return;
  }
  const script = document.createElement("script");
  script.src = "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js";
  script.onload = initMap;
  document.head.appendChild(script);
})();
</script>

NG例 → OK例:

<!-- NG: WordPress/SWELLはコンテンツ内のscript srcを無視する -->
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>

<!-- OK: 動的ロード(D3ブートストラップ) -->
<script>
if (typeof d3 === "undefined") {
  const s = document.createElement("script");
  s.src = "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js";
  s.onload = initMap;
  document.head.appendChild(s);
} else {
  initMap();
}
</script>

Step N: 動作確認

  1. ブラウザのDevToolsでConsoleにエラーが出ていないか確認
  2. 47都道府県の色が正しく塗られているか確認(特に離島・北海道・沖縄)
  3. 都道府県クリックで詳細パネルが表示されるか確認
  4. スマホサイズ(767px以下)でタッチが正常に動作するか確認
  5. 全国平均の表示値が26.7%付近になっているか確認

つまずきポイントまとめ

  • TopoJSONのオブジェクト名が境界データによって異なる: japanTopo.objects のキー名を事前に console.log で確認する。prefecturesprefjapan など様々なケースがある
  • 都道府県コードの不一致: JIS X 0401(総務省)とTopoJSONのIDが一致しないケースがある。最初にマッピングテーブルを手動作成して確認すること
  • SVGの高さが0pxになる: SWELLのCSSリセットがSVGのデフォルト表示を上書きするため発生。.aji-usage-map-wrap svg { display: block; height: 400px; } で明示的に指定する
  • && 演算子が &#038;&#038; に変換される: WordPressのコンテンツフィルターが && をHTMLエンティティ変換しJSが構文エラーになる。三項演算子かネストifに置き換える
  • Google TrendsのAPIがない: 自動取得はサードパーティラッパーを使うしかないが利用規約上グレー。定期的な手動CSVダウンロードで対応している
  • TopoJSONの座標系とD3プロジェクションのズレ: fitSize() を使わず手動で scale()translate() を設定すると、画面サイズ変更時に地図が画面外に出る。必ず fitSize() を使うこと

まとめ

今回学んだこと:

  • 公式統計がない指標は「代理変数 + 公式値へのキャリブレーション」で推定できる
  • d3.geoMercator().fitSize() でSVGサイズへの自動フィットが確実
  • WordPress/SWELLでのD3.js実装は「CSSスコープ」「CDN動的ロード」「&&演算子回避」の3点が必須
  • スマホのインタラクションは下部固定パネル方式を最初から設計すること
  • TopoJSONは使用する境界データのオブジェクト名・都道府県コードを最初に確認する

関連ツール一覧: AI Japan Index — 日本のAI関連データを無料でインタラクティブに可視化


データ基準日: 2026年3月時点

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?