React上のSVGで円形のゲージを作る

SVG の基本知識のおさらいをしてから、円形のゲージを作るにはどうすればよいのかを考えていきましょう。stroke-dasharray がポイントになります。
2021.10.31

SVG で円形のゲージを作ってみます。

デモ

あまり使いやすくはないのですが、簡単にデモサイトもつくってあります。コードは GitHub で公開しています。

前提となる SVG の基本知識のおさらい

1. stroke-align

下記は Figma の画面をキャプチャしたものになります。

inset outset center

Figma ではラインを引いて、そのラインを太くしようとしたときに、どちらの向きに太さをもたせるかといった設定ができます。

この設定を SVG の属性で再現するには stroke-align が最適です。

なのですが、この実装が入っているブラウザは残念ですが今のところありません(2021 年 10 月 31 日現在)。

そのため、単純に 100px * 100px の svg 上に直径 100pxstroke-width10px の円を描くと次のように円のアウトラインが欠けた状態となります。

アウトラインが欠けた円

円の半径を outerR とすると、<circle /> に指定するべき routerR - stroke-width / 2 となり、上記の円を欠けない円にするには <circle r="45" /> とする必要があります。

2. stroke-dasharray

stroke-dasharray については上記の MDN のサイトがとてもわかりやすいです。名前の通り、stroke の破線とその間隔を指定する属性になります。

Adobe Illustrator などでは破線の線のある部分を 線分(dash)、線のない部分を間隔(gap)としているため以降の説明に使用していきます。

stroke-dasharray は繰り返しのこの線分と間隔を指定できるプロパティです。以下にいくつかの具体例を画像にしています。例のそれぞれの線全体の長さは 300px です。

見ていただけるとわかるように、例えばただ 10 を指定すると線分と間隔を 10px で繰り返す破線を描画することになります。これは 10 10 のような指定と等価になります。

連続した数値をしている例でわかりやすいのが 10 20 30 のような指定で、最初の線分が 10、間隔が 20、次の線分が 30 でその次の間隔が 10 と線分と間隔を繰り返すような破線になります。

今の時点ではあまり意味のある指定にはなっていませんが、線の長さと同じ数値を入れた 300 の指定されたものは指定なしと同じ表示になります。ただし見えてはいない範囲には指定なしとは異なり、両サイドに 300px の間隔が存在しています。この間隔が次の章の stroke-dashoffset と合わせることで面白い効果を生み出すことになります。

3. stroke-dashoffset

stroke-dashoffsetstroke-dasharray のオフセットを設定するものです。負の値も指定できます。

線分の開始位置がずれる、以下のように考えるとわかりやすいです。薄い灰色の枠内が通常の表示部分とすると、offset に正の値を指定すると青い枠で囲われている部分が実際に表示される部分です。

上記の例ではなかなか使い所の想像がつきにくいのですが、以下の画像を見てもらえるとわかりやすいです。

このように、<line /> の長さを l として、その lvalue % の長さの線は、stroke-dasharraylstroke-dashoffsetl * (100 - value) / 100 とすることで表現できます。

円の線が描画される起点

<circle /> の stroke の起点は以下の図の赤いまるで囲われた部分です。

これは仕様で定義されています。

The arc of a ‘circle’ element begins at the "3 o'clock" point on the radius and progresses towards the "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform in the same manner as the geometry of the element.

円の起点は上にあってほしいので、これを調整するために transform を使って回転させる必要があります。

回転は指定なしだと原点座標を基準に回転するため、円の中心で回転するように指定する必要があります。今回は円なので、半径の長さを指定することになります。

円形ゲージを作る

以上の前提をふまえると、円のゲージを作ることはそこまで難しくありません。

import { SVGAttributes, useMemo } from "react";

type Props = Readonly<{
  color: SVGAttributes<SVGCircleElement>["stroke"];
  r: number;
  strokeWidth: number;
  value: number;
}>;

export const Circle = (props: Props) => {
  const { color, r: outerR, strokeWidth, value } = props;

  /**
   * SVGのwidthとheightとなるサイズ
   */
  const size = useMemo(() => {
    return outerR * 2;
  }, [outerR]);

  /**
   * strokeWidthを考慮した半径
   */
  const r = useMemo(() => {
    return outerR - strokeWidth / 2;
  }, [outerR, strokeWidth]);

  /**
   * 円周
   */
  const circumference = useMemo(() => {
    return 2 * Math.PI * r;
  }, [r]);

  /**
   * 表示する円周の長さ
   */
  const dashoffset = useMemo(() => {
    return circumference * ((100 - value) / 100);
  }, [circumference, value]);

  return (
    <svg
      width={size}
      height={size}
      viewBox={`0 0 ${size} ${size}`}
    >
      <circle
        r={r}
        cx={outerR}
        cy={outerR}
        stroke={color}
        fill="transparent"
        strokeWidth={strokeWidth}
        strokeDasharray={circumference}
        strokeDashoffset={dashoffset}
        transform={`rotate(-90) ${outerR} ${outerR}`}
      />
    </svg>
  );
};

Props

r: outerR は円の半径です。Props の指定で円のもっとも外側のラインを含めた半径のサイズを指定します。例えばここに 50 と指定すると縦と横が 100px の SVG に欠けることのない円がぴったり表示されることになります。

strokeWidth はそのまま <circle />strokeWidth が入ります。ただし単位つきだとうまく計算できない場合もあるので、ここでは数値のみ指定できるようにしています。

value には円のゲージがどこまで伸長するかを割合で指定します。

コンポーネント内で行っている計算

size は SVG の縦と横の長さになります。これは円の直径と同じサイズになるため、outerR * 2 で計算しています。

r は半径です。r: outerR の半径とは異なり stroke-align を指定できないことを考慮した半径になります。

circumference は円周です。円周は 2 * Math.PI * r で求めることができます。ここで使用する半径は実際の線の r になります。

dashoffset はどこまでゲージを伸ばすかを strokeDashoffset に指定する値となります。この値が circumference * ((100 - props.value) / 100) で求められることは stroke-dashoffset の項目で確認できています。

stroke-dashoffset を使わずに作る

円形ゲージの伸長を表現するために stroke-dashoffset を使いました。アニメーションなどを作るときにもよく使う手法なのですぐに思いついたのはこの方法でした。

ですが円形ゲージを作るために stroke-dashoffset が必須ではありません。stroke-dasharray を使って線分と間隔の比率を調整することで、同じことができます。

私はこの方法が思いつかず、以下のサイトを参考にさせていただきました。

<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
  <circle
    r={r}
    cx={outerR}
    cy={outerR}
    stroke={color}
    fill="transparent"
    strokeWidth={strokeWidth}
    strokeDasharray={`${circumference * (value / 100)} ${circumference}`}
    transform={`rotate(-90 ${outerR} ${outerR})`}
  />
</svg>

こちらのほうが stroke-dashoffset を使うよりもシンプルでわかりやすいですね。