こんにちは、 TECH PLAY Academyでメンターをしている久保です。本記事ではReactとCanvasを使用したチャートの描画方法に関して執筆します。
TECH PLAY Academyでは、企業のエンジニア向けにプログラミングの技術研修を提供しています。研修に参加した受講者のスキルを測定するWeb アプリを開発するにあたり、受講者ごとのスコアを視覚化するチャート機能が必須でした。ところが、Chart.js や Recharts、Victory といった代表的なチャートライブラリでは、本アプリ固有のデザイン要件を満たせませんでした。そこで、Canvas を採用し、ピクセル単位で描画処理を制御することでライブラリの制限を取り払い、どのようなデザイン要件でも実装できるようにしました。
本記事では、まず Canvas の基本を簡潔に整理し、その後 レーダーチャートの描画手順 をステップバイステップで紹介します。なお、記事内で取り上げるレーダーチャートはあくまでも解説用のサンプルであり、実際に運用している Web アプリの UI とは異なりますのでご留意ください。
Canvasの基本
レーダーチャートの描画手順に先立って、Cavasを使用したUI作成の基本的な流れやルールに関しておさらいします。
描画の手順は「パスを引く → スタイルの設定 → 描画」
Canvasを使用した描画はプログラムを使用して「ここからここまで線を引いて」の様に指示をすることが基本です。その際、「ここからここまで線を引いて」が定義されたものがパスです。パス自体には描画作用は無く、透明な輪郭を描くイメージです。輪郭ができたらペンの色や太さを定義し(スタイルの設定)、最後に輪郭(パス)にそって着色(描画)します。以下の手順は単純に地点Aから地点Bまでの黄色の線を描画する処理の流れです。
- パスを開始
ctx.beginPath()
- パスを線を描き始めたい地点に移動
ctx.moveTo(地点A)
- パスを線を描き終えたい地点にまで伸ばす
ctx.lineTo(地点B)
- 線の色を黄色に設定する
ctx.strokeStyle = "yellow";
- 完成したパスに沿って線を描画する
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
) に置きつつ、描画前に textAlign
と textBaseline
を軸の位置に応じて切り替えます。
- 右半分 (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 ... />
に values
、labels
、maxValue
を渡すだけで 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の既存社員向け研修の詳細はこちらから