📝

[Canvas 2D] progress chartを作る

2022/09/26に公開

Canvas 2Dでprogress chartを実装する機会があったのでメモです。

canvas?

2次元から3次元のグラフィックAPI。
HTMLの<canvas>タグをJavaScriptのcanvas.getContext()でアクセスして色々レンダリングするやつです。

今回は2次元の図形を描きたいのでcanvas.getContext("2d")を使っていきます。
ちなみに2次元以上の表現をするときはcanvas.getContext("webgl")です。なぜgetContext("3d")じゃないのだ🙄

HTMLを作成

<div class="graph-box">
  <canvas id="js_canvas" width="200" height="200"></canvas>
  <p class="chart-data">
    <span class="count" id="js_percent">67</span>
  </p>
</div>

JSで操作する<canvas>があればok。width, heightはJSやCSSで指定してもよいです。
chart-dataなるものはcanvasの真ん中に来る、進捗度合いを表すテキスト用のDOMです。

CSSを作成

今回のデザインでは<canvas>レイヤーの上にchart-dataを上下中央寄せしたかったので、positionを使って配置を調整した程度です。

.graph-box {
  position: relative;
  display: flex;
  justify-content: center;

  .chart-data {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
  }

  .count {
    display: block;

    &:after {
      content: "%";
    }
  }
}

font-sizeやcolorなどは良しなに。

canvas描画

本題。JSでやることはこんな感じ。

  1. canvasの準備
  2. 図形を描画
  3. アニメーションをつける

アニメーションは最後に考えればいいので、まずは静的な完成系の状態を考えます。
イメージ画像
※それぞれの図形を描画する意味で
ここでは背面・前面と名前をつけています

1) canvasの準備

変数とラジアン計算用のメソッドを用意

const canvas = document.getElementById("js_canvas");
const ctx = canvas.getContext("2d");

let posX = canvas.width / 2,
    posY = canvas.height / 2,
    percent = 67, // 最終形態 => 67%
    onePercent = 360 / 100,
    result =  onePercent * percent; // ラジアンを求める用

// ラジアン用
const radianStart = getRadian(270);
const radianEnd = getRadian(270 + result);

// ラジアンを返す関数
const getRadian = (degree) => {
  return degree * Math.PI / 180;
};

<canvas>の中心座標に図形を描きます(ここでは200 x 200の領域から、posX、posYで半分の値を持っておきました)これは後で使います。

円と円弧の描画はarc()を使うのですが、ラジアンが必要になるので
以下参考に最初にラジアン計算用の関数を用意しました。

ラジアンとは
プログラミングでは円周を弧度法(ラジアン)で表す

https://zenn.dev/vava/articles/96c9b644665670

// 一周は2πラジアン
360° = 2π
JavaScriptでは Math.PIで π が得られる
// 角度からラジアンを求める
radian = degree * Math.PI / 180

2) 図形を描画

背面の円を描く

先に背面の正円を用意します

ctx.clearRect(0, 0, canvas.width, canvas.height);

ctx.beginPath();
ctx.strokeStyle = "#eff7fb";
ctx.lineWidth = 15;
ctx.arc(
 posX,
 posY,
 90,
 0,
 2 * Math.PI,
 false
);
ctx.stroke();

描画開始前にclearRect()を使ってキャンバスを初期化しています。

void ctx.clearRect(x, y, width, height);
https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/clearRect
clearRect() メソッドは、矩形領域のピクセルを透明な黒 (rgba(0,0,0,0)) に設定します。矩形の角は (x, y) にあり、大きさは width と height で指定されます。

その後すぐにbeginPath()してパスの描画を伝えます。
デザインを見ながらstrokeStyleに線の色、lineWidthに線の幅を設定します。

ctx.arc(
 x, // 水平座標
 y, // 垂直座標
 radius, // 半径
 startAngle, // 開始角度(ラジアン)
 endAngle, // 終了角度(ラジアン)
 counterclockwise // 描画方向が時計回りか否か(bool)
);

arcメソッド内をそれぞれ指定して、
最後にstroke()を呼べば描画完了です。

前面の円弧を描く

続いて前面

ctx.beginPath();
ctx.strokeStyle = "#1689c8";
ctx.lineWidth = 15;
ctx.arc(
 posX,
 posY,
 90,
 radianStart,
 radianEnd,
 false
);
ctx.stroke();

arcの開始角度と終了角度は、3時の位置にあたるところが「0」で、
12時の位置は「270」になるそうです。

ラジアン

今回は270から描画したいので、
開始角度はgetRadian(270)
終了角度はgetRadian(270 + 角度(のちにアニメーションするので変数にしています))

アニメーションなしの図ができました

3) アニメーションをつける

requestAnimationFrame()を使ってアニメーションをつけました。
終了角度のgetRadian(270 + counts)で、resultに到達するまでの間を1度ずつ動かしたいので

let count = 0;
const arcInterval = () => {
  if (count <= result) {
    count += 1;
    percent = count / onePercent;
    
    // 真ん中の文字列
    document.getElementById("js_percent").innerHTML = percent.toFixed();

    /* この中で 2)の描画処理いろいろ〜〜〜 */
  }
 requestAnimationFrame(arcInterval);
}

で前面の図形をアニメーション。
同時に数字も表示するためjs_percentに整形した数をinnerHTMLしました。

以下参考にしました。ありがとうございます🎉

参考
JavaScript: canvas に円や円弧を描画する
円を描く方法
JavaScript | ラジアンを角度、角度をラジアンに変換する方法

[おまけ] CSSで作る

今回は、前面の図形の描画と真ん中の文字列を一緒に動かす必要があったのでCanvas APIにしていますが、progress chartをCSSだけで作るとこんな風になるかなと思います。

background-imageのconic-gradientとmask-imageで頑張るやつ

参考
CSSで円グラフや集中線が描けるconic-gradient入門

[おまけ] SVGで作る

同じものをSVG DOMで作るとこう

stroke-linecap: round かわいくてすき。

SVG

<circle>にcx(x軸の中心座標)、cy(y軸の中心座標)、r(半径)を指定します。
円周を15pxの線で縁取るのでそれを踏まえてr値を正確に指定します。

=> storoke-widthは外周の内側と外側に肉付けされてアウトラインが引かれる

CSS

stroke-dashoffsetに外周を使った計算を入れる必要があるので、あらかじめ外周を求めておきます。
sass.mathモジュールを活用するのが楽かも(なぜかCodePenでは使えなかった🤔)

@use "sass:math";
// 外周 = 直径 x Math.PI
$dashArray: 185 * math.$pi;
$chartCount: 67;

stroke-dasharray: $dashArray;
// 外周 ― (外周 × 入れたい数値) ÷ 100
stroke-dashoffset: calc($dashArray - ($dashArray * $chartCount) / 100);

canvas APIの時に開始位置の調整をしていましたが、SVGでも図形が3時の位置から開始になるのでしっかり直しておく必要があります。
とりあえず回転させるだけで良さそうなのでsvgにtransform: rotate(-90deg)を当てました。

svgイメージ画像

参考
SVGのcircleがstrokeによって見切れなくする方法
HTMLとCSSで作るインフォグラフィック(円/棒グラフ・レーダーチャート)【アニメーションつき】

Discussion