# スクリーンリーダーで数値が伝わらないグラフ

Source: https://easeweb.dev/learn/chart-alternatives
Topics: 表とデータ > グラフにテキストの代替がある (WCAG 1.1.1)
The fix: グラフの canvas に role="img" を付け、データから作った名前を与えます。名前にすべての値を書くか、主な読み取り結果だけの短い名前にして、値は同じデータから作った表でグラフのそばに置きます。グラフが変わるたびに、どちらも更新します。
Test with: screen reader, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html https://www.w3.org/WAI/tutorials/images/complex/ https://www.w3.org/WAI/WCAG22/Techniques/failures/F20 https://www.w3.org/WAI/WCAG22/Techniques/failures/F67 https://www.chartjs.org/docs/latest/general/accessibility.html https://www.highcharts.com/docs/accessibility/accessibility-module https://github.com/recharts/recharts/releases/tag/v3.0.0

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術でのテスト結果は報告していません。

## 場面

ネットショップの管理画面を開くと、最初に「日別の注文数」という見出しのカードがあります。見出しの下に「先週」とあり、その下に、月曜日から日曜日まで1日1本ずつ棒が並ぶ棒グラフがあります。見れば、一番多かったのは土曜日の31件、一番少なかったのは月曜日の12件だと、すぐに分かります。

スクリーンリーダーでカードを読むと、「日別の注文数、見出し」「先週」と読んだ後、カードの次の内容へ進みます。グラフには一言も触れません。よくあるもう一つの作り方では、グラフに名前が付いていて、スクリーンリーダーは「Orders chart、画像」と読みます。グラフがあることは分かっても、何を示しているのかは分かりません。一番多かった曜日も、7日分の件数も一つも伝わりません。

## 確認できる問題

このグラフは、Chart.js が `canvas` 要素に描いたものです。ライブラリーの入門の例 `<canvas id="myChart"></canvas>` と同じ作りです。canvas はピクセルを描くための面で、棒も曜日のラベルも目盛りの数字も、その上に絵として描かれます。どれも、ブラウザーがスクリーンリーダーに渡せるテキストとしては存在しません。Chart.js のドキュメントにも、canvas の内容はスクリーンリーダーからは利用できないとあり、テキストの代替は、canvas にARIAを付けるか、canvas の中に代わりの内容を置くかして、ページの側で用意するよう求めています。この canvas にはそのどちらもないため、読むものが何もありません。

達成基準1.1.1「非テキストコンテンツ」は、情報を伝える非テキストコンテンツのすべてに、同じ目的を果たすテキストの代替を求めています。グラフはデータと、そこから読み取れることを示すためにあるので、テキストの代替もそれを伝える必要があります。二つ目の作り方では `role="img"` と `aria-label="Orders chart"` を付けています。これでグラフに名前は付きますが、あるのは名前だけです。この名前はグラフであることを言うだけで、何を伝えているかは言いません。Highcharts も、アクセシビリティモジュールを読み込まないと、グラフの SVG にタイトルだけを名前として付けます。こうした名前は、グラフと同じ目的を何も果たさないため、名前がない場合と同じように1.1.1を満たしません。

## 利用者への影響

管理画面は、ショップの運営者がその週の売れ行きを確かめる場所です。その件数がページに出ているのは、グラフの中だけということもよくあります。

## 自動チェックとAIの限界

axe には中身のない canvas を調べるルールがないため、一つ目の作りは axe を通ります。axe の `role-img-alt` は、`role="img"` があるのに名前のない要素を報告します。react-chartjs-2 の canvas は、名前を渡さない限り `role="img"` だけを付けるため、これに当たります。ところが `aria-label="Orders chart"` を付ければ、このルールは何も言わなくなります。どれほど中身のない名前でも同じです。手で直しても、AIのアシスタントに直させても、この程度の修正で終わってしまうことがあります。この診断は、Chart.js、Highcharts、Recharts、ApexCharts、ECharts、Plotly が描いたグラフを見分けます。テキストの代替がまったくないか、名前が「chart」のような言葉だけで、そばに表もないグラフは、問題として報告します。それ以外に見つけたグラフは、付いている名前やキャプションを引用した確認候補として出し、棒と見比べるのは人に任せます。名前が棒の示す内容を伝えているかどうかは、グラフが見える人にしか判断できません。

## 修正前：読むものが何もない canvas に描いたグラフ

```html
<section class="card">
  <h2>Orders per day</h2>
  <p class="range">Last week</p>
  <canvas id="orders"></canvas>
</section>
```

```js
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const orders = [12, 15, 14, 18, 24, 31, 20];
const canvas = document.getElementById('orders');

new Chart(canvas, {
  type: 'bar',
  data: {
    labels: days.map(day => day.slice(0, 3)),
    datasets: [{ label: 'Orders', data: orders }]
  }
});
```

- 棒も曜日のラベルも目盛りもピクセルとして描かれるため、どれも読めない。
- canvas には名前も中身もないため、スクリーンリーダーは何も言わずに通り過ぎる。

## 修正前：何であるかだけを名前にしたグラフ

```html
<section class="card">
  <h2>Orders per day</h2>
  <p class="range">Last week</p>
  <canvas id="orders" role="img" aria-label="Orders chart"></canvas>
</section>
```

```js
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const orders = [12, 15, 14, 18, 24, 31, 20];
const canvas = document.getElementById('orders');

new Chart(canvas, {
  type: 'bar',
  data: {
    labels: days.map(day => day.slice(0, 3)),
    datasets: [{ label: 'Orders', data: orders }]
  }
});
```

canvas が名前のある画像として読まれるようになった点は正しい作りです。問題は、その名前の中身です。

- 「Orders chart」はグラフであることを言うだけで、何を示しているかは言わない。
- 曜日も件数もテキストのどこにもないため、一番多かった曜日はやはり伝わらない。

## 修正後：グラフの名前にデータを書く

```html
<section class="card">
  <h2>Orders per day</h2>
  <p class="range">Last week</p>
  <canvas id="orders" role="img"></canvas>
</section>
```

```js
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const orders = [12, 15, 14, 18, 24, 31, 20];
const canvas = document.getElementById('orders');

new Chart(canvas, {
  type: 'bar',
  data: {
    labels: days.map(day => day.slice(0, 3)),
    datasets: [{ label: 'Orders', data: orders }]
  }
});
canvas.setAttribute('aria-label', describe());

function describe() {
  const top = orders.indexOf(Math.max(...orders));
  const each = days.map((day, i) => `${day} ${orders[i]}`).join(', ');
  return `Orders per day, last week. Busiest on ${days[top]}, with ${orders[top]}. ${each}.`;
}
```

1. canvas に `role="img"` を付け、名前のある一つの画像として読まれるようにする。canvas には、名前を持てる役割がもともとない。
2. グラフを描くのと同じ二つの配列から名前を作る。何を示すグラフか、主な読み取り結果、各曜日とその件数の順に書く。スクリーンリーダーは「Orders per day, last week. Busiest on Saturday, with 31. Monday 12, Tuesday 15, …, Sunday 20、画像」と読む。
3. 名前はグラフを描くコードの中で設定し、新しい週のデータには新しい名前が付くようにする。

これで二つの修正前の例はどちらも直り、この程度の小さなグラフなら1.1.1を満たします。7つの値なら、聞いて追える一文に収まります。ただ、数値が届くのはスクリーンリーダー利用者だけで、しかも一続きの文として読まれるため、木曜日で止めたり、金曜日から読み直したりはできません。データ点が数十あるグラフや、系列がいくつもあるグラフには、次の方法の表が必要です。

## 修正後：短い名前と、グラフのそばのデータの表

```html
<section class="card">
  <h2>Orders per day</h2>
  <p class="range">Last week</p>
  <canvas id="orders" role="img"></canvas>
  <details>
    <summary>Show the numbers</summary>
    <table id="orders-table">
      <caption>Orders per day, last week</caption>
      <thead>
        <tr>
          <th scope="col">Day</th>
          <th scope="col">Orders</th>
        </tr>
      </thead>
      <tbody></tbody>
    </table>
  </details>
</section>
```

```js
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const orders = [12, 15, 14, 18, 24, 31, 20];
const canvas = document.getElementById('orders');

new Chart(canvas, {
  type: 'bar',
  data: {
    labels: days.map(day => day.slice(0, 3)),
    datasets: [{ label: 'Orders', data: orders }]
  }
});
canvas.setAttribute('aria-label', describe());
fillTable();

function describe() {
  const top = orders.indexOf(Math.max(...orders));
  return `Orders per day, last week. Busiest on ${days[top]}, with ${orders[top]}. Every day's count is under Show the numbers.`;
}

function fillTable() {
  const body = document.querySelector('#orders-table tbody');
  body.replaceChildren();
  days.forEach((day, i) => {
    const row = body.insertRow();
    const head = document.createElement('th');
    head.scope = 'row';
    head.textContent = day;
    row.append(head);
    row.insertCell().textContent = orders[i];
  });
}
```

1. canvas に `role="img"` を付け、データから短い名前を作る。何を示すグラフか、主な読み取り結果、数値がどこにあるかを書く。スクリーンリーダーは「Orders per day, last week. Busiest on Saturday, with 31. Every day's count is under Show the numbers、画像」と読む。
2. グラフのすぐ下に `details` 要素を置き、`summary` には中身が分かる「Show the numbers」を書く。開くまでは閉じたままなので、カードの見た目は変わらない。
3. その中に `table` を置いてキャプションと各列・各行の見出しを付け、同じ二つの配列から行を作る。スクリーンリーダーではセルを一つずつ移動でき、「Saturday、31」が見出しと一緒に読まれる。正確な件数を知りたい人は、誰でも開いて読める。
4. どちらもグラフを描くコードの中で実行し、名前と行が棒と一緒に変わるようにする。

これで二つの修正前の例はどちらも直り、グラフの大きさにかかわらず1.1.1を満たします。表は canvas の外に置きます。`role="img"` の要素の中身は表示のためだけのものとされ、ブラウザーはスクリーンリーダーに渡さないことになっています。そのため、canvas の中に代わりの内容として表を置いても読まれません。

## どの方法を選ぶか

この例のグラフなら、どちらの方法でも1.1.1を満たします。名前にデータを書く方法は、属性一つと関数一つで済む最小の変更で、棒が数本のカードのように、場所の限られた小さなグラフに向いています。ただ、数値が届くのはスクリーンリーダー利用者だけで、一続きの文として読まれ、棒が増えるたびに長くなります。表を添える方法は、数行のコードと、グラフの下の閉じた折りたたみウィジェットが増える代わりに、どの大きさのグラフでも使えます。曜日を一つずつ移動して戻ることもでき、目が見えていても、棒の高さから見積もるより数字を読みたい人に正確な件数が届きます。名前だけの方法はグラフが大きくなるとすぐに使えなくなるため、まずは表を添える方法を選んでください。

## 実装の判断

**名前に書く内容。** グラフで伝えたいことを書きます。何をどの期間について数えたのかと、読み手に一番伝えたい一点です。運営者がこのグラフを見るのは「土曜日が一番多く、31件」を知るためです。「青い棒の棒グラフ」のような描き方は書かず、「グラフ：」のような言葉で始めることもしません。「画像」とはスクリーンリーダーが読み上げます。

**データが変わるグラフ。** 管理画面のグラフは、期間を変えたとき、絞り込みをかけたとき、新しい注文が入ったときに描き直されます。名前と表の行は、`chart.update()` を呼ぶのと同じ関数の中で設定し直し、テキストが古い棒の内容のまま残らないようにします。HTMLに一度だけ書いた名前のまま、スクリプトでグラフだけを描き直すのは、W3Cの失敗例F20にあたります。

**`aria-describedby` と `aria-details`。** `aria-describedby` で表を指すと、表は一続きの言葉として読まれ、行や見出しの構造が失われます。一文の説明には向いていても、表には向きません。`aria-details` なら構造を失わずに表を指せますが、詳細があることを伝えるかどうかも、そこへの移動のしかたも、スクリーンリーダーによって違います。この例ではどちらも要りません。表はグラフのすぐ後にあり、その場所は名前で伝えています。

**常に表示する表。** レポートのページなど、正確な数値が大事な場所では、`details` で囲まずに表をそのまま表示するか、グラフと表を切り替えられるようにします。どちらでも、同じデータから作る同じ表です。

**グラフのライブラリーの機能。** Highcharts のアクセシビリティモジュール（`accessibility.js`）は、グラフに説明とキーボード操作を加えます。export-data モジュールを使えば、グラフのデータを表として表示することもできます。Recharts の `accessibilityLayer` は、バージョン3.0から標準で有効になっていて、グラフにキーボード操作を加えます。役に立つところでは使い、スクリーンリーダーで実際に何が聞こえるかを確かめてください。ライブラリーの説明は設定から作られるため、タイトルと読み取り結果は、やはり自分で与える必要があります。

**グラフが本文の繰り返しにすぎない場合。** グラフのすぐそばの表のように、数値がすでにテキストとしてページにあるなら、グラフはスクリーンリーダー利用者に新しいことを何も伝えません。それでも、何を示すグラフで、数値は表にあることを短い名前で伝えるか、`aria-hidden="true"` で隠します。`role` も名前もないグラフの SVG は、スクリーンリーダーによって扱いが異なり、読み飛ばされることも、グループとして読まれることもあります。軸のラベルなど、SVG の中に描かれた文字が、ばらばらに読み上げられることもあります。

青とオレンジの2本の線を凡例で見分けるように、系列を色だけで区別するのは、1.4.1「色の使用」の別の問題です。ステータスの印での同じ問題を[色だけで区別されるステータス表示](/ja/learn/status-colours)で扱っています。表計算ソフトから書き出したPNGのように、グラフを画像にした場合も、同じテキストの代替が必要です。1.4.5「文字画像」は、グラフのように文字以外の情報を多く含む図の中の文字を対象外としているため、グラフのラベルは文字画像にあたりません。ただし、数値の表を画像に描き込んだものは文字画像です。[実際のテキストの代わりに使われた文字画像](/ja/learn/images-of-text)で扱っています。

## 修正の確認

1. コードで、グラフをすべて探す。グラフのライブラリーが描く `canvas` と `svg`、グラフの画像のすべてが対象。
2. ブラウザーのアクセシビリティツリーで、各グラフの名前を読む。「chart」やグラフのタイトルだけでなく、何を示すかと主な読み取り結果を言っていることを確認する。
3. 名前と、表があれば表を、グラフのデータと比べる。グラフに描いた値がすべてあり、読み取り結果が一番大きい値や一番大事な値と合っていることを確認する。
4. スクリーンリーダーでカードを上から読む。グラフが読み取り結果と一緒に読まれ、名前か表で、各曜日の件数に移動して一つずつ読めることを確認する。
5. データを変える。別の期間を選ぶ、絞り込みをかける、別の日にページを開くなどしてから、もう一度グラフを読み、名前と表が新しい数値を伝えることを確認する。

OS、ブラウザーと支援技術のバージョン、ビルド、実施日、各手順の実際の結果を記録してください。グラフのコンポーネントやデータの取得元を変更した際は、再度確認します。

## 制限

このサンプルは、どのグラフでも同じ結果になることを保証するものではありません。`Chart` は Chart.js のことで、どのライブラリーで描いたグラフにも、手で描いたグラフにも、同じテキストの代替が当てはまります。このガイドで扱うのは、グラフのテキストの代替です。ホバーしたときだけ値を表示したり、棒をクリックするとページが絞り込まれたりするなど、データ点や凡例、ツールチップがポインターでしか働かないグラフは別の問題です。凡例については[キーボードでは系列を切り替えられないグラフの凡例](/ja/learn/chart-legends)で、ホバーしたときだけ出る値については[キーボードでは表示できないグラフのツールチップ](/ja/learn/chart-tooltips)で扱っています。グラフ以外の画像については[代替テキストがない、情報を伝える画像](/ja/learn/informative-images)を参照してください。管理画面全体を、対象とする支援技術で確認してください。

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
