本文へジャンプ

Canvasで時系列のデータを円グラフ風に描画する [JavaScript] [canvas]

Posted by kenta sugiyama

JavaScriptには多くのライブラリがあり、日々の業務でもお世話になっています。
表題にもある円グラフをはじめグラフ表示となるとChart.jsを個人的にはよく使っているのですが、それだと表示要件をクリアできないものに出くわしました(Chart.jsでも拡張すれば再現できるのかもしれませんが、、)。

要件的には

  • 24時間のデータを円グラフ風に表示する
  • それぞれのデータの分割線は表示しない
  • 各時間のデータの量は一緒だが、分類によって色分けする
  • 24分割の区切りに時間ラベルを表示
  • 過去の時間のデータ、ラベルはグレーアウトさせる

といったものになります。
Chart.jsの拡張でもできそうな感じですが、拡張するにあたり調査する時間とラベル部分はどちらにしても自分で書くことになりそうなので作ってしまった方が早いかな、、と。(笑)
久しぶりにCanvasでお絵描きしました。24時間のアナログ時計の文字盤を作る容量ですね。

HTML/CSS

描画はJavaScriptでしますのでHTML/CSSはcanvasタグの設置とサイズなどのstyleを書くだけですね。

<canvas id="js-graph" class="graph"></canvas>
.graph {
  width: 204px;
  height: 204px;
}

JavaScript 初期設定まで

JavaScriptで要素取得とCanvasサイズなどを設定します。
等倍だとジャギってしまいますので2倍で設定します。
過去時間をグレーにするのでDateの準備もしておきます。

const date = new Date();
const hour = date.getHours();
const graph = document.getElementById('js-graph');
const ctx = graph.getContext('2d');
const gWidth = graph.clientWidth * 2;
graph.width = gWidth;
graph.height = gWidth;
const gCenter = gWidth / 2;
const gRadius = gCenter * 0.77;

const data = [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 5, 1, 2, 3, 5, 4, 5, 5, 5, 5, 5];

const LEVEL_1 = '#0157a8';
const LEVEL_2 = '#3a8dcc';
const LEVEL_3 = '#4c9c05';
const LEVEL_4 = '#e78200';
const LEVEL_5 = '#e7616c';

function drawPi(ctx, w, c, r, d) {
  // ここに描画内容を記述していく
  // 時計の部分描画
  // データ描画
}
drawPi(ctx, gWidth, gCenter, gRadius, data);

JavaScript 文字盤の描画

Math.PI = 180°なので円形を描画するのでMath.PI * 2。
それを24分割するので1時間あたりの角度はMath.PI / 12になります。
ただ、このまま描画させると3時(6時)の位置から始まってしまうのでi-6としています。
c * 0.84などの半径はデザインによると思いますので、適宜調整します。

デザイン上、ラベル部分の短線とデータ部分の間に隙間を入れます。
描画部分をクリア、矩形であればclearRectすればいいですが、今回は円形にクリアする必要があります。
その場合は合成方法(globalCompositeOperation)を変更して円形を描画してクリアするようにします。
destination-outとすることで、現在イメージの領域のみが描画され重なった部分は描画されないようになります。

他の合成方法などはCanvasリファレンスをご確認ください。

  // 時計の部分描画
  ctx.save();
  for (let i=0;i<24;i++) {
    const angleRad = (Math.PI / 12 )* (i - 6);
    const x = (c * 0.84) * Math.cos(angleRad) + c;
    const y = (c * 0.84) * Math.sin(angleRad) + c;
    const txtX = (c * 0.92) * Math.cos(angleRad) + c;
    const txtY = (c * 0.92) * Math.sin(angleRad) + c;
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(c, c);
    ctx.closePath();

    ctx.restore();
    ctx.textAlign = 'center';
    if (i < hour) {
      ctx.strokeStyle = '#bbb';
      ctx.fillStyle = '#bbb';
    } else {
      ctx.strokeStyle = '#000';
      ctx.fillStyle = '#000';
    }
    ctx.font = '18px din-condensed';
    ctx.stroke();
    ctx.fillText(String(i)+'時', txtX, txtY + 4);
  }

  // 中心部分をクリアする
  ctx.globalCompositeOperation = 'destination-out';
  ctx.beginPath();
  ctx.arc(c, c, (c * 0.80), 0, Math.PI*2, false);
  ctx.fill();

JavaScript データ描画

程の文字盤部分でlineを引いていたのが、扇形を描画することになった以外は基本的には先程の内容と変わりません。
データごとにfillする色が変わりますので、swich文で切り替えています。
データごとに隙間が出てしまいますので、stroke(境界線)を描画することで、これを防ぎます。

  // データ描画
  ctx.globalCompositeOperation = 'source-over';
  ctx.save();
  for (let i=0;i<24;i++) {
    ctx.beginPath();
    ctx.arc(c, c, r, (Math.PI/12)* (i - 5), (Math.PI/12)*(i - 6), true);
    ctx.lineTo(c,c);
    ctx.closePath();
    ctx.restore();
    if(d[i]) {
      switch (d[i]) {
        case 1:
          ctx.strokeStyle = LEVEL_1;
          ctx.fillStyle = LEVEL_1;
          break;
        case 2:
          ctx.strokeStyle = LEVEL_2;
          ctx.fillStyle = LEVEL_2;
          break;
        case 3:
          ctx.strokeStyle = LEVEL_3;
          ctx.fillStyle = LEVEL_3;
          break;
        case 4:
          ctx.strokeStyle = LEVEL_4;
          ctx.fillStyle = LEVEL_4;
          break;
        case 5:
          ctx.strokeStyle = LEVEL_5;
          ctx.fillStyle = LEVEL_5;
          break;
        default:
          break;
      }
    } else {
      ctx.strokeStyle = '#ccc';
      ctx.fillStyle = '#ccc';
    }
    ctx.fill();
    ctx.stroke();
  }

最後に

Canvasというと難しく捉えられがちですが、しっかりと整理して考えればそこまで難しいことはないですね。
ここにアニメーションなど付けようとしたらやることはいろいろとやることは増えると思いますが。
こちらのデモコードはCodePenにアップしていますので良ければ参考にお使いください。

Recent Entries
MD EVENT REPORT
What's Hot?