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 v7】日本AI市場7指標ダッシュボードをWordPressに組み込んでみた—対数スケール・動的更新・KPIアニメの実装メモ

0
Posted at

ai-impact-report.jpg

この記事でできること

  • D3.js v7をWordPress(SWELL)に<script src>なしで組み込む方法を知る
  • 米国1,091億ドル vs 日本9億ドルのような桁違いデータを対数スケールで可視化する
  • 年度切り替えボタンでD3.jsのSVGチャートを動的に更新する
  • KPIカードにrequestAnimationFrameでカウントアップアニメを実装する
  • &&演算子がWordPressで壊れる問題と回避方法を理解する

完成形はAI Impact Report(ai-japan-index.com)で動作確認できる。


環境・前提

  • D3.js: v7.9.0(動的ロード)
  • ブラウザ: Chrome 133 / Safari 18 / Firefox 135 で動作確認
  • WordPress: 6.7.x + SWELLテーマ 2.11.x
  • デプロイ形式: <!-- wp:html --> ブロック(カスタムHTML)
  • 外部ライブラリ: D3.jsのみ
  • ビルドツール: 不使用(静的HTML)

完成形

日本のAIに関する7指標(市場規模・導入率・投資額・特許・人材・スタートアップ・論文)を1画面に統合したダッシュボード。KPIカードのカウントアップ、セクションナビ(スクロールスパイ)、年度切り替えドーナツチャートが実装されている。

動作確認はこちら(AI Japan Index - AI Impact Report)


Step 1: D3.jsをWordPressに動的ロードする

Chart.jsを使っていたが、SWELLテーマのJSと競合して<canvas>が描画されない問題が5回の修正でも解決しなかった。D3.jsに変えて即解決した。

ただしD3.jsも<script src>形式ではSWELLが無視する。動的ロードが必須:

NG(SWELLで無視される):

<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>

OK(動的ロード):

function loadD3ThenInit() {
  if (typeof d3 !== "undefined") {
    initAll();
    return;
  }
  var s = document.createElement("script");
  s.src = "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js";
  s.onload = function() { initAll(); };
  s.onerror = function() {
    document.getElementById("dashboard-root").textContent =
      "グラフの読み込みに失敗しました。ページを再読み込みしてください。";
  };
  document.head.appendChild(s);
}
document.addEventListener("DOMContentLoaded", loadD3ThenInit);

typeof d3チェックで他ページでの二重ロードを防ぎ、onerrorでCDN失敗時のフォールバックを表示する。


Step 2: &&演算子をWordPressで安全に使う

WordPressは投稿コンテンツ内の&&&#038;&#038;にHTML変換する。これでJS全体がSyntaxErrorになる。

NG(WordPressが壊す):

var highlighted = data.filter(d => d.rank <= 3 && d.highlight);

OK(ネストifで&&を回避):

var highlighted = data.filter(function(d) {
  if (d.rank > 3) return false;
  return d.highlight ? true : false;
});

この問題は発見が難しい。コンソールに Unexpected token '&'Unexpected token 'amp' が出たら&&の変換を疑う。


Step 3: 対数スケールで桁違いデータを可視化する

米国1,091億ドルと日本9億ドルを同じY軸に表示すると、日本のバーが視覚的に消える。

通常スケール(NG):

米国 |████████████████████████████████████| 1091
中国 |████                                | 93
英国 |██                                  | 45
日本 |                                    | 9  ← 見えない

対数スケール(OK):

// D3.js v7の対数スケール
var yScale = d3.scaleLog()
  .domain([1, d3.max(data, function(d) { return d.amount; }) * 1.2])
  .range([height, 0]);

// 手動で見やすいtickを指定
var yAxis = d3.axisLeft(yScale)
  .tickValues([1, 10, 100, 1000])
  .tickFormat(function(d) { return d + "億ドル"; });

対数スケールのデメリット(「直感的に差が読めない」)はツールチップで補完:

// スマホは下部固定パネル、PCはツールチップ
function buildTooltipText(d) {
  var ratio = Math.round(1091 / d.amount);
  return d.name + ": " + d.amount + "億ドル(米国の約" + ratio + "分の1)";
}

Step 4: D3.jsのSVGチャートを年度切り替えで動的更新する

年度ボタンでドーナツチャートを切り替える実装。D3.jsにはchart.destroy()がないため、svg.selectAll("*").remove()でクリアしてから再描画する:

var talentData = {
  2025: { shortage: 88000,   total: 200000  },
  2030: { shortage: 124000,  total: 300000  },
  2040: { shortage: 3260000, total: 5000000 }
};

function renderTalentChart(year) {
  var svgContainer = d3.select("#talent-chart-svg");

  // 既存コンテンツを全消去(これがないと二重描画になる)
  svgContainer.selectAll("*").remove();

  var data = talentData[year];
  var pieData = [
    { label: "充足", value: data.total - data.shortage, color: "#2d9e6b" },
    { label: "不足", value: data.shortage,              color: "#ef4444" }
  ];

  var width  = 200;
  var height = 200;
  var radius = Math.min(width, height) / 2 - 8;

  var svg = svgContainer
    .attr("viewBox", "0 0 " + width + " " + height)
    .append("g")
    .attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");

  var pie = d3.pie().value(function(d) { return d.value; });
  var arc = d3.arc().innerRadius(radius * 0.5).outerRadius(radius);

  svg.selectAll(".slice")
    .data(pie(pieData))
    .enter()
    .append("path")
    .attr("d", arc)
    .attr("fill", function(d) { return d.data.color; });

  // 中央テキスト
  svg.append("text")
    .attr("text-anchor", "middle")
    .attr("dy", "0.35em")
    .attr("fill", "#ef4444")
    .attr("font-size", "13px")
    .text((data.shortage / 10000).toFixed(1) + "万人不足");
}

// 年度ボタンのイベント
document.querySelectorAll(".year-btn").forEach(function(btn) {
  btn.addEventListener("click", function() {
    document.querySelectorAll(".year-btn").forEach(function(b) {
      b.classList.remove("active");
    });
    this.classList.add("active");
    renderTalentChart(parseInt(this.dataset.year));
  });
});

selectAll("*").remove()を呼ばないと古い描画が残り、二重描画になる。


Step 5: KPIカードのカウントアップアニメーション

function animateCountUp(el, targetValue, duration) {
  duration = duration || 1200;
  var startTime = performance.now();
  var isFloat = (targetValue % 1 !== 0);

  function update(now) {
    var elapsed  = now - startTime;
    var progress = elapsed / duration;
    if (progress > 1) progress = 1;
    // easeOutCubic: 最初は速く最後はゆっくり止まる
    var eased   = 1 - Math.pow(1 - progress, 3);
    var current = targetValue * eased;

    if (isFloat) {
      el.textContent = current.toFixed(1);
    } else {
      el.textContent = Math.floor(current).toLocaleString();
    }

    if (progress < 1) requestAnimationFrame(update);
  }
  requestAnimationFrame(update);
}

// 使い方
var el = document.getElementById("kpi-market-value");
animateCountUp(el, 13412, 1200);  // 0 → 13,412 を1.2秒でカウントアップ

setIntervalの代わりにrequestAnimationFrameを使うことで、フレームレートに同期した滑らかな動きになる。


Step 6: 動作確認

// コンソールで確認
console.log("D3 version:", d3.version);  // "7.x.x" → D3が正常にロードされている

// データが正しく参照されているか
console.log("KPI data:", aiImpactData.kpiSummary.length);  // 4 → 4件

// チャートが描画されているか(SVGの子要素数確認)
console.log("Investment bars:", document.querySelectorAll("#investment-chart-svg .bar").length);  // 4

確認ポイント:

  1. KPIカードの数値が0からカウントアップするか
  2. 「年度を切り替える」ボタンでドーナツが更新されるか
  3. 投資額チャートのY軸が対数スケールか(ラベルが1, 10, 100, 1000と等間隔)
  4. スマホでバー横の数値ラベルが非表示になるか

つまずきポイントまとめ

  • D3.jsがロードされない: <script src>はSWELLで無視される。動的ロードに変更。typeof d3で確認
  • &&でSyntaxError(&#038;: WordPressの自動変換。三項演算子・ネストifで回避
  • 二重描画: svg.selectAll("*").remove()を呼ぶのを忘れた。動的更新前に必ず呼ぶ
  • 日本のバーが見えない: 通常スケールではゼロに見える。scaleLog()に変更
  • バブルが画面外にはみ出る: 半径はMath.sqrt(amount) * scaleFactorでスケーリング
  • KPIアニメがカクカク: setIntervalrequestAnimationFrame + easeOutCubicに変更

まとめ

  • D3.js v7のWordPress組み込みは動的ブートストラップ必須
  • &&演算子はWordPressで必ず壊れる。使わない
  • D3.jsの動的更新はsvg.selectAll("*").remove()でSVGをクリアしてから再描画
  • 桁違いデータにはscaleLog()を使い、ツールチップで相対比を補足する
  • KPIアニメにはrequestAnimationFrame + easeOutCubicの組み合わせが最適

完成したツール(AI Japan Index - AI Impact Report)


FAQ(手順・エラー対処視点)

Q1. Unexpected token '&' エラーが出た場合の対処法は?
WordPressが&&&#038;&#038;に変換している。コードをview-source:で確認して&#038;が含まれていたら、ソースの&&を全て三項演算子またはネストifに書き直す。スクリプト全体のエラーになるため、1箇所でも残っていると全てのチャートが動かない。

Q2. D3.jsの動的ロードが失敗した時に確認すべきことは?
ブラウザのネットワークタブでd3.min.jsのリクエストを確認する。404または403が出ている場合はCDN URLの変更を試す(unpkg.com/d3@7またはcdnjs.cloudflare.com)。コンテンツブロッカーが原因のこともある。

Q3. スマホで数値ラベルを非表示にするコードパターンは?
window.innerWidth <= 767でモバイル判定し、ラベル描画をifで囲む。d3.selectAll(".bar-label").style("display", isMobile ? "none" : "block")でCSSでの切り替えも可能だが、D3.jsでSVGを生成する際は最初から描画しない方がSVGのDOMが軽量になる。

Q4. 対数スケールで0の値が含まれるデータはどう扱いますか?
scaleLog()は0や負の値を扱えない。0が含まれる場合は0.10.5に置き換えるか、d3.scaleSymlog()(対数に近い対称スケール)を使う選択肢がある。ツールチップで「データなし」と表示する方が誠実な場合も多い。


データ出典: IDC Japan(2025年5月)、総務省 令和7年版情報通信白書(2025年7月)、Stanford AI Index 2025(2025年4月)、経産省 IT人材需給調査(2019年4月)/ 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?