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のレーダーチャートで「軸ラベルがスマホで消える」問題を解決してみた

0
Posted at

muscle-meal-planner.jpg

食費管理ツールの栄養バランスをレーダーチャートで表示しようとして、スマホでラベルが消える(というか画面外に飛ぶ)問題で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を削除して再描画する。


引用・出典

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?