Skip to content
On this page

canvas でグラフを描く

グラフの見た目、今までは chartjs なりのライブラリに頼っていたが、意外と自分でもできそうなので試しにやってみたメモ。
世の中にはいくつかデータを渡すだけで簡単にグラフを描画できるライブラリが存在しているが、大抵の場合オーバースペックであり、こんな機能いらないというものがたくさんある。(どんなライブラリでもそうだが)

今回は、線のグラフを 1 種類描いてみて肌感を確かめられればそれでいいなと思ったくらいの温度感で少しだけ手を動かした。

demo

作成手順

今回は2次元グラフを作成する。手順は大きく 3 つに分かれていて

  1. X 軸の描画
  2. Y 軸の描画
  3. データの描画

という手順になる。それぞれ、canvas の API を使って描画を行う。

canvas の API は、MDN に詳しく書かれているので、そちらを参照すると良い。
今回主に使ったのは以下。

  • beginPath
    • パスをリセットする
    • 上記の手順の実行前のたびに呼び出す
  • moveTo
    • beginPath でリセットしたパスから新しいサブパスの座標を移動する
  • lineTo
    • beginTo から moveTo に移動させた座標から lineTo で指定した座標まで線を引く
    • グラフを描くのもそうだし、軸を描くのにも使う
  • stroke
    • 描画する
  • arc
    • サブパスに円を描く
    • グラフを描画する際に座標に丸を描くのに使用
  • fillText
    • 文字列を描画する
    • 軸の目盛りだったり、グラフの要素の値を描画する際に使用

最低限の折れ線グラフを描こうとすると、上記を使って描画することができる。

以下に登場するコードは少し端折ってる部分もある。また、context という変数は ref で取得した canvas の context である。

X 軸

X 軸は、軸の線と目盛りを描画するだけで良い。
目盛りは、0 から要素数 -1 つまでの数値を描画するだけで良い。

すごく簡単な例で以下のようなコードになる。
手順は上記で記述したものになる。加えて目盛りを描画する際は要素分だけループを回して目盛りを打っていく。
基本的には上記の API をひたすら呼んで描画していくだけで良い。

// パスのリセット
context.beginPath();
// パスの移動
context.moveTo(groundX, height);
// 線で結ぶ
context.lineTo(width, height);
// 描画
context.stroke();
// 目盛りの描画
for (let i = 0; i < data.length; i++) {
  const x = groundX + i * pitchX * 10;
  context.beginPath();
  context.moveTo(x, height);
  context.lineTo(x, height + 5);
  context.stroke();
  context.textAlign = "center";
  context.textBaseline = "top";
  context.font = "10pt Arial";
  context.fillText(i.toString(), x, height + 10);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Y 軸

Y 軸は、X 軸と同じく軸の線と目盛りを描画するだけで良い。 X 軸のメモリは要素数だったが、Y 軸は目盛りは 0 から要素の最大値までを 100 ごとで刻むようにする。 ここらへんはライブラリとして公開する場合のことを考えると(現状は考えてないが)どんなインターフェイスにしようかとか、どういうデータを受け取るかとか、どういうデータを返すかとか、そういうのを考える面白みが出てくる箇所かなと思う。

// 描画するデータ
const data = [100, 200, 300, 400, 500];
// 要素数の取得
const yMax = Math.max.apply({}, data);

// パスのリセット
context.beginPath();
// パスの移動
context.moveTo(groundX, groundY);
// 線で結ぶ
context.lineTo(groundX, height);
// 描画
context.stroke();
// 目盛りの描画
// 0 から要素の最大値までを 100 ごとで刻むので +=100 をする
for (let i = 0; i <= yMax; i += 100) {
const y = height - i _ pitchY _ 5;
console.log(height, i, pitchY, y);
context.beginPath();
context.moveTo(groundX, y);
context.lineTo(groundX - 5, y);
context.stroke();
context.textAlign = "right";
context.textBaseline = "middle";
context.font = "10pt Arial";
context.fillText(i.toString(), groundX - 10, y);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

データ

データの描画も基本的には同じで、要素分だけループを回して描画していくだけで良い。
X 軸と Y 軸でそれぞれ取得してる方法と同様のやり方で座標を取得すると XY が求まり、それを線で結ぶことで折れ線グラフを描画することができる。
また、その座標には arc 関数を使って円を描画し fillText 関数を使って値を描画することでデータの値をわかりやすく表示することができる。なくても良いが。

context.beginPath();
context.moveTo(groundX, height - data[0] * pitchY * 5);
for (let i = 1; i < data.length; i++) {
  const x = groundX + i * pitchX * 10;
  const y = height - data[i] * pitchY * 5;
  context.lineTo(x, y);
  context.moveTo(x, y);
  context.arc(x, y, 3, 0, Math.PI * 2, false);
  context.stroke();
  context.fillText(data[i].toString(), x, y - 10);
}
1
2
3
4
5
6
7
8
9
10
11

イメージ

ここまで書くと、以下のようなグラフが表示できるようになる。なんとも質素だが最低限というのはこれくらいでいい。
今後は X/Y 軸に任意の値を指定できたり、動きをつけたり、バリエーションを増やしたり色々できることはあるが、ひとまずこれくらいで良さそうに感じる。

image.png

まとめ

考える気力があれば意外と簡単だった。
しかしどうしても命令的な書き方になってしまうので、もう少し宣言的に書ける方法や hooks を模索してみてもいいかもしれない。