グラフのアクセシビリティについて考える
`<canvas>` や `<svg>` 要素で描画されたグラフは、スクリーンリーダーを使用するユーザーにとって内容を正しく理解することが難しいです。この記事では、グラフの内容をスクリーンリーダーを使用するユーザーに伝える方法について考えてみます。
Chart.js や d3.js などのライブラリを使用することで、手軽にグラフを作成できます。しかし、これらのライブラリはデフォルトではアクセシビリティに対応する機能を提供していないので、実装者が意識して対応する必要があります。
Chart.js と d3.js はそれぞれ <canvas> と <svg> を使用してグラフを描画します。<canvas> 要素に対してスクリーンリーダーがアクセスできません。そのため、スクリーンリーダーを使用するユーザーにとっては、グラフの内容を理解できません。<svg> 要素についても同様にグラフとして認識させるのは難しいでしょう。
この記事では、Chart.js を例にスクリーンリーダーに対応したグラフを作成する方法を考えてみます。
Chart.js を用いてグラフを作成したコードは以下のようになります。
import { Chart, registerables } from "chart.js";
import React, { useEffect, useRef } from "react";
Chart.register(...registerables);
const labels = [
"1月",
"2月",
"3月",
"4月",
"5月",
"6月",
"7月",
"8月",
"9月",
"10月",
"11月",
"12月",
];
const datasets = [
{
label: "東京",
data: [
4.9, 5.2, 10.9, 15.3, 18.8, 23.0, 27.4, 27.5, 24.4, 17.2, 14.5,
7.5,
],
},
{
label: "札幌",
data: [
-3.2, -2.2, 2.6, 9.1, 14.9, 16.8, 23.1, 22.7, 19.8, 12.6, 7.1,
-1.4,
],
},
]
export const Graph: React.FC = () => {
const chartRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (chartRef.current === null) {
return;
}
const chart = new Chart(chartRef.current, {
type: "line",
data: {
labels,
datasets
},
options: {
plugins: {
title: {
display: true,
text: "2022年の日平均気温の月平均値(℃)",
},
},
},
});
return () => {
chart.destroy();
};
}, []);
return (
<div
style={{
width: 800,
height: 600,
}}
>
<canvas ref={chartRef} />
</div>
);
};<canvas> 要素を role="img" としてグラフの概要を代替テキストとして指定する
始めに最もシンプルな方法から考えてみましょう。<canvas> に role="img" を付与することで、スクリーンリーダーが <canvas> を画像として認識するようになります。画像として認識された <canvas> に対して aria-label で代替テキストをとしてグラフの概要指定することで、スクリーンリーダーがグラフのとして理解できるようになります。
<canvas
ref={chartRef}
role="img"
aria-label="2022年の日平均気温の月平均値(℃)のグラフ"
/>VoiceOver で確認すると、「2022 年の日平均気温の月平均値(℃)のグラフ、イメージ」と読み上げられるようになりました。

グラフのデータを代替テキストとして指定する
グラフの概要を代替テキストとして指定することで、スクリーンリーダーにより何も読み上げられない状況よりも良い状況になりました。しかし、グラフの概要を読み上げるだけでは、グラフの内容を理解できません。グラフを閲覧するユーザーにとっては、グラフがどのようなデータを提供しているかに興味があるはずです。
次は代替テキストとしてグラフのデータを指定してみましょう。グラフのデータは datasets に格納されているので、これを文字列に変換して指定します。
/**
* 東京の平均気温は、1月は4.9℃、2月は5.2℃、... のような文字列を作る
*/
const datasetsText = datasets.reduce((acc, dataset) => {
return `${acc} ${dataset.label}の平均気温は、${dataset.data.reduce(
(acc, data, index) => {
return `${acc} ${labels[index]}は${data}℃、`;
},
""
)}です。`;
}, "");このテキストは aria-label として指定してもよいのですが、代替テキストの読み上げが長くなりすぎてしまうという問題があります。aria-label に指定するテキストは <img> に指定する alt 属性と同様に簡潔であるべきです。より詳細な情報を提供する場合には aria-describedby を使用するのが良いでしょう。aria-describedby に指定した要素の内容は description としてアクセシビリティツリーから公開されます。VoiceOver の場合には description の内容は aria-label などで指定した「アクセシブルな名前」が読み上げられてから一拍置いて読み上げられます。そのため、スクリーンリーダーを利用するユーザーは「アクセシブルな名前」で概要を理解したあとに、より詳細な情報を読み上げるかどうかを判断できます。
aria-describedby は id で要素を指定して、要素の内容を description として公開します。そのためテキストを表示するための要素を追加して id を指定する必要があります。
<canvas
ref={chartRef}
role="img"
aria-label="2022年の日平均気温の月平均値(℃)のグラフ"
aria-describedby="chart-description"
>
<p id="chart-description">
{datasetsText}
</p>基本的には青眼のユーザーに対しても同一の情報を提供するために、aria-describedby で公開する要素は視覚的にも表示されるのが好ましいです。しかし、スペースの都合などで詳細なテキストを表示するのが難しい場合もあるでしょう。このようにスクリーンリーダーにのみ情報を公開したい要素に対しては、下記のような .sr-only のようなクラスを用意しておくと便利です。display: none; などのスタイルで非表示にした場合、スクリーンリーダーからも読み上げられなくなってしまいます。
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}aria-describedby 詳細なグラフのデータをテキストして指定した結果、以下のように読み上げられるようになりました。

グラフのデータをテーブルとして表示する
グラフのデータを aria-describedby で公開することで、グラフの内容を理解できるようになりました。ですが、グラフのデータは平坦なテキストとなったことで、認知負荷が高くなってしまっています。例えば、「札幌の 8 月の平均気温」が知りたい場合でも、テキストを最初から最後までひととおり読み上げる必要があります。また、グラフにどのくらいの量のデータが含まれているかも把握できません。
解決策として、テキストを構造化した形式で提供することが考えられます。<table> 要素を使用してグラフのデータを表示することで、グラフのデータを構造化した形式で提供できます。
<canvas
ref={chartRef}
role="img"
aria-label="2022年の日平均気温の月平均値(℃)のグラフ"
/>
<table>
<caption>
2022年の日平均気温の月平均値(℃)
</caption>
<thead>
<tr>
<td></td>
{labels.map((label) => (
<th key={label}>{label}</th>
))}
</tr>
</thead>
<tbody>
{datasets.map((dataset) => (
<tr key={dataset.label}>
<th scope="row">{dataset.label}</th>
{dataset.data.map((data) => (
<td key={data}>{data}</td>
))}
</tr>
))}
</tbody>
</table>上記の <table> を実装する際には、以下の 3 つの工夫をしています。
<caption>でテーブルの概要を指定する<th>要素で見出しを指定する<th scope="col">で列の見出しを指定する
<caption> でテーブルの概要を指定する
<caption> 要素はテーブルの説明を提供するために使用します。青眼のユーザーにとってももちろん便利なのですが、スクリーンリーダーにとってテーブルの詳細なデータを読み取るかどうか決定するために頼りになるため、特に重要です。特に 1 つのページ内に複数のテーブルが存在する場合には、テーブル同士を区別するための重要な情報となります。
VoiceOver でテーブルにフォーカスを当てると、テーブルの概要として <caption> の内容と、テーブルの行数と列数が読み上げられます。

注意点として、<caption> は必ず <table> の最初の子要素として配置する必要があります。
<th> 要素で見出しを指定する
<th> 要素はテーブルの見出してして使用されます。テーブルの見出しはスクリーンリーダーにとって特に重要です。テーブルの見出しがない場合、テーブルのセルを読み上げる際に、単にセルの値だけを読み上げるのでどの列のデータを読み上げているのかがわかりにくくなってしまいます。
<th> 要素によってテーブルの見出しが設定されている場合、スクリーンリーダーはテーブルのセルの内容を読み上げる際に、セルの値と、セルに対応する見出しを関連付けて読み上げるようになります。

<th scope="row"> で行の見出しとしてを指定する
テーブルの構造によっては、行の見出しと列の見出しが必要になる場合があります。グラフのデータをテーブルとして表示する場合にはまさに当てはまるでしょう。今回のグラフの場合には、列の見出しとして月を、列の見出しとして地域が必要になります。
| 1月 | 2月 | 3月 | |
|---|---|---|---|
| 東京 | 4.9 | 5.2 | 10.9 |
| 札幌 | -3.2 | -2.2 | 2.6 |
<th> 要素に scope 属性が指定されていない場合、ブラウザは自動的に列に対応する見出しか行に対応する見出しかを判断します。場合によっては正しく見出しが関連付けられない恐れがあるので、scope 属性を指定して明示的に見出しの関連付けを行うようにしています。scope 属性は以下の 4 つの値を指定できます。
row:行の見出しとして指定するcol:列の見出しとして指定するrowgroup:行グループの見出しとして指定するcolgroup:列グループの見出しとして指定する
その他の考慮事項
その他テーブルを使用する利点として、スクリーンリーダーを使用している場合テーブルは上下左右のカーソルキーで移動できるので、情報を得たいデータに対して素早く移動できることがあげられます。
また、aria-describedby で詳細なテキストを指定する場合に議論したように、基本的には青眼のユーザーに対しても同等の情報を提供できるように、テーブルは視覚的にも表示されるのが好ましいです。スペースの都合上でテーブルを表示するのが難しい場合には同様に .sr-only のようなクラスを指定するとよいでしょう。ただし、<table> のデフォルトのスタイルである display: table; は width の指定が効かないため注意が必要です。<table> に display: block; を指定することで、width の指定が効くようになり width: 1px として視覚的に表示されないようになります。
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}その他の手段として、<detail> 要素を使用してテーブルを折りたたみ/展開可能にするという方法も考えられます。
まとめ
<canvas>や<svg>で描画されたグラフはスクリーンリーダーにとって内容を正しく理解することが難しい<canvas>にrole="img"を指定することで、aria-labelで代替テキストを指定することでスクリーンリーダーがグラフの概要を理解できるようになる- グラフの詳細なデータは
<table>で表示することで、グラフの内容を理解できるようになる
