企業のAI準備度を診断するツールを作ることになった。メインのビジュアルはレーダーチャット。6軸のスコアをユーザーの回答とベンチマークで重ねて表示する。
「D3.jsでレーダーチャートを作ってWordPressに貼る」。これだけのはずだったのに、4回書き直すことになった。
この記事はその失敗の記録。完成したものはこちら。
AI導入レディネス診断
この記事でできること
- D3.jsでレーダーチャートの基本実装を学べる
- WordPress(SWELL)でD3.jsが動かない時の対処法がわかる
- スマホでレーダーチャートの軸ラベルが切れる問題を解決できる
- 実際に動作するツールはAI導入レディネス診断で確認できる
環境・前提
- ブラウザ: Chrome/Firefox/Safariの最新版
- D3.js: v7
- CMS: WordPress(SWELLテーマ)
- JavaScript: Vanilla JS(ES5スタイル)
- 外部依存: なし(D3.jsのみ動的ロード)
完成形
6軸のスコアをレーダーチャートで表示し、業種ベンチマークと2系列重ね表示する。業種を選ぶと比較対象が変わる。サブチャートとしてダンベルチャートで差分も見せる。
スマホでは画面下部固定パネルを使って詳細を表示する。
手順
Step 1: D3.jsを動的ロードで読み込む
WordPressのカスタムHTMLブロックに <script src="https://cdn..."> と書いてもD3.jsは読み込まれない。SWELLテーマがこのタグを無視する仕様だからだ。
NG例(SWELLでは読み込まれない):
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
OK例(動的ロードで確実に読み込む):
function loadD3(cb) {
if (typeof d3 !== 'undefined') { cb(); return; }
var s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/d3@7';
s.onload = cb;
document.head.appendChild(s);
}
loadD3(function() {
initChart();
});
typeof d3 !== 'undefined' のチェックは、同じページに複数のD3.jsチャートブロックがある場合の二重ロード防止に使う。最初のブロックがロードしたD3.jsを後のブロックでも使い回せる。
つまずきポイント: 「コンソールにエラーが出ない」のに動かない場合は、このパターンが原因のことが多い。<script src>が無視されているだけで、エラーは何も出ない。
Step 2: && を使わない
WordPressのフィルター(wpautop)は && をHTMLエンティティ変換する。
// NG: WordPressが && に書き換えてJSが壊れる
if (score >= 1.0 && score < 2.0) {
label = '基盤構築必要';
}
// OK: ネストifで回避
if (score >= 1.0) {
if (score < 2.0) {
label = '基盤構築必要';
}
}
つまずきポイント: ブラウザのデベロッパーツールで「ソースを見る」と && に見える(ブラウザが表示を戻すから)。でも実際に実行されているコードは && で壊れている。原因特定に2時間かかった。
Step 3: レーダーチャートの座標計算
レーダーチャートの各軸の座標は極座標から計算する。6軸なら60度ずつ割り振る。
var AXES = [
{ id: 'data', label: 'データ基盤' },
{ id: 'talent', label: '人材・スキル' },
{ id: 'culture', label: '組織文化' },
{ id: 'strategy', label: '戦略・経営層コミット' },
{ id: 'infra', label: '技術インフラ' },
{ id: 'governance', label: 'ガバナンス・倫理' }
];
var cx = width / 2; // 中心X
var cy = height / 2; // 中心Y
var radius = Math.min(cx, cy) * 0.65; // チャート半径
function getPoint(value, axisIndex) {
var angle = (axisIndex / AXES.length) * 2 * Math.PI - Math.PI / 2;
var r = (value / 5) * radius; // 1〜5を半径にマッピング
return {
x: cx + r * Math.cos(angle),
y: cy + r * Math.sin(angle)
};
}
スコア1〜5が内側〜外側に対応する。スコア5がチャートの最外周になるよう (value / 5) * radius で正規化している。
ポイント: - Math.PI / 2 を引いているのは、デフォルトだと「右(3時方向)」が0度になるため。これを引くと「上(12時方向)」が0度になり、最初の軸が上に来る。
Step 4: 多角形パスを描く
各軸の座標を計算したら、d3.line() で多角形パスを作る。
var points = AXES.map(function(axis, i) {
return getPoint(scores[axis.id], i);
});
// 終点を始点に戻して多角形を閉じる
var lineGenerator = d3.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
svg.append('path')
.datum(points.concat([points[0]])) // 最後に始点を追加して閉じる
.attr('d', lineGenerator)
.attr('fill', 'rgba(255, 137, 6, 0.2)')
.attr('stroke', '#ff8906')
.attr('stroke-width', 2);
points.concat([points[0]]) で最後の点を始点に戻して多角形を閉じている。これを忘れると最後の頂点から始点に線が引かれないので図形が開いてしまう。
Step 5: 軸ラベルをHTMLで配置する
ここが最初の書き直しポイントだった。
最初はSVGの <text> 要素でラベルを描いた。
// NG: スマホで日本語ラベルが切れる
svg.selectAll('.axis-label')
.data(AXES)
.enter()
.append('text')
.attr('x', function(d, i) { return getPoint(5.5, i).x; })
.attr('y', function(d, i) { return getPoint(5.5, i).y; })
.text(function(d) { return d.label; });
スマホで見たら「人材・スキル」が「人材・」で切れていた。SVGのテキストはCSSの word-wrap が効かないので、画面幅に関係なく一定幅でぶつ切りになる。
解決策はHTMLのdivで軸ラベルを配置すること。
// OK: チャートコンテナに position:relative を設定してHTML divを絶対配置
var container = document.getElementById('radar-container');
container.style.position = 'relative';
AXES.forEach(function(axis, i) {
var labelPos = getPoint(5.8, i); // チャート外側にオフセット
var div = document.createElement('div');
div.className = 'radar-label';
div.textContent = axis.label;
div.style.position = 'absolute';
div.style.left = labelPos.x + 'px';
div.style.top = labelPos.y + 'px';
div.style.transform = 'translate(-50%, -50%)';
container.appendChild(div);
});
CSSはこう。
.radar-label {
font-size: 11px;
color: #a7a9be;
text-align: center;
max-width: 56px;
word-break: keep-all;
overflow-wrap: break-word;
line-height: 1.4;
pointer-events: none;
}
@media (max-width: 767px) {
.radar-label {
font-size: 10px;
max-width: 44px;
}
}
word-break: keep-all は日本語の単語(ひらがな・カタカナ・漢字のまとまり)をなるべく途中で切らないようにする。overflow-wrap: break-word と組み合わせることで自然な位置で改行される。
つまずきポイント: SVGの座標とHTMLの絶対座標は基準が違う。SVGコンテナに position: relative がないと座標がずれる。チャートの親要素全てのpositionを確認する。
Step 6: スマホの下部固定パネルを実装する
スマホでチャートをタップした時に詳細を表示するパネルだ。
function showPanel(axisData) {
var panel = document.getElementById('detail-panel');
panel.innerHTML =
'<div class="panel-title">' + axisData.label + '</div>' +
'<div class="panel-score">自社スコア: ' + axisData.userScore.toFixed(1) + ' / 5.0</div>' +
'<div class="panel-bench">業種平均: ' + axisData.benchmark.toFixed(1) + '</div>';
panel.style.display = 'block';
}
// タッチイベント
element.addEventListener('touchstart', function(e) {
e.preventDefault();
e.stopPropagation(); // これを忘れるとパネルが即閉じる
showPanel(data);
});
// パネル外タップで閉じる
document.addEventListener('touchstart', function() {
var panel = document.getElementById('detail-panel');
panel.style.display = 'none';
});
つまずきポイント: e.stopPropagation() を書き忘れると、タップイベントがdocumentに伝わって即座に「パネル外タップ」と判定される。パネルが開いて0.1秒で閉じる挙動になる。
つまずきポイントまとめ
-
<script src>が効かない: SWELLはHTMLブロック内の外部scriptタグを無視する。動的ロードで対応する -
&&がJSを壊す: WordPressのwpautopが&&に変換する。ネストifか三項演算子で回避する -
スマホでラベルが切れる: SVGの
<text>は自動改行できない。HTMLのdivに切り替える -
パネルが即閉じる: touchstartの
stopPropagation()忘れ。タップイベントのバブリングを止める -
SVGとHTMLの座標がずれる: チャートコンテナに
position: relativeを設定してから絶対座標を使う
まとめ
- D3.jsのレーダーチャートはWordPressで動く(Chart.jsは競合して動かない)
-
<script src>は使わない。createElementで動的ロードする -
&&は使わない。ネストifか三項演算子で書く - 日本語ラベルはSVGに入れない。HTMLのdivに絶対座標で配置する
- スマホのタッチイベントには
stopPropagation()を必ず書く
完成したツール: AI導入レディネス診断
FAQ
Q. position: absolute で配置したラベルがSVGの下に隠れます。
SVGのz-indexはCSSのz-indexが効かない場合がある。SVGコンテナの position を relative に設定し、HTMLラベルの z-index を明示的に設定してみる。それでも隠れる場合は isolation: isolate をコンテナに追加する。
Q. スマホでチャートがはみ出します。
ラッパーに overflow-x: hidden を追加し、SVGに max-width: 100% を設定する。それでもはみ出す場合は、SVGの viewBox に余白を追加してチャートを内側に収める。
Q. SWELL以外のWordPressテーマでも同じ問題が起きますか?
&& の変換はWordPress本体のwpautopフィルターなのでテーマに依存しない。<script src> の無視はテーマによるが、多くのテーマで同じ問題が報告されている。動的ロードのパターンを使えばテーマに関わらず確実に動く。
引用・出典
- D3.js v7 公式: https://d3js.org/
- WordPress wpautop: https://developer.wordpress.org/reference/functions/wpautop/
- SWELLテーマ: https://swell-theme.com/
- AI導入レディネス診断: https://ai-japan-index.com/ai-readiness-check/
