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】115倍の値域差があるデータを対数スケールで可視化してみた—日本企業AI投資マップ実装手順

0
Posted at

ai-investment-ranking.jpg

この記事でできること

  • D3.js v7で対数スケール(scaleLog)を使ったバーチャートを実装できる
  • 値域差が100倍以上あるデータの可視化設計パターンを理解できる
  • WordPress(SWELL)でD3.jsを正しく動かすための3つの必須対応を把握できる
  • スマホ対応(Y軸ラベル非表示+タッチパネル)の実装パターンを習得できる

実物の動作確認は日本AI投資マップ(AI Japan Index)でできる。


環境・前提

  • D3.js: v7.9(CDN経由)
  • JavaScript: Vanilla JS(ES2020)
  • デプロイ先: WordPress 6.x(テーマ: SWELL)
  • 動作確認ブラウザ: Chrome 120+、Safari 17+、Firefox 121+
  • モバイル: iOS Safari、Android Chrome(767px以下でスマホ判定)

完成形

最小520億円(Sakana AI)から最大6兆円(ソフトバンクグループ)まで、115倍の値域差があるデータを1画面に収めた横棒グラフ。4段階のTier(規模帯)で色分けし、スマホではバータップで下部パネルに詳細を表示する。


手順

Step 1: WordPress向けD3.js読み込み(動的インジェクション)

WordPressのコンテンツ内に<script src="CDN">を書いても、SWELLテーマのフィルターが無視する。代わりにdocument.createElement('script')で動的にロードする。

// WordPress/SWELL対応のD3.js読み込みパターン
function loadD3AndInit() {
  if (typeof d3 !== 'undefined') {
    // すでにロード済みなら即実行
    initAll();
    return;
  }
  const s = document.createElement('script');
  s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
  s.onload = function() { initAll(); };
  document.head.appendChild(s);
}

document.addEventListener('DOMContentLoaded', loadD3AndInit);

つまずきポイント1: <script src="https://cdn.jsdelivr.net/npm/d3@7/..."></script> を直接書くとWordPressが無視する。必ず動的インジェクションを使う。


Step 2: データ設計——commitment_typeフィールドを必ず入れる

「AI投資額」は企業ごとに定義が異なる。この違いをデータ構造に埋め込んでおかないと、後でチャートに注記を入れるときに対処できない。

const companies = [
  {
    id: "softbank-group",
    name: "ソフトバンクグループ",
    tier: 1,
    commitment_jpy_bn: 60000, // 億円
    commitment_type: "MA",    // MA/CAPEX/REVENUE/FUNDING のいずれか
    period: "2025年〜",
    description: "OpenAI追加投資 最大400億ドル(約6兆円)",
    source: "SBG公式プレスリリース 2025/4/1",
    lastVerified: "2026-03-29"
  },
  {
    id: "hitachi",
    name: "日立製作所",
    tier: 2,
    commitment_jpy_bn: 3000,
    commitment_type: "CAPEX",
    period: "2024年〜",
    description: "生成AI関連投資。Lumadaプラットフォーム中核",
    source: "日立公式発表 2024/6",
    lastVerified: "2026-03-29"
  },
  {
    id: "ntt",
    name: "NTTグループ",
    tier: 2,
    commitment_jpy_bn: 1500,
    commitment_type: "REVENUE",
    period: "2025年度",
    description: "AI受注額見通し。tsuzumi 2を軸に拡大",
    source: "NTT IR資料 2025",
    lastVerified: "2026-03-29"
  }
  // 以下同様に17社追加
];

const commitmentTypeLabel = {
  MA:      "出資・M&A",
  CAPEX:   "設備・R&D投資",
  REVENUE: "受注額(売上)",
  FUNDING: "調達額"
};

Step 3: 対数スケールの設定

const margin = { top: 20, right: 40, bottom: 60, left: isMobile() ? 15 : 200 };
const innerWidth = totalWidth - margin.left - margin.right;
const innerHeight = totalHeight - margin.top - margin.bottom;

// 対数スケール(底は10がデフォルト)
const xScale = d3.scaleLog()
  .domain([
    100,  // 最小表示値(0は対数で表現不可)
    d3.max(companies, d => d.commitment_jpy_bn) * 1.3
  ])
  .range([0, innerWidth])
  .nice();

// Y軸(企業名のバンドスケール)
const yScale = d3.scaleBand()
  .domain(companies.map(d => d.name))
  .range([0, innerHeight])
  .padding(0.25);

NG例と対処:

// NG: 0値をscaleLogに渡す(-Infinityになり描画エラー)
const data = [{ name: "X社", value: 0 }, ...];
xScale(0); // → エラー

// OK: 0値(非開示)は別処理
const chartData = companies.filter(d => d.commitment_jpy_bn > 0);
// 非開示企業はテーブル形式で別セクションに表示

Step 4: バーとラベルの描画

// バー描画
g.selectAll('.bar')
  .data(companies)
  .enter()
  .append('rect')
  .attr('class', d => 'bar tier-' + d.tier)
  .attr('x', 0)
  .attr('y', d => yScale(d.name))
  .attr('width', d => xScale(d.commitment_jpy_bn))
  .attr('height', yScale.bandwidth())
  .attr('fill', d => tierColors[d.tier]);

// PC: バー右端に数値ラベル表示(スマホは非表示)
if (!isMobile()) {
  g.selectAll('.bar-label')
    .data(companies)
    .enter()
    .append('text')
    .attr('class', 'bar-label')
    .attr('x', d => xScale(d.commitment_jpy_bn) + 6)
    .attr('y', d => yScale(d.name) + yScale.bandwidth() / 2 + 5)
    .text(d => formatAmount(d.commitment_jpy_bn));
}

// 数値フォーマット(億円→兆円変換)
function formatAmount(bn) {
  if (bn >= 10000) return (bn / 10000).toFixed(1) + '兆円';
  if (bn >= 1000) return (bn / 1000).toFixed(1) + '千億円';
  return bn + '億円';
}

つまずきポイント2: スマホで数値ラベルをバー内に入れると他の要素と重なる。if (!isMobile())でPC専用にすること。


Step 5: X軸(対数スケールの目盛り設定)

対数スケールのデフォルト目盛りは2, 5, 10, 20, 50...と細かく出すぎる。任意の目盛り値を指定する。

const xAxis = d3.axisBottom(xScale)
  .tickValues([100, 500, 1000, 5000, 10000, 50000])
  .tickFormat(d => {
    if (d >= 10000) return (d / 10000) + '兆円';
    return (d / 100) + '千億円';
  });

g.append('g')
  .attr('class', 'x-axis')
  .attr('transform', `translate(0, ${innerHeight})`)
  .call(xAxis);

// スマホ: X軸ラベルを3本に間引く(日本語は4本以上で重なる)
if (isMobile()) {
  const allTicks = g.selectAll('.x-axis .tick');
  const tickCount = allTicks.size();
  allTicks.each(function(_, i) {
    const show = [0, Math.floor((tickCount - 1) / 2), tickCount - 1];
    if (!show.includes(i)) d3.select(this).style('display', 'none');
  });
}

Step 6: スマホ用タッチパネルの実装

スマホではY軸ラベルを非表示にする代わりに、バータップで詳細パネルを表示する。

<!-- HTML側: 下部固定パネル -->
<div id="aji-s3-panel" style="
  display: none;
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  background: #1a1a2e;
  border-top: 2px solid #ff8906;
  max-height: 40vh;
  overflow-y: auto;
  z-index: 10000;
  padding: 16px;
">
  <button id="aji-s3-panel-close">×</button>
  <div id="aji-s3-panel-content"></div>
</div>
// タッチイベント(e.stopPropagation()必須)
bars.on('touchstart', function(e, d) {
  e.preventDefault();
  e.stopPropagation(); // これがないとパネルが開いて即閉じる
  showPanel(d);
});

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

つまずきポイント3: e.stopPropagation()を省略すると、タップイベントがdocumentまで伝播し「パネル外タップ」と判定されてパネルが即座に閉じる。e.preventDefault()e.stopPropagation()はセットで書く。


Step 7: &&演算子の代替(WordPress必須対応)

WordPressのコンテンツフィルターが&&&#038;&#038;に変換し、JSが構文エラーになる。

// NG(WordPressがエンティティ変換してエラー)
if (isValid && data.length > 0) {
  render();
}

// OK(三項演算子)
if (isValid ? data.length > 0 : false) {
  render();
}

// OK(ネストif)
if (isValid) {
  if (data.length > 0) {
    render();
  }
}

つまずきポイント4: このバグはブラウザのデベロッパーツールでHTMLソースを確認すると&#038;&#038;という文字列が見えるため、発見できる。WordPressのHTMLエディタ上では&&と表示されていても、保存時に変換される。


つまずきポイントまとめ

  • <script src="CDN">が動かない: WordPressコンテンツ内では無視される。document.createElement('script')で動的ロードする
  • 対数スケールに0値を渡すとエラー: log10(0) = -Infinity。0値データは別処理(非表示または別セクション)
  • &&がエラーになる: WordPressが&#038;&#038;に変換する。三項演算子またはネストifで代替
  • スマホでパネルが即閉じる: touchstartハンドラにe.stopPropagation()がない。必ずセットで書く
  • 対数スケールの目盛りが細かすぎる: デフォルトのticks()は過剰。tickValues([])で任意指定する

まとめ

  • d3.scaleLog()で100倍超の値域差も1画面に収まる横棒グラフを実装できた
  • WordPress向けには3点(動的ロード・&&禁止・スマホパネル)の対応が必須
  • 定義が異なるデータを同一チャートに並べる場合は、commitment_type等のフィールドで「何の数字か」を明示する設計が重要
  • 対数スケールの直感性低下は、Tier分類によるカラーコーディングで補完できる

関連ツールはAI Japan Indexで公開中。D3.jsによる他の可視化実装も参考になる。


参考リンク

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?