ライブラリでは満たせない要件を叶える!React + Canvasでカスタムレーダーチャートを描く

ノウハウ 公開日:
ブックマーク
ライブラリでは満たせない要件を叶える!React + Canvasでカスタムレーダーチャートを描く
TECH PLAY Academyが開発したアセスメントサービスでは、Canvas を採用して受講者ごとのスコアを視覚化するチャート機能を作成しました。Canvas を使うことでライブラリ依存の制約から解放され、思い通りのスタイルや描画順序を実現できる点が大きな魅力です。本記事では、Cavasを使用したUI作成の基本的な流れやルール、レーダーチャートの描画手順を解説します。

こんにちは、 TECH PLAY Academyでメンターをしている久保です。本記事ではReactとCanvasを使用したチャートの描画方法に関して執筆します。

TECH PLAY Academyでは、企業のエンジニア向けにプログラミングの技術研修を提供しています。研修に参加した受講者のスキルを測定するWeb アプリを開発するにあたり、受講者ごとのスコアを視覚化するチャート機能が必須でした。ところが、Chart.js や Recharts、Victory といった代表的なチャートライブラリでは、本アプリ固有のデザイン要件を満たせませんでした。そこで、Canvas を採用し、ピクセル単位で描画処理を制御することでライブラリの制限を取り払い、どのようなデザイン要件でも実装できるようにしました。

本記事では、まず Canvas の基本を簡潔に整理し、その後 レーダーチャートの描画手順 をステップバイステップで紹介します。なお、記事内で取り上げるレーダーチャートはあくまでも解説用のサンプルであり、実際に運用している Web アプリの UI とは異なりますのでご留意ください。

Canvasの基本

レーダーチャートの描画手順に先立って、Cavasを使用したUI作成の基本的な流れやルールに関しておさらいします。

描画の手順は「パスを引く → スタイルの設定 → 描画」

Canvasを使用した描画はプログラムを使用して「ここからここまで線を引いて」の様に指示をすることが基本です。その際、「ここからここまで線を引いて」が定義されたものがパスです。パス自体には描画作用は無く、透明な輪郭を描くイメージです。輪郭ができたらペンの色や太さを定義し(スタイルの設定)、最後に輪郭(パス)にそって着色(描画)します。以下の手順は単純に地点Aから地点Bまでの黄色の線を描画する処理の流れです。

  1. パスを開始 ctx.beginPath()
  2. パスを線を描き始めたい地点に移動ctx.moveTo(地点A)
  3. パスを線を描き終えたい地点にまで伸ばすctx.lineTo(地点B)
  4. 線の色を黄色に設定するctx.strokeStyle = "yellow";
  5. 完成したパスに沿って線を描画するctx.stroke();

コードの下に書いたものが「手前」に描画される

Canvas は 後から描いたものほど上に重なる ペインターズアルゴリズムでレンダリングされます。

// 青い四角
ctx.fillStyle = "blue";
ctx.fillRect(20, 20, 80, 80);

// 赤い円(後に描くので四角の上に来る)
ctx.beginPath();
ctx.arc(60, 60, 40, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();

この例では赤い円が青い四角の上に重なります。

z‑index 的概念は存在せず、重ね順を制御したいときは描画の順番を変えるだけで OKです。

React コンポーネントとして Canvas を扱う方法

React で Canvas を使う基本形は<canvas ref={...}>useEffect ****です。

描画は副作用なので、DOM がマウントされた後に一度だけ(もしくはstate が変わるたび)実行します。

import { useRef, useEffect } from "react";

export default function RadarChart({ labels, values, maxValue }: Props) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const cvs = canvasRef.current;
    if (!cvs) return;
    const ctx = cvs.getContext("2d");
    if (!ctx) return;

    /* ------ キャンバス初期化 ------ */
    const w = 430, h = 300;
    const dpr = window.devicePixelRatio || 1;
    cvs.style.width  = `${w}px`;
    cvs.style.height = `${h}px`;
    cvs.width  = w * dpr;
    cvs.height = h * dpr;
    ctx.scale(dpr, dpr);
    ctx.clearRect(0, 0, w, h);

    /* === ▼ 描画ロジック ========================== */
    ...
    /* ===================================================== */
  }, [labels, values, maxValue]);

  return <canvas ref={canvasRef} />;
}

上記の様にすることでCanvasをReactコンポーネントとして定義することができます。また描画のロジックは「▼ 描画ロジック」に記述していきます。次章では実際にCanvasを使用してレーダーチャートを描画する手順を説明します。


レーダーチャートの描画手順

コードの下に書いたものが手前に表示されるので、レーダーチャートを描画する際は同心円やラベルなどのベースをコードの上に記述し、その下に実際のデータを描画する処理を記述していきます。

1. 同心円ガイドを描画

まずはチャート全体のバランスを決める 中心点 を計算します。

const centerX = canvas.width  / 2; // 横幅の半分
const centerY = canvas.height / 2; // 高さの半分

次に stepsで指定した回数だけ同心円を描画します。半径を段階的に増やしながらループすることで、等間隔のガイドラインが完成します。

for (let i = 1; i <= steps; i++) {
  const currentRadius = (radius / steps) * i; // 目盛りごとの半径
  ctx.beginPath();                    // 新しいパス開始
  ctx.arc(                            // 円弧(ここでは 360°→円)
    centerX,
    centerY,
    currentRadius,
    0,
    Math.PI * 2,
    false
  );
  ctx.closePath();                    // パスを閉じる(始点と終点を結ぶ)
  ctx.strokeStyle = "#EFEFEF";        // 線色を設定
  ctx.stroke();                       // 線を描画
}

関数の役割:

関数 目的
beginPath() それ以前の図形と独立した新しいパスを開始する
arc() 円弧や円のパスを描く(中心座標, 半径, 開始角度, 終了角度)
closePath() パスを始点で結び、図形を閉じる
stroke() 現在のパスに沿って線を描画する

Tip: beginPath() を忘れると、前の図形と同じパス扱いになり、意図しない線が繋がることがあります。


2. 放射状の線(軸)を描画

表示する項目数(data.length)だけ軸を描画します。中心(centerX, centerY)からパスを書き始め、終点 (x, y) は三角関数で計算 します。

for (let i = 0; i < data.length; i++) {
  const angle = i * angleStep;           // 各軸の角度
  const x = centerX + Math.cos(angle) * radius; // x: 中心 + cosθ * 半径
  const y = centerY + Math.sin(angle) * radius; // y: 中心 + sinθ * 半径

  ctx.beginPath();
  ctx.moveTo(centerX, centerY); // パスを中心に移動 (線は引かない)
  ctx.lineTo(x, y);             // 中心→終点へ直線のパスを追加する
  ctx.stroke();                 // パスにそって線を描画する
  ctx.closePath();             
}

関数の役割:

関数 目的
moveTo() 指定座標へ パスを移動する(線は引かない)
lineTo() 現在位置から指定座標へ直線パスを引く

3. 項目のラベルを描画

表示する項目のラベル数(label.length)だけラベルを描画します。ラベルの描写開始点 (x, y) は三角関数で計算 します。

ctx.fillStyle = "#64748B";
ctx.font = "600 12px Arial";

for (let i = 0; i < labels.length; i++) {
  const angle = startAngle + i * angleStep;
  const x = centerX + Math.cos(angle) * (radius + 20);
  const y = centerY + Math.sin(angle) * (radius + 20);

  ctx.fillText(labels[i], x, y); // 文字を描画
}

上記の設定で問題なくテキストは表示されますが、以下の画像のようにテキストがズレて見えてしまいます。

これは**「指定座標に文字列の左端を合わせて描く」** というCanvasのデフォルトの設定(textAlign = "start”textBaseline = "alphabetic")に起因しており、円の左側や下側ではラベルが軸と重なるというずれが起こるのです。

そこで、ラベルは軸終端より 少し外側(radius + 20px に置きつつ、描画前に textAligntextBaseline を軸の位置に応じて切り替えます。

  • 右半分 (x > centerX) … 文字列は左側に伸ばしたい → textAlign = "left"
  • 左半分 (x < centerX) … 文字列は右側に伸ばしたい → textAlign = "right"
  • 中央 (x = centerX) … 真ん中揃え → "center"

上下方向も同様に、ラベルが円外側に向くよう textBaseline を調整します。

// x 方向の位置に応じて左・中央・右寄せを選択
ctx.textAlign = x > centerX ? "left" : x < centerX ? "right" : "center";
// y 方向の位置に応じて上・中央・下寄せを選択
ctx.textBaseline = y > centerY ? "top" : y < centerY ? "bottom" : "middle";

上記を追記することで以下のようにラベルが適切な位置に描画されるようになります。

4. データのポリゴンを描画

データ配列の値を 最大値 maxValue で正規化 し、各軸方向に頂点を配置して線で結びます。

ctx.beginPath();
for (let i = 0; i < data.length; i++) {
  const value = (data[i] / maxValue) * radius; // 値→半径にスケール
  const angle = startAngle + i * angleStep;
  const x = centerX + Math.cos(angle) * value;
  const y = centerY + Math.sin(angle) * value;

  if (i === 0) ctx.moveTo(x, y); // 最初の頂点に移動
  else         ctx.lineTo(x, y); // それ以外は線を結ぶ
}
ctx.closePath();
ctx.fillStyle = "rgba(0, 128, 255, 0.3)"; // 塗りつぶしの色を指定
ctx.fill();                   // パス内の領域を塗りつぶす(描画する)
ctx.strokeStyle = "#0078FF";              // 枠線の色を指定
ctx.stroke();                             // パスの枠線を描画する

5. データの頂点を描画

ポリゴンだけでは “どこに実際のデータ点があるか” が直感的に分かりにくいので、

各頂点に**マーカー(円)**を重ねて描画します。やることはポリゴン描画とほぼ同じで、値を半径方向にスケール → 角度で位置決定 →ctx.arc()で円を描くだけです。

// ❶ 頂点 (dot) をすべて描画する
for (let i = 0; i < data.length; i++) {
  // === 極座標 → デカルト座標 ===
  const value = (data[i] / maxValue) * radius;   // 正規化して半径へマッピング
  const angle = startAngle + i * angleStep;      // 各軸の角度
  const x = centerX + Math.cos(angle) * value;   // X 座標
  const y = centerY + Math.sin(angle) * value;   // Y 座標

  // === マーカーを描画 ===
  ctx.beginPath();
  ctx.arc(x, y, 4, 0, Math.PI * 2);              // 半径 4px の円
  ctx.fillStyle   = "blue";                      // 本体色
  ctx.fill();
  ctx.strokeStyle = "white";                     // 縁取り色
  ctx.lineWidth   = 3;                           // 縁取り太さ
  ctx.stroke();
  ctx.closePath();
}

6. RadarChartコンポーネントの呼び出し

完成したRadarChartコンポーネントは普通の React コンポーネントと同じ感覚で使えます。親側では<RadarChart ... /> に valueslabelsmaxValue を渡すだけで Canvas 上にレーダーチャートが描画されます。

export default function Page() {
  return (
    <div className="p-5">
      <div className="w-fit rounded-md p-5 shadow-md">
        <h2 className="mb-3 text-lg font-bold">
          Canvasを使用したレーダーチャート描画
        </h2>
        <RadarChart
          values={[2, 4, 4, 4, 4, 2, 4, 4]}
          labels={[
            "Appearance",
            "Diversity",
            "Price",
            "Advertisement",
            "Services",
            "Channel",
            "Quality",
            "Durability",
          ]}
          maxValue={5}
        />
      </div>
    </div>
  );
} 

これで 「作ったらあとはいつものように使うだけ」 という、再利用性の高いチャートコンポーネントになりました。


最後に

本記事では、Canvas を使用したレーダーチャートの描画方法について解説しました。実際にチャートを描画することで、Web アプリに必要な視覚的な表現を柔軟に実現することができました。Canvas を使うことで、ライブラリ依存の制約から解放され、思い通りのスタイルや描画順序を実現できる点が大きな魅力です。これにより、既存のライブラリの制限を超えたデザインが可能となり、アプリケーションに適したカスタマイズが行えることを実感しました。

React と Canvas を組み合わせたチャート作成は、思った以上に自由度が高く、さまざまなデザイン要求にも対応できる強力な手法であることが分かりました。今回紹介した内容を基に、ぜひご自身のプロジェクトにも活用していただければと思います。最後に、チャート作成の際はパフォーマンスにも注意を払い、適切な最適化を行うことをお勧めします。

今後も別の技術や実践的なチュートリアルを取り上げていきますので、引き続きチェックしてみてください。ありがとうございました。

TECH PLAY Academyの既存社員向け研修の詳細はこちらから

TECH PLAY Academyの新卒社員向け研修の詳細はこちらから

TECH PLAY Academy が運営するテクノロジー人材育成に関するイベントページです。 TECH PLAY Academyは、テクノロジー活用によって事業を進化させるために実践者である現役テクノロジー人材を講師とした、ワーク中心の実践的な人材育成プログラムを提供しています。 本コミュニティでは、“IT時代で勝ち抜ける組織“を増やしていくための、イベントや記事等の情報発信をいたします。

テクノロジーと共に成長しよう、
活躍しよう。

TECH PLAYに登録すると、
スキルアップやキャリアアップのための
情報がもっと簡単に見つけられます。

面白そうなイベントを見つけたら
積極的に参加してみましょう。
ログインはこちら

タグからイベントをさがす