0
1

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×政府統計】AI人材需給ギャップマップを作ってみた——経産省340万人不足データをWordPressで可視化する手順

0
Posted at

ai-talent-gap.jpg

この記事でできること

  • 政府統計(経産省・厚労省・国税庁)のCSV/PDFからD3.js向けのJSONデータを整形する手順を理解する
  • WordPress(SWELLテーマ)上でD3.jsの水平バーチャートを表示する
  • WordPress投稿内のJSがよく壊れる2パターン(&&問題・<script src>無視)を回避する

実際に動作する完成形はAI人材需給ギャップマップ(AI Japan Index)で確認できる。


環境・バージョン

  • D3.js: v7.9.0(CDN: https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js
  • WordPress: 6.7系
  • SWELLテーマ: 2.10系
  • ブラウザ確認: Chrome 133 / Safari 17 / Firefox 124
  • Node.js(ローカル前処理用): v22.11.0
  • Python(PDF→テキスト変換用): 3.12

完成形

6セクションのインタラクティブチャートを1ページに収めたWordPressツールを作る。

実物: AI人材需給ギャップマップ(AI Japan Index)

チャート一覧:

  • 需給ギャップ(需要782万人 vs 供給443万人のビジュアル比較)
  • 職種別年収ランキング(水平バーチャート、7職種)
  • 求人倍率推移(折れ線チャート)
  • フリーランス市場単価(バーチャート)
  • 地域別不足状況(マップ)
  • スキル別求人数(水平バーチャート)

Step 1: データソースの確認と整形

政府統計はPDF形式で公開されることが多く、そのままJSで扱えない。まず使うデータソースを確認する。

主なデータソース(全て無料・公開):

  • 経産省「2040年の就業構造推計(改訂版)」(2026年3月、PDF): 339万人の需給ギャップ
  • 厚労省jobtag: AIエンジニア629万円、データサイエンティスト573万円(JSON/HTMLから取得可)
  • 国税庁「令和6年分 民間給与実態統計調査」(2025年9月、PDF): 日本平均年収478万円
  • doda転職求人倍率(CSV形式で公開)

つまずきポイント: PDFの数値が機械読み取りできない

経産省の推計PDFは「テキスト抽出可能型」と「画像PDF」が混在している。画像PDFの場合、pdfminer等のPythonライブラリでテキスト抽出しても空文字が返ってくる。

# OK: テキストPDFの抽出例
from pdfminer.high_level import extract_text

text = extract_text("meti_employment_2040.pdf")
# → "AI・ロボット利活用人材\n需要: 782万人..." のように取得できる

# NG(画像PDFの場合)
text = extract_text("image_based_report.pdf")
# → "" (空文字。OCRが必要)

今回の経産省PDFはテキスト抽出可能型だったため、抽出→正規表現で数値を取り出す方法が使えた。

Step 1の出力として作る data.js の構造:

// data.js(HTMLにインライン展開する)
const supplyDemandData = {
  year: 2040,
  demand: 782,  // 万人
  supply: 443,  // 万人
  gap: 339,     // 万人(不足)
  source: "経産省「2040年の就業構造推計(改訂版)」",
  sourceDate: "2026年3月",
  sourceUrl: "https://www.mext.go.jp/content/...",
  lastVerified: "2026-03-28"
};

const salaryData = [
  { role: "プロンプトエンジニア", salary: 818, source: "求人ボックス", sourceDate: "2025年", lastVerified: "2026-03-28" },
  { role: "機械学習エンジニア",   salary: 684, source: "求人ボックス", sourceDate: "2025年6月", lastVerified: "2026-03-28" },
  { role: "AIエンジニア",         salary: 629, source: "厚労省jobtag", sourceDate: "2025年9月", lastVerified: "2026-03-28" },
  { role: "データサイエンティスト", salary: 573, source: "厚労省jobtag", sourceDate: "2025年9月", lastVerified: "2026-03-28" },
  { role: "日本平均",             salary: 478, source: "国税庁「令和6年分」", sourceDate: "2025年9月", lastVerified: "2026-03-28" }
];

Step 2: WordPress環境でD3.jsを読み込む

<script src="CDN">タグを使わない——これがWordPressでのD3.js活用の最重要ルールだ。

WordPressの投稿コンテンツ内に書いた<script src="...cdn..."> はSWELLが無視する仕様になっている。D3.jsはJavaScriptで動的ロードする必要がある。

// NG: WordPressが無視するパターン
// <script src="https://cdn.jsdelivr.net/npm/d3@7/..."></script>

// OK: 動的ロードパターン
function loadD3AndInit() {
  if (typeof d3 !== 'undefined') {
    // D3が既にロード済みの場合
    initAllCharts();
    return;
  }
  const s = document.createElement('script');
  s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
  s.onload = function() {
    initAllCharts();
  };
  s.onerror = function() {
    document.getElementById('aji-talent-gap-wrap').innerHTML =
      '<p>グラフの読み込みに失敗しました。ページを再読み込みしてください。</p>';
  };
  document.head.appendChild(s);
}

document.addEventListener('DOMContentLoaded', loadD3AndInit);

Step 3: 水平バーチャートの実装

職種別年収ランキングのバーチャートを実装する。スマホ対応(Y軸ラベル非表示 + タッチで下部パネル)が必須だ。

function renderSalaryChart() {
  const wrap = document.getElementById('salary-chart');
  if (!wrap) return;

  const mobile = (window.innerWidth <= 767);
  const margin = {
    top: 24,
    right: mobile ? 10 : 90,
    bottom: 48,
    left: mobile ? 15 : 165
  };
  const W = wrap.clientWidth || 600;
  const H = salaryData.length * (mobile ? 36 : 44) + margin.top + margin.bottom;
  const innerW = W - margin.left - margin.right;
  const innerH = H - margin.top - margin.bottom;

  const svg = d3.select(wrap).append('svg')
    .attr('width', W)
    .attr('height', H);

  const g = svg.append('g')
    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

  const yScale = d3.scaleBand()
    .domain(salaryData.map(d => d.role))
    .range([0, innerH])
    .padding(0.3);

  const xScale = d3.scaleLinear()
    .domain([0, d3.max(salaryData, d => d.salary) * 1.15])
    .range([0, innerW]);

  // バー
  g.selectAll('.bar')
    .data(salaryData)
    .enter()
    .append('rect')
    .attr('y', d => yScale(d.role))
    .attr('x', 0)
    .attr('height', yScale.bandwidth())
    .attr('width', d => xScale(d.salary))
    .attr('fill', d => (d.role === '日本平均') ? '#a7a9be' : '#ff8906')
    .attr('rx', 3);

  // PC: バー右端の数値ラベル
  if (!mobile) {
    g.selectAll('.val-label')
      .data(salaryData)
      .enter()
      .append('text')
      .attr('y', d => yScale(d.role) + yScale.bandwidth() / 2 + 4)
      .attr('x', d => xScale(d.salary) + 6)
      .attr('fill', '#e0e0e0')
      .attr('font-size', '12px')
      .text(d => d.salary + '万円');
  }

  // PC: Y軸ラベル(職種名)
  if (!mobile) {
    g.append('g')
      .call(d3.axisLeft(yScale).tickSize(0))
      .select('.domain').remove();
    g.selectAll('.tick text').attr('fill', '#e0e0e0');
  }

  // X軸
  const xAxis = g.append('g')
    .attr('transform', 'translate(0,' + innerH + ')')
    .call(d3.axisBottom(xScale).tickFormat(d => d + ''));
  xAxis.select('.domain').remove();
  xAxis.selectAll('text').attr('fill', '#a7a9be');

  // スマホ: X軸ラベルを3本に間引く
  if (mobile) {
    const ticks = xAxis.selectAll('.tick');
    const n = ticks.size();
    const showIdx = [0, Math.floor((n - 1) / 2), n - 1];
    ticks.each(function(d, i) {
      if (showIdx.indexOf(i) === -1) {
        d3.select(this).style('display', 'none');
      }
    });
  }

  // スマホ: タッチで下部パネルに詳細表示
  if (mobile) {
    g.selectAll('.bar')
      .on('touchstart', function(e, d) {
        e.preventDefault();
        e.stopPropagation();
        showMobilePanel(d.role + ': ' + d.salary + '万円(' + d.source + ' ' + d.sourceDate + '');
      });
  }
}

Step 4: && 問題の回避

WordPressのコンテンツフィルターは &&&#038;&#038; に変換する。これが投稿内のJSスクリプトの中に含まれると構文エラーが発生し、スクリプト全体が停止する。

// NG: WordPressが &#038;&#038; に変換して壊れる
if (data !== null && data.length > 0) {
  renderChart(data);
}

// OK: ネストifで回避
if (data !== null) {
  if (data.length > 0) {
    renderChart(data);
  }
}

// OK: 三項演算子で回避
const hasData = (data !== null) ? (data.length > 0) : false;
if (hasData) { renderChart(data); }

Step 5: WordPressへのデプロイ

<!-- wp:html --> ブロック(タイプA)を使う。<!DOCTYPE html>で始まるフルHTML(タイプB)は、wpautopフィルターが<script>内に<p>タグを挿入してJSが破壊される。

<!-- wp:html -->
<div class="aji-talent-gap-wrap">
  <!-- CSS(スコープ付き) -->
  <style>
    .aji-talent-gap-wrap * { box-sizing: border-box; }
    .aji-talent-gap-wrap { background: #0f0e17; color: #e0e0e0; padding: 20px; }
    /* ... */
  </style>

  <!-- HTML構造 -->
  <div id="salary-chart"></div>
  <div id="mobile-panel" style="display:none; position:fixed; bottom:0; left:0; width:100%;
       background:#1a1a2e; border-top:2px solid #ff8906; padding:12px;
       z-index:10000; max-height:40vh; overflow-y:auto;"></div>

  <!-- JSON-LD -->
  <script type="application/ld+json">
  { "@context": "https://schema.org", "@type": "Dataset",
    "name": "AI人材需給ギャップマップ2026", "url": "https://ai-japan-index.com/ai-talent-gap/" }
  </script>

  <!-- JS(data.jsインライン展開 + D3ブートストラップ) -->
  <script>
    /* data.js の内容をここにインライン展開 */
    /* D3ブートストラップ + renderSalaryChart() を配置 */
  </script>
</div>
<!-- /wp:html -->

つまずきポイントまとめ

  • <script src="CDN"> がWordPressで無視される → 動的ロード(document.createElement('script'))で回避
  • && 演算子がWordPressで &#038;&#038; に変換されてJS構文エラー → ネストifまたは三項演算子で回避
  • CSSのグローバルセレクタ(body{}, *{}等)がSWELLテーマのレイアウトを破壊 → .aji-wrap でスコープを限定
  • Chart.jsのcanvasがSWELLで描画されない(5回試して全失敗)→ D3.jsのSVG描画に変更
  • scaleBandのdomain()に渡す値とy(d.xxx)が不一致でバーがずれる → domainに渡す値とアクセサを同一の値で統一
  • スマホでY軸の日本語ラベルが必ず見切れる → Y軸ラベルをif (!mobile)で囲んで非表示にし、タッチで下部パネルに詳細を出す
  • d3.eventがv7で廃止 → イベントオブジェクトはハンドラ第一引数function(event, d)で受け取る

まとめ(今回学んだこと)

  • WordPressでD3.jsを動かすには「動的ロード」「&&禁止」「<!-- wp:html -->ブロック使用」の3点が必須
  • 政府統計PDFはテキスト抽出型か画像型かを最初に確認する
  • CSSスコープは最初から.aji-wrapで設計する——後から直すのは手間がかかる
  • スマホ対応はY軸非表示 + 下部固定パネルのセットで設計する

全ツールのデータと動作はAI Japan Indexで無料公開している。

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?