Web技術で真ん中がくりぬかれたようなドーナツ型の円形グラフを作る

2026年10月19日 Posted 野々瀨(フロントエンドエンジニア)

Webの技術で真ん中がくり抜かれたようなドーナツ型の円形グラフを、ライブラリを使わずに作る方法をご紹介します。

Canvas APIで作る

Canvas APIを使用して作る方法です。まずcanvas要素を記述します。

<canvas width="160" height="160"></canvas>

次のようなJavaScriptを記述します。

const datas = [
    {
        value : 36,
        color : "#ffb1b1"
    },
    {
        value : 20,
        color : "#ffbe35"
    },
    {
        value : 25,
        color : "#bddd77"
    },
    {
        value : 5,
        color : "#38f9d1"
    },
    {
        value : 14,
        color : "#99baff"
    }
];

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

const halfWidth = canvas.width / 2;
const total     = datas.reduce((t, d) => t + d.value, 0);
const outRadius = (halfWidth / 3) * 2; // 中央を抜く大きさ(半径)

let prevRadians = -90 * (Math.PI / 180); // 円の開始を90度垂直に
let prevAngle   = 0;

datas.forEach(data => {
    const angle   = data.value / total * 360;
    const radians = (prevAngle + angle - 90) * (Math.PI / 180);

    ctx.beginPath();
    ctx.fillStyle = data.color;
    ctx.arc(halfWidth, halfWidth, halfWidth, prevRadians, radians);
    ctx.lineTo(halfWidth, halfWidth);
    ctx.closePath();
    ctx.fill();

    prevRadians = radians;
    prevAngle += angle;
});

// くり抜く
ctx.beginPath();
ctx.fillStyle = "#fff";
ctx.arc(halfWidth, halfWidth, outRadius, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();

ポイント

データの値の合計を100%として360度でそれぞれ割って、そこから角度を求めて円を描いていっています。

CanvasRenderingContext2D.arcメソッドで円形のグラフを作っています。

中央のくりぬきは白塗りの円形を作ってそれっぽく見せています。

CSSのconic-gradient関数で作る

CSSのconic-gradient関数で作る方法です。まず空の要素を記述します。

<div class="sample"></div>

次のようなCSSを記述します。

.sample {
    --val-1: 36;
    --val-2: 20;
    --val-3: 25;
    --val-4: 5;
    --val-5: 14;

    --color-1: #ffb1b1;
    --color-2: #ffbe35;
    --color-3: #bddd77;
    --color-4: #38f9d1;
    --color-5: #99baff;

    --total: calc(var(--val-1) + var(--val-2) + var(--val-3) + var(--val-4) + var(--val-5));

    --angle-1: calc(var(--val-1) / var(--total) * 360deg);
    --angle-2: calc(var(--angle-1) + (var(--val-2) / var(--total) * 360deg));
    --angle-3: calc(var(--angle-2) + (var(--val-3) / var(--total) * 360deg));
    --angle-4: calc(var(--angle-3) + (var(--val-4) / var(--total) * 360deg));

    border-radius: 50%;
    width: 160px;
    aspect-ratio: 1 / 1;
    background:
        radial-gradient(#fff 46%, transparent 46.6%),
        conic-gradient(
            var(--color-1) var(--angle-1),
            var(--color-2) var(--angle-1), var(--color-2) var(--angle-2),
            var(--color-3) var(--angle-2), var(--color-3) var(--angle-3),
            var(--color-4) var(--angle-3), var(--color-4) var(--angle-4),
            var(--color-5) var(--angle-4), var(--color-5)
        );
}

ポイント

calc関数で値の割合と角度を算出します。

conic-gradient関数で扇型のグラデーションを作れるので、それを応用して角度と色を当てます。

中央のくりぬきはradial-gradient関数で白塗りの円形を作ります。

SVGで作る

SVGで作る方法です。SVG自身は自動で計算はできないので、あらかじめ別途算出したりJavaScriptで動的に処理したりといったことが必要です。

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="160" height="160" transform="rotate(-90)">
    <circle cx="50" cy="50" r="41" fill="none" stroke-width="18" stroke="#ffb1b1" stroke-dasharray="92.6928,257.48" />
    <circle cx="50" cy="50" r="41" fill="none" stroke-width="18" stroke="#ffbe35" stroke-dasharray="51.496,314" stroke-dashoffset="-92.6928" />
    <circle cx="50" cy="50" r="41" fill="none" stroke-width="18" stroke="#bddd77" stroke-dasharray="64.37,314" stroke-dashoffset="-144.188" />
    <circle cx="50" cy="50" r="41" fill="none" stroke-width="18" stroke="#38f9d1" stroke-dasharray="12.874,314" stroke-dashoffset="-208.5588" />
    <circle cx="50" cy="50" r="41" fill="none" stroke-width="18" stroke="#99baff" stroke-dasharray="36.0472,314" stroke-dashoffset="-221.4328" />
</svg>

ポイント

円を描く際、水平方向の右側から開始されるため、垂直上から開始するようにsvg要素のtransform属性で-90度回転させます。

stroke-width属性でグラフ部分の太さを指定します。

r属性で半径を指定しますが、stroke-width属性による太さは外側と内側に広がって描画されるため、stroke-width属性の値の半分の値を引いた値をr属性に指定します。

stroke-dasharray属性で線を破線にします。グラフのデータを反映するための指定で、値をデータの円周の長さの割合,円周の長さで指定します。

stroke-dashoffset属性で破線の開始位置を指定します。直前までの「データの円周の長さの割合」を足した値を指定します。stroke-dashoffset属性は引っ張るという意味合いなので、負で指定することで前進します。例えば三つ目のデータは一つ目と二つ目の「データの円周の長さの割合」を足した値を負の値で指定します。

PHPのGDで作る

// データ
$datas = [
    [
        'value' => 36,
        'color' => '#ffb1b1'
    ],
    [
        'value' => 20,
        'color' => '#ffbe35'
    ],
    [
        'value' => 25,
        'color' => '#bddd77'
    ],
    [
        'value' => 5,
        'color' => '#38f9d1'
    ],
    [
        'value' => 14,
        'color' => '#99baff'
    ]
];

// グラフの大きさ
$size = 160;

// ジャギーを少なくするためのスケール
$scale = 3;

/**
 * 16進数カラーを10進数RGBカラーに変換
 */
function hex2rgb($hex_color) {
    preg_match('/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i', $hex_color, $m);

    return array_combine(
        ['red', 'green', 'blue'],
        array_map(fn($color) => hexdec($color), array_slice($m, -3))
    );
}

$half_width = $size * $scale / 2;
$total      = array_sum(array_map(fn ($x) => $x['value'], $datas));
$out_radius = ($size * $scale / 3) * 2;

$image = imagecreatetruecolor($size, $size);
$graph = imagecreatetruecolor($size * $scale, $size * $scale);

imagefill($image, 0, 0, imagecolorallocate($image, ...hex2rgb('#ffffff')));
imagefill($graph, 0, 0, imagecolorallocate($graph, ...hex2rgb('#ffffff')));

$prev_angle = -90;

foreach ($datas as $data) {
    // 色
    $color = imagecolorallocate($graph, ...hex2rgb($data['color']));

    // 角度
    $angle = ceil($data['value'] / $total * 360);

    imagefilledarc(
        $graph,
        $half_width,
        $half_width,
        $size * $scale,
        $size * $scale,
        $prev_angle,
        $prev_angle + $angle,
        $color,
        IMG_ARC_PIE
    );

    $prev_angle += $angle;
}

// くり抜き
imagefilledarc(
    $graph,
    $half_width,
    $half_width,
    (int)$out_radius,
    (int)$out_radius,
    0,
    360,
    imagecolorallocate($image, ...hex2rgb('#ffffff')),
    IMG_ARC_PIE
);

// リサンプリング
imagecopyresampled($image, $graph, 0, 0, 0, 0, $size, $size, $size * $scale, $size * $scale);

header('Content-type: image/png');
imagepng($image);
imagedestroy($image);

ポイント

色は分かりやすくするため、用意するデータは16進数のRGBで指定しています。
そこから内部的に利用できるように、10進数へ変換する関数hex2rgbを用意しています。

通常GDで円を描くとジャギーが発生するため、グラフ用と書き出し用の二つのイメージを用意し、グラフを数倍の大きさで用意。imagecopyresampledでリサンプリングして標準の大きさにリサイズします。

GDの多くの関数はfloat型には対応していないので、float型になってしまう部分はint型に変換しています。