ドーナツグラフ(Doughnut Chart)— 中央テキスト・複数並べ

グラフ・チャート 初級

このコンポーネントについて

ドーナツグラフは円グラフの中央が空洞になった形式で、その空洞部分に達成率や合計値などのテキストを重ねて表示できるのが最大の特徴です。 タスク完了率・KPI達成率・予算消化率など、「何%が達成されているか」を一目で示すダッシュボードやレポートでよく使われます。 このページでは Chart.js v4(CDN)を使い、外部の JSON ファイルからデータを fetch して2つのパターンを紹介します。

  • Pattern 1 — 中央テキスト付きドーナツ — タスク進捗を「完了 / 進行中 / 未着手」の3区分で可視化。Chart.js v4 のプラグイン API(afterDraw)を使って中央に完了率を描画する
  • Pattern 2 — 複数ドーナツ横並べ — 部門ごとの目標達成率を3つのドーナツで横並び表示。各ドーナツの中央に達成率(%)を表示し、一覧性を高める

実装のポイント・注意点

ドーナツグラフの中央テキスト表示は、Chart.js v4 のプラグイン APIafterDraw)を使って canvas 上に直接テキストを描画します。ctx.fillText() で描画位置を chartArea の中心座標に合わせることで、グラフのサイズが変わっても常に中央に表示されます。このプラグインは new Chart(ctx, { plugins: [myPlugin] }) のようにローカルプラグインとして渡すと、そのグラフだけに適用されます。

Pattern 2 では、JSON の部門数に応じて canvas 要素を JavaScript で動的生成し、new Chart() に渡します。注意:new Chart() の第1引数に #id 文字列を渡す方法は、appendChild 前にDOMに存在しない場合はエラーになります。canvas 要素(HTMLCanvasElement)を直接渡してください。

cutout: '65%' は中央の穴の大きさを指定します。文字列でパーセント指定し、数値で指定するとピクセル値として解釈されるため注意してください(Chart.js v4 の仕様)。穴が大きいほどテキストが読みやすくなります。

HTML・CSS・JavaScriptで実装しており、Chart.jsライブラリ(CDN)を1本追加するだけで動きます。フレームワーク不要でコピペすぐに使えます。

デモ

Pattern 1 — 中央テキスト付きドーナツ(タスク完了率)

データを読み込んでいます...

Pattern 2 — 複数ドーナツ横並べ(部門別達成率)

サンプルソース

4つのファイルを同じフォルダに保存し、ローカルサーバー(VS Code Live Server 等)で index.html を開くと動作確認できます。
ファイル名:index.html / style.css / script.js / data/data.json — 保存時の文字コードは UTF-8 を指定してください。
⚠️ fetchfile:// では動作しません。ローカルサーバー経由(http://)で開いてください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ドーナツグラフ サンプル</title>
  <link rel="stylesheet" href="./style.css">
  <!-- Chart.js v4(UMD ビルド) -->
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
</head>
<body>

<!-- ===== Pattern 1: 中央テキスト付きドーナツ ===== -->
<section class="chart-section">
  <h2 class="chart-title">Pattern 1 — 中央テキスト付きドーナツ(タスク完了率)</h2>
  <div class="chart-wrap chart-wrap--single">
    <canvas id="doughnut-task" aria-label="タスク完了率のドーナツグラフ" role="img"></canvas>
  </div>
</section>

<!-- ===== Pattern 2: 複数ドーナツ横並べ ===== -->
<section class="chart-section">
  <h2 class="chart-title">Pattern 2 — 複数ドーナツ横並べ(部門別達成率)</h2>
  <!-- JS で部門数分の canvas を動的生成 -->
  <div class="chart-grid" id="doughnut-grid"></div>
</section>

<script src="./script.js"></script>
</body>
</html>
body {
  font-family: sans-serif;
  padding: 24px;
  background: #F8FAFC;
  color: #1E293B;
}

.chart-section {
  margin-bottom: 48px;
}

.chart-title {
  font-size: 15px;
  font-weight: 700;
  margin: 0 0 16px;
}

/* Pattern 1: 単体ドーナツ */
.chart-wrap--single {
  max-width: 360px;
  height: 360px;
  background: #fff;
  border: 1px solid #E2E8F0;
  border-radius: 8px;
  padding: 16px;
  box-sizing: border-box;
}

/* Pattern 2: グリッド */
.chart-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
  width: 100%;
}

.chart-grid-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  background: #fff;
  border: 1px solid #E2E8F0;
  border-radius: 8px;
  padding: 16px 12px 12px;
  box-sizing: border-box;
}

.chart-grid-canvas-wrap {
  width: 100%;
  height: 140px;
}

.chart-grid-label {
  margin: 10px 0 0;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
}

@media (max-width: 480px) {
  .chart-grid {
    grid-template-columns: 1fr;
    max-width: 280px;
  }
}
// 中央テキストを描画するローカルプラグイン
// chart.options.plugins.centerText = { line1, line2 } で渡す
var centerTextPlugin = {
  id: 'centerText',
  afterDraw: function (chart) {
    var opts = chart.options.plugins.centerText;
    if (!opts) return;
    var ctx = chart.ctx;
    var area = chart.chartArea;
    var cx = area.left + area.width / 2;
    var cy = area.top + area.height / 2;
    ctx.save();
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    // 上段: ラベル
    ctx.font = 'bold 12px sans-serif';
    ctx.fillStyle = '#64748B';
    ctx.fillText(opts.line1, cx, cy - 12);
    // 下段: 数値
    ctx.font = 'bold 22px sans-serif';
    ctx.fillStyle = '#1E293B';
    ctx.fillText(opts.line2, cx, cy + 12);
    ctx.restore();
  }
};

document.addEventListener('DOMContentLoaded', function () {
  fetch('./data/data.json')
    .then(function (r) { return r.json(); })
    .then(function (data) {
      renderTaskCompletion(data.taskCompletion);
      renderDepartmentGoals(data.departmentGoals);
    })
    .catch(function (err) {
      console.error('データの取得に失敗しました:', err);
    });
});

// Pattern 1: タスク完了率(中央テキスト付き)
function renderTaskCompletion(data) {
  var values = data.datasets[0].data;
  var total = values.reduce(function (a, b) { return a + b; }, 0);
  var pct = Math.round(values[0] / total * 100);
  var ctx = document.getElementById('doughnut-task');
  new Chart(ctx, {
    type: 'doughnut',
    plugins: [centerTextPlugin],
    data: {
      labels: data.labels,
      datasets: [{
        data: values,
        backgroundColor: [
          'rgba(43, 127, 232, 0.85)',
          'rgba(251, 191, 36, 0.85)',
          'rgba(203, 213, 225, 0.85)'
        ],
        borderColor: '#fff',
        borderWidth: 2,
        hoverOffset: 6
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      cutout: '65%',
      plugins: {
        centerText: { line1: '完了率', line2: pct + '%' },
        title: {
          display: true,
          text: data.label,
          font: { size: 13 },
          padding: { bottom: 12 }
        },
        legend: {
          position: 'bottom',
          labels: { padding: 14, font: { size: 12 } }
        }
      }
    }
  });
}

// Pattern 2: 部門別達成率(複数ドーナツ横並べ)
function renderDepartmentGoals(data) {
  var grid = document.getElementById('doughnut-grid');
  data.departments.forEach(function (dept) {
    var item = document.createElement('div');
    item.className = 'chart-grid-item';

    var canvasWrap = document.createElement('div');
    canvasWrap.className = 'chart-grid-canvas-wrap';

    var canvas = document.createElement('canvas');
    canvas.setAttribute('aria-label', dept.name + ' 達成率のドーナツグラフ');
    canvas.setAttribute('role', 'img');
    canvasWrap.appendChild(canvas);

    var label = document.createElement('p');
    label.className = 'chart-grid-label';
    label.textContent = dept.name;

    item.appendChild(canvasWrap);
    item.appendChild(label);
    grid.appendChild(item);

    // canvas を DOM に追加してから Chart を初期化する
    new Chart(canvas, {
      type: 'doughnut',
      plugins: [centerTextPlugin],
      data: {
        labels: ['達成', '未達'],
        datasets: [{
          data: [dept.achieved, dept.unachieved],
          backgroundColor: [
            'rgba(43, 127, 232, 0.85)',
            'rgba(203, 213, 225, 0.6)'
          ],
          borderColor: '#fff',
          borderWidth: 2,
          hoverOffset: 4
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        cutout: '68%',
        plugins: {
          centerText: { line1: '達成率', line2: dept.achieved + '%' },
          legend: { display: false },
          title: { display: false }
        }
      }
    });
  });
}
{
  "taskCompletion": {
    "label": "プロジェクトA タスク進捗",
    "labels": ["完了", "進行中", "未着手"],
    "datasets": [
      {
        "data": [28, 12, 10]
      }
    ]
  },
  "departmentGoals": {
    "label": "部門別 目標達成率(Q2)",
    "departments": [
      { "name": "営業部", "achieved": 78, "unachieved": 22 },
      { "name": "開発部", "achieved": 92, "unachieved": 8 },
      { "name": "マーケ部", "achieved": 65, "unachieved": 35 }
    ]
  }
}

AI用プロンプト

各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定やデータ変更など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

Pattern 1 — 中央テキスト付きドーナツ

# ドーナツグラフ・中央テキスト付き(Chart.js v4)作成依頼

## 概要
Chart.js v4(CDN)と JSON fetch を使ったドーナツグラフを実装してください。
グラフの中央に達成率テキストを表示します。

## 要件
- Chart.js v4 の UMD ビルドを CDN から読み込む
- data.json を fetch で取得してグラフを描画する
- グラフの種類はドーナツ(type: 'doughnut')
- cutout を 65% 程度に設定して穴を大きくする
- Chart.js v4 のプラグイン API(afterDraw)を使ってグラフ中央にテキストを描画する
  - 上段: ラベル(例:「完了率」)
  - 下段: 数値(例:「72%」)
  - ctx.fillText() で描画し、chartArea の中心座標に配置する
- 各スライスに色を設定する(3色以上のパレットを配列で管理する)
- スライス間の区切りに白い border を設定する(borderColor: '#fff', borderWidth: 2)
- グラフタイトルをグラフ内(上部)に表示する
- 凡例(legend)を下部に表示する
- fetch 中はローディングメッセージを表示し、完了後に非表示にする
- レスポンシブ対応(responsive: true)
- canvas の親要素の高さを CSS で指定し maintainAspectRatio: false で制御する

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:Chart.js v4(https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js)
- レスポンシブ対応:必要

## データ形式(data.json)
{
  "label": "グラフタイトル",
  "labels": ["完了", "進行中", "未着手"],
  "datasets": [
    { "data": [28, 12, 10] }
  ]
}

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。

Pattern 2 — 複数ドーナツ横並べ

# ドーナツグラフ・複数並べ(Chart.js v4)作成依頼

## 概要
Chart.js v4(CDN)と JSON fetch を使った複数のドーナツグラフを横並びで表示する実装を作成してください。
部門ごとの達成率を並べて比較するダッシュボード向けパターンです。

## 要件
- Chart.js v4 の UMD ビルドを CDN から読み込む
- data.json を fetch で取得してグラフを描画する
- data.json の departments 配列をループし、部門数分のドーナツグラフを動的に生成する
- 各ドーナツは JavaScript で canvas 要素を createElement して生成し、グリッドコンテナに appendChild する
- CSS Grid(grid-template-columns: repeat(3, 1fr))で横並びにする
- 各ドーナツは 達成(青)/ 未達(グレー)の2色で構成する
- 各ドーナツの中央に達成率(%)を afterDraw プラグインで描画する
- 各ドーナツの下に部門名を p タグで表示する
- 凡例・タイトルは非表示にする
- スマホ幅(480px以下)では1列縦並びに切り替える
- レスポンシブ対応(responsive: true)

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:Chart.js v4(https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js)
- レスポンシブ対応:必要

## データ形式(data.json)
{
  "departmentGoals": {
    "departments": [
      { "name": "営業部", "achieved": 78, "unachieved": 22 },
      { "name": "開発部", "achieved": 92, "unachieved": 8 },
      { "name": "マーケ部", "achieved": 65, "unachieved": 35 }
    ]
  }
}

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。