🍩

Chart.js不要!SVGでドーナツチャートを実装してみた

2023/01/23に公開

バヅクリの伊藤です。
今回は Chart.js を使用せずに SVG でドーナツチャートを実装してみました。

背景

今回実装する、中央に数値が表示されるドーナツチャートを Chart.js を使用して実装していたところ、Chart.jsではデフォルトで真ん中に数値を表示する様なサポートはない為、
Chart.jsで作成したに対してラッパー要素を追加し、数値部分はpositionを使用して位置調整していました。
その際の、レスポンシブ対応・レイアウトの調整に苦戦しました...😇
この記事では、同じ様な実装に苦しんでいる方に SVG でも再現できることを伝えたいのと、実装するにあって調べことをアウトプットする目的で書きました ✏️

実装するもの

レッツ、トライ

今回は <svg><path> を使用して実装してみました!

const r = 100 / (2 * Math.PI);

export const Circle = ({ score }) => {
  return (
    <svg width="200" height="200" viewBox="0 0 40 40">
      <path
        d={`M20 ${(40 - (r + r)) / 2}
        a ${r} ${r} 0 0 1 0 ${r + r}
        a ${r} ${r} 0 0 1 0 -${r + r}`}
        fill="none"
        stroke="#F2F2F2"
        strokeWidth="6"
        strokeDasharray="100"
      />
      <path
        d={`M20 ${(40 - (r + r)) / 2}
        a ${r} ${r} 0 0 1 0 ${r + r}
        a ${r} ${r} 0 0 1 0 -${r + r}`}
        fill="none"
        stroke="#2fb6f0"
        strokeWidth="6"
        strokeDasharray={`${score} 100`}
      />
      <text x="12" y="23" fontSize=".5em" textAnchor="center">
        {score}%
      </text>
    </svg>
  );
};

1. <svg> で描画エリアを指定する

<svg width="200" height="200" viewBox="0 0 40 40">

width, heightで viewport を指定し、viewBoxで表示する寸法や位置を指定します

viewBox について

  • svg 要素に属性として追加でき、下記のようにスペースで区切って値を指定することができる
<svg viewBox="x y width height"></svg>

x 左上からの X 座標
y 左上からの Y 座標
width ビューボックスの寸法
height ビューボックスの寸法

下記の記事がわかりやすかったです 👇

ビューボックスの寸法、つまり最後の 2 つのパラメータを、ビューポートの寸法より大きくして「ズームアウト」し、小さくして「ズームイン」します。
https://webdesign.tutsplus.com/tutorials/svg-viewport-and-viewbox-for-beginners--cms-30844

2. <path>で円を作る

グレーの円と、ブルーのパーセントに応じて変動する 2 つ円を実装しています。
まずはパーセントに応じて変動する円を作ります!

<path
  d={`M20 ${(40 - (r + r)) / 2}
        a ${r} ${r} 0 0 1 0 ${r + r}
        a ${r} ${r} 0 0 1 0 -${r + r}`}
  // 塗りつぶしの色
  fill="none"
  // アウトライン(輪郭)の色
  stroke="#2fb6f0"
  // 線幅
  strokeWidth="6"
  // 線の間隔
  strokeDasharray={`${score} 100`}
/>

0〜100 の間で変動するグラフを作りたいので円周を 100 とした円の半径を求めます

const r = 100 / (2 * Math.PI);
  • d 属性に描画する直線や曲線を指定する

    • M に続くのは始点の座標。
      X 軸は、(width 40px / 2)、Y 軸は、(height 40px - 直径(r + r)) / 2
    • a に続くのは半径、半径、回転角、0:短孤 or 1:長孤、終点

参考記事

https://medium.com/@pppped/how-to-code-a-responsive-circular-percentage-chart-with-svg-and-css-3632f8cd7705

3. もう一つ<path>で円を作る

ベースとなるグレーの円を作成していきます。

<path
  d={`M20 ${(40 - (r + r)) / 2}
        a ${r} ${r} 0 0 1 0 ${r + r}
        a ${r} ${r} 0 0 1 0 -${r + r}`}
  fill="none"
  stroke="#F2F2F2"
  strokeWidth="6"
  strokeDasharray="100"
/>

4. 中央に数値を表示する

<text>を使用して、数値を表示しています

<text x="12" y="23" fontSize=".5em" textAnchor="center">
  {score}%
</text>

グラデーションをつけてみる

<linearGradient>を使用して線形グラデーションをつけました!

const r = 100 / (2 * Math.PI);

export const Circle = ({ score }) => {
  return (
    <svg width="200" height="200" viewBox="0 0 40 40">
+    <defs>
+        <linearGradient id="circle-gradient" x1="0" x2="0" y1="0" y2="1">
+          <stop offset="30%" stopColor="#2fb6f0" />
+          <stop offset="60%" stopColor="#396de7" />
+          <stop offset="100%" stopColor="#3a39e7" />
+        </linearGradient>
+      </defs>
      <path
        d={`M20 ${(40 - (r + r)) / 2}
        a ${r} ${r} 0 0 1 0 ${r + r}
        a ${r} ${r} 0 0 1 0 -${r + r}`}
        fill="none"
        stroke="#F2F2F2"
        strokeWidth="6"
        strokeDasharray="100"
      />
      <path
        d={`M20 ${(40 - (r + r)) / 2}
        a ${r} ${r} 0 0 1 0 ${r + r}
        a ${r} ${r} 0 0 1 0 -${r + r}`}
        fill="none"
+        stroke="url('#circle-gradient')"
        strokeWidth="6"
        strokeDasharray={`${score} 100`}
      />
      <text x="12" y="23" fontSize=".5em" textAnchor="center">
        {score}%
      </text>
    </svg>
  );
};

アニメーションをつけてみる

対象の<path>にクラスを付与し、CSS でアニメーションをつけました 🪩

css
.circle {
  animation: circleAnimation 1s;
}

@keyframes circleAnimation {
  0% {
    stroke-dasharray: 0 100;
  }
}

<circle>で作ってみる

<circle>を使用して実装することも可能です 💃🏻

// 円周(C = 2πr)
const circumference = 2 * Math.PI * 75;

export const Circle = ({ score }) => {
  return (
    <svg width="500" height="500" viewBox="0 0 500 500">
      <circle
        r="75"
        cx="90"
        cy="90"
        stroke="#F2F2F2"
        fill="transparent"
        strokeWidth="30"
        strokeDasharray={circumference}
        transform="rotate(-90 90 90)"
      ></circle>
      <circle
        r="75"
        cx="90"
        cy="90"
        stroke="#2fb6f0"
        fill="transparent"
        strokeWidth="30"
        strokeDasharray={`${circumference * (score / 100)} ${circumference}`}
        transform="rotate(-90 90 90)"
      ></circle>
      <text x="56" y="100" fontSize="2em" textAnchor="center">
        {score}%
      </text>
    </svg>
  );
};

参考記事

https://dev.classmethod.jp/articles/react-svg-circle-gauge/

最後に

バヅクリでは Rails エンジニアを募集しています 😍
カジュアル面談等も受け付けておりますので、ご興味ある方は下記リンクから是非お問い合わせください 📮

https://herp.careers/v1/buzzkuri/wCQFGVkQS3Xa

https://buzzkuri.co.jp/recruit

バヅクリテックブログ

Discussion