您现在的位置是:网站首页 > 无障碍访问设计文章详情
无障碍访问设计
陈川
【
ECharts
】
34646人已围观
7889字
无障碍访问设计的重要性
无障碍访问设计确保所有用户都能平等地获取和使用数字产品。对于数据可视化库ECharts来说,这意味着图表需要能被屏幕阅读器识别、支持键盘操作、提供足够的颜色对比度等。忽视无障碍设计会导致视障用户、运动障碍用户等人群无法正常使用图表功能。
屏幕阅读器兼容性
ECharts图表默认生成的SVG或Canvas元素对屏幕阅读器不友好。解决方案是通过ARIA属性增强可访问性:
option = {
aria: {
enabled: true,
label: {
description: '这是一个展示2023年季度销售额的柱状图。第一季度销售额为43万元,第二季度为82万元,第三季度为65万元,第四季度为97万元。'
}
},
xAxis: {
type: 'category',
data: ['第一季度', '第二季度', '第三季度', '第四季度'],
axisLabel: {
aria: {
enabled: true,
label: {
description: '季度'
}
}
}
},
yAxis: {
type: 'value',
axisLabel: {
aria: {
enabled: true,
label: {
description: '销售额(万元)'
}
}
}
},
series: [{
data: [43, 82, 65, 97],
type: 'bar',
itemStyle: {
aria: {
enabled: true,
label: {
description: function(params) {
return `${params.name}销售额:${params.value}万元`;
}
}
}
}
}]
};
键盘导航支持
为图表添加键盘交互需要监听键盘事件并实现焦点管理:
myChart.getZr().on('keydown', function(e) {
const keyCode = e.event.keyCode;
// 左右箭头切换数据项
if (keyCode === 37 || keyCode === 39) {
const currentIndex = highlightedIndex || 0;
const newIndex = keyCode === 37 ? Math.max(0, currentIndex - 1) : Math.min(data.length - 1, currentIndex + 1);
highlightDataPoint(newIndex);
}
// 回车键触发工具提示
else if (keyCode === 13 && highlightedIndex !== null) {
showTooltip(highlightedIndex);
}
});
function highlightDataPoint(index) {
myChart.dispatchAction({
type: 'downplay',
seriesIndex: 0
});
myChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: index
});
highlightedIndex = index;
}
颜色对比度与模式
确保图表有足够的颜色对比度,并提供高对比度模式:
- 使用WCAG 2.1 AA标准(至少4.5:1的对比度)
- 避免仅靠颜色传达信息
- 提供高对比度主题选项
// 高对比度配色方案
const highContrastColors = [
'#000000', // 黑色
'#FFFFFF', // 白色
'#FF0000', // 红色
'#00FF00', // 绿色
'#0000FF' // 蓝色
];
function applyHighContrastTheme() {
myChart.setOption({
color: highContrastColors,
backgroundColor: '#FFFFFF',
textStyle: {
color: '#000000'
},
grid: {
borderColor: '#000000'
}
});
}
动态内容更新通知
当图表数据动态更新时,需要通知辅助技术用户:
function updateChart(newData) {
myChart.setOption({
series: [{
data: newData
}]
});
// 创建隐藏的ARIA实时区域
const ariaLiveRegion = document.getElementById('chart-aria-live') ||
document.createElement('div');
ariaLiveRegion.id = 'chart-aria-live';
ariaLiveRegion.setAttribute('aria-live', 'polite');
ariaLiveRegion.setAttribute('aria-atomic', 'true');
ariaLiveRegion.style.position = 'absolute';
ariaLiveRegion.style.left = '-9999px';
document.body.appendChild(ariaLiveRegion);
// 更新通知内容
ariaLiveRegion.textContent = `图表已更新,最新数据点值为:${newData[newData.length - 1]}`;
}
可缩放矢量图形(SVG)的优化
当使用SVG渲染时,可以添加额外的无障碍属性:
option = {
renderer: 'svg',
graphic: {
elements: [{
type: 'text',
style: {
text: '重要数据趋势',
fontSize: 16,
fill: '#333'
},
left: 'center',
top: 10,
extra: {
'aria-label': '图表标题:重要数据趋势',
role: 'heading',
'aria-level': '2'
}
}]
}
};
复杂图表的描述策略
对于包含多个系列的复杂图表,需要分层级提供描述:
option = {
aria: {
enabled: true,
label: {
general: {
description: '这是一个包含销售额和利润的双Y轴组合图表,横轴表示时间。'
},
series: [
{
description: '蓝色柱状图表示每月销售额,单位为万元。'
},
{
description: '红色折线图表示每月利润率,单位为百分比。'
}
]
}
}
};
触摸设备兼容性
确保图表在触摸设备上也能无障碍操作:
myChart.on('touchstart', function(params) {
// 放大点击区域
if (params.componentType === 'series') {
const point = params.dataIndex;
highlightDataPoint(point);
// 振动反馈
if (navigator.vibrate) {
navigator.vibrate(50);
}
}
});
// 增加点击区域
myChart.getZr().on('click', function(params) {
const pointInPixel = [params.offsetX, params.offsetY];
const pointInGrid = myChart.convertFromPixel('grid', pointInPixel);
const dataIndex = myChart.getModel().getSeries()[0].coordinateSystem.dataToPointIndex(pointInGrid[0]);
highlightDataPoint(dataIndex);
});
数据表格替代方案
为图表提供数据表格作为替代访问方式:
function renderDataTable(containerId, chartData) {
const container = document.getElementById(containerId);
let html = '<table class="chart-data-table" aria-label="图表数据表格"><thead><tr><th>类别</th><th>值</th></tr></thead><tbody>';
chartData.forEach(item => {
html += `<tr><td>${item.name}</td><td>${item.value}</td></tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
// 与图表同步
myChart.on('rendered', function() {
const chartData = myChart.getOption().series[0].data;
renderDataTable('chart-data-table', chartData);
});
自定义无障碍控件
添加专门的无障碍控制界面:
<div class="chart-controls" role="group" aria-label="图表控制">
<button id="play-pause" aria-label="播放/暂停动画">▶️</button>
<button id="prev-point" aria-label="上一个数据点">◀</button>
<button id="next-point" aria-label="下一个数据点">▶</button>
<select id="theme-selector" aria-label="选择图表主题">
<option value="default">默认主题</option>
<option value="high-contrast">高对比度</option>
<option value="dark">暗黑模式</option>
</select>
</div>
document.getElementById('play-pause').addEventListener('click', toggleAnimation);
document.getElementById('prev-point').addEventListener('click', showPreviousPoint);
document.getElementById('next-point').addEventListener('click', showNextPoint);
document.getElementById('theme-selector').addEventListener('change', changeTheme);
性能与无障碍的平衡
在实现无障碍功能时需考虑性能影响:
- 延迟加载非关键ARIA属性
- 使用虚拟滚动处理大数据集
- 对频繁更新的内容使用
aria-live="polite"
// 大数据集的无障碍处理
function loadLargeDataSet(data) {
// 初始只加载可见区域数据
const visibleData = data.slice(0, 50);
myChart.setOption({
series: [{
data: visibleData,
progressive: 400,
progressiveThreshold: 3000
}]
});
// 为屏幕阅读器提供摘要
const summary = `共${data.length}条数据,当前显示前50条。使用页面滚动查看更多数据。`;
document.getElementById('data-summary').textContent = summary;
}
多语言支持
无障碍设计需要考虑多语言用户:
const i18n = {
en: {
chartTitle: 'Sales Trend',
xAxis: 'Quarter',
yAxis: 'Amount (10K)',
description: 'Bar chart showing quarterly sales data.'
},
zh: {
chartTitle: '销售趋势',
xAxis: '季度',
yAxis: '金额(万元)',
description: '展示季度销售数据的柱状图。'
}
};
function setLanguage(lang) {
myChart.setOption({
title: {
text: i18n[lang].chartTitle,
aria: {
label: i18n[lang].chartTitle
}
},
aria: {
label: {
description: i18n[lang].description
}
},
xAxis: {
name: i18n[lang].xAxis,
axisLabel: {
aria: {
label: {
description: i18n[lang].xAxis
}
}
}
},
yAxis: {
name: i18n[lang].yAxis,
axisLabel: {
aria: {
label: {
description: i18n[lang].yAxis
}
}
}
}
});
}
用户偏好检测与适应
自动检测用户的无障碍偏好并调整图表:
// 检测高对比度模式
const highContrastMediaQuery = window.matchMedia('(prefers-contrast: more)');
highContrastMediaQuery.addListener(handleContrastChange);
handleContrastChange(highContrastMediaQuery);
function handleContrastChange(e) {
if (e.matches) {
applyHighContrastTheme();
} else {
applyDefaultTheme();
}
}
// 检测减少动画偏好
const reducedMotionMediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
reducedMotionMediaQuery.addListener(handleMotionPreference);
handleMotionPreference(reducedMotionMediaQuery);
function handleMotionPreference(e) {
myChart.setOption({
animation: !e.matches,
animationDuration: e.matches ? 0 : 1000
});
}