食費管理ツールの栄養バランスをレーダーチャートで表示しようとして、スマホでラベルが消える(というか画面外に飛ぶ)問題で1日詰まった。
調べると同じ問題にハマる人が多いらしく、解決策を残しておくことにした。
完成したもの: AI献立+予算管理ツール
この記事でできること
- D3.jsでシンプルなレーダーチャートが描ける
- スマホでも日本語ラベルが正しく表示される実装ができる
- WordPressでも動くレーダーチャートを作れる
- 実際に動作するツールはAI献立+予算管理ツールで確認できる
環境・前提
- ブラウザ: Chrome / Firefox
- D3.js: v7(動的ロードで読み込む)
- WordPress: SWELL テーマ(記事内HTMLブロックに埋め込む)
- ビルドツール: 不要(Vanilla JS)
完成形
6軸のレーダーチャートに「タンパク質・炭水化物・脂質・食物繊維・カルシウム・ビタミンC」が表示される。
PCでも スマホでもラベルが正しく見える。
手順
Step 1: レーダーチャートの基本を描く
まずSVGに六角形のグリッドを描いて、データを重ねる。
function drawRadar(data, axes, svgEl, size) {
var n = axes.length;
var margin = { top: 40, right: 40, bottom: 40, left: 40 };
var w = size - margin.left - margin.right;
var h = size - margin.top - margin.bottom;
var cx = w / 2;
var cy = h / 2;
var r = Math.min(cx, cy) - 20;
var svg = d3.select(svgEl)
.attr('width', size)
.attr('height', size)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// グリッド線(3段)
var levels = [0.33, 0.67, 1.0];
for (var lv = 0; lv < levels.length; lv++) {
var points = [];
for (var i = 0; i < n; i++) {
var angle = (Math.PI * 2 * i / n) - Math.PI / 2;
var x = cx + r * levels[lv] * Math.cos(angle);
var y = cy + r * levels[lv] * Math.sin(angle);
points.push(x + ',' + y);
}
svg.append('polygon')
.attr('points', points.join(' '))
.attr('fill', 'none')
.attr('stroke', '#2a2a4a')
.attr('stroke-width', 1);
}
// データの多角形
var dataPoints = [];
for (var j = 0; j < n; j++) {
var ang = (Math.PI * 2 * j / n) - Math.PI / 2;
var val = data[j] / 100; // 0〜100% を 0〜1 に変換
dataPoints.push(
(cx + r * val * Math.cos(ang)) + ',' + (cy + r * val * Math.sin(ang))
);
}
svg.append('polygon')
.attr('points', dataPoints.join(' '))
.attr('fill', '#6BBFB833')
.attr('stroke', '#6BBFB8')
.attr('stroke-width', 2);
}
つまずきポイント: data[j] / 100 で100%基準に正規化している。ここを忘れると値が大きすぎてチャートが崩れる。
Step 2: SVG textで軸ラベルを描く(失敗)
最初はSVG textで描いた。
// NG: スマホで切れる
for (var k = 0; k < n; k++) {
var a = (Math.PI * 2 * k / n) - Math.PI / 2;
var lx = cx + (r + 20) * Math.cos(a);
var ly = cy + (r + 20) * Math.sin(a);
svg.append('text')
.attr('x', lx)
.attr('y', ly)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('font-size', '11px')
.attr('fill', '#a7a9be')
.text(axes[k]);
}
ローカルのブラウザ(PC)では正しく表示された。スマホでは「炭水化物」「食物繊維」の文字が画面外に出て、一部のラベルが見えなくなった。
SVGの <text> は自動改行しない。スマホ画面(375px幅)では長い日本語ラベルが収まらない。
学び: チャート周囲の日本語ラベルはSVG textでは書かない。
Step 3: HTML divに変える(解決)
軸ラベルをHTMLに変えた。
// OK: HTMLのdivでラベルを描く
function drawRadarLabels(container, axes, cx, cy, r) {
// コンテナにposition:relativeが必要
container.style.position = 'relative';
var n = axes.length;
for (var k = 0; k < n; k++) {
var angle = (Math.PI * 2 * k / n) - Math.PI / 2;
var lx = cx + (r + 30) * Math.cos(angle);
var ly = cy + (r + 30) * Math.sin(angle);
var div = document.createElement('div');
div.textContent = axes[k];
div.style.position = 'absolute';
div.style.left = (lx + 40) + 'px'; // margin.leftを加算
div.style.top = (ly + 40) + 'px'; // margin.topを加算
div.style.transform = 'translate(-50%, -50%)';
div.style.fontSize = '11px';
div.style.color = '#a7a9be';
div.style.maxWidth = '60px';
div.style.wordBreak = 'keep-all';
div.style.overflowWrap = 'break-word';
div.style.textAlign = 'center';
div.style.lineHeight = '1.3';
container.appendChild(div);
}
}
// 使い方(SVGのコンテナdivに対して呼び出す)
var wrapper = document.getElementById('radar-wrap');
drawRadarLabels(wrapper, axes, cx, cy, r);
word-break: keep-all + overflow-wrap: break-word のセットが重要だ。keep-all だけだと英数字の区切りが効かない場合があり、break-word を組み合わせると確実に折り返される。
つまずきポイント: position: absolute の座標はコンテナ(position: relative)に対する相対位置だ。SVGの margin が10pxなら、divのleft/topにもその分を足す。ずれる場合はSVGのmarginと一致しているか確認する。
Step 4: スマホでのタップ対応
スマホでは軸の頂点(データの点)をタップすると詳細を出す。
// タップ領域(不可視の円)を各頂点に置く
for (var m = 0; m < n; m++) {
var tapAngle = (Math.PI * 2 * m / n) - Math.PI / 2;
var val2 = data[m] / 100;
var tx = cx + r * val2 * Math.cos(tapAngle);
var ty = cy + r * val2 * Math.sin(tapAngle);
svg.append('circle')
.attr('cx', tx)
.attr('cy', ty)
.attr('r', 16)
.attr('fill', 'transparent')
.style('pointer-events', 'all')
.on('touchstart', (function(idx) {
return function(e) {
e.preventDefault();
e.stopPropagation(); // 忘れるとパネルが即閉じになる
showMobilePanel(axes[idx], data[idx]);
};
})(m));
}
(function(idx) { ... })(m) のクロージャが重要だ。ループ変数 m はループ後に最終値になるため、クロージャで各イテレーションの値を閉じ込める。ES5でも使えるパターン。
Step 5: && を使わないようにする
WordPress環境ではフィルターボタンの条件分岐で && を使わない。
// NG
if (selectedNutrient !== null && selectedNutrient !== undefined) {
showDetail(selectedNutrient);
}
// OK
if (selectedNutrient !== null) {
if (selectedNutrient !== undefined) {
showDetail(selectedNutrient);
}
}
コードが長くなるが、WordPressで確実に動く。
つまずきポイントまとめ
-
SVG textで日本語ラベルが切れる: HTMLのdivに変える。
word-break: keep-all+overflow-wrap: break-wordのセット - divの座標がずれる: SVGのmarginをleft/topに加算する
-
ループ内のタッチイベントが全部最後の値になる:
(function(idx){ })(m)のクロージャで閉じ込める -
タップしたら即閉じになる:
e.stopPropagation()を必ず書く -
&&がWordPressで壊れる: ネストifで代替する
まとめ
- レーダーチャートの軸ラベルはHTMLのdivで描く。SVG textは使わない
-
position: absoluteのleft/topはSVGのmarginを考慮する - ループ内のタッチハンドラはクロージャでインデックスを閉じ込める
-
stopPropagation()はタッチイベントに必ずセットで書く -
&&はWordPressが壊すので使わない
完成したツール: AI献立+予算管理ツール
FAQ
Q. レーダーチャートのライブラリはありますか?Chart.jsでも作れますか?
Chart.jsのRadarChartを使う方法もあるが、WordPressのSWELLテーマとChart.jsは競合することがある。D3.jsはSVGを直接操作するため衝突しにくい。ゼロから書く量は増えるが、制御しやすい。
Q. クロージャが難しくてよくわからないのですが。
var で宣言した変数はループが終わると最終値になってしまう、という問題を回避するための書き方だ。let が使える環境なら for (let m = 0; m < n; m++) { ... } と書くだけで同じ問題が起きない。WordPressのフィルター問題を気にしなくていい環境なら let を使う方が読みやすい。
Q. スマホ幅に合わせてSVGサイズを変えたい場合は?
window.innerWidth でサイズを判定して drawRadar(data, axes, svgEl, mobileSize) と呼び出す。ウィンドウリサイズ時は既存のSVGを削除して再描画する。
引用・出典
- D3.js v7 公式: https://d3js.org/
- WordPress wpautop: https://developer.wordpress.org/reference/functions/wpautop/
- AI献立+予算管理ツール: https://ai-japan-index.com/ai-meal-planner/
