この記事でできること
- 政府統計(経産省・厚労省・国税庁)の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ツールを作る。
チャート一覧:
- 需給ギャップ(需要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のコンテンツフィルターは && を && に変換する。これが投稿内のJSスクリプトの中に含まれると構文エラーが発生し、スクリプト全体が停止する。
// NG: WordPressが && に変換して壊れる
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で&&に変換されて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で無料公開している。
