Skip to content

[A11Y] [Low] SVG charts and gauges lack accessible descriptions #9

@continue

Description

@continue

Accessibility Issue: Data Visualizations Without Accessible Alternatives

WCAG Level: A
Severity: Medium
Category: Non-text Content (WCAG 1.1.1)

Issue Description

The application includes SVG-based charts, gauges, and data visualizations (score gauges, zone charts, heatmaps) that convey important information visually but lack accessible alternatives for screen reader users.

User Impact

  • Affected Users: Blind users, low vision users
  • Severity: Medium - important data insights are inaccessible

Violations Found

File: src/pages/Insights.tsx

Lines: ~45-66 (ScoreGauge component)

<!-- Current Code -->
function ScoreGauge({ score, size = 120, label, color }: { ... }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}>
      <svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
        <circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="rgba(255,255,255,0.08)" strokeWidth={8} />
        <circle ... />
        <text ... >{score}</text>
      </svg>
      <span>{label}</span>
    </div>
  );
}

Issue: SVG has no accessible name or description. Screen readers may announce nothing or just numbers.

File: src/pages/Insights.tsx

Lines: ~68-92 (ZoneChart component)

<!-- Current Code -->
function ZoneChart({ zones, percentages, totalTimeSec }: { ... }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
      {zones.map((z, i) => (
        <div key={z.zone} ...>
          {/* Visual bar chart */}
        </div>
      ))}
    </div>
  );
}

Issue: Chart data not accessible to screen readers in a meaningful way.

File: src/pages/Analytics.tsx

Lines: ~186-230 (ConsistencyHeatmap component)

<!-- Current Code -->
function ConsistencyHeatmap({ data }: { data: ConsistencyDay[] }) {
  // Visual heatmap grid
  return (
    <div style={{ display: 'flex', gap: '0.15rem' }}>
      {weeks.map((week, wi) => (
        <div key={wi} ...>
          {week.map((day, di) => (
            <div key={di} title={day.date ? `${day.date}: ${day.miles} mi` : ''} ... />
          ))}
        </div>
      ))}
    </div>
  );
}

Issue: Heatmap uses title attribute (not announced reliably) and lacks text alternative.


Recommended Fix

<!-- Fixed Code - ScoreGauge -->
function ScoreGauge({ score, size = 120, label, color }: { ... }) {
  return (
    <div 
      style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem' }}
      role="img"
      aria-label={`${label}: ${score} out of 100`}
    >
      <svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }} aria-hidden="true">
        {/* SVG content */}
      </svg>
      <span aria-hidden="true">{label}</span>
    </div>
  );
}

<!-- Fixed Code - ZoneChart -->
function ZoneChart({ zones, percentages, totalTimeSec }: { ... }) {
  // Generate accessible summary
  const summary = zones.map((z, i) => 
    `Zone ${z.zone} ${z.name}: ${percentages[i]}%`
  ).join(', ');
  
  return (
    <div role="img" aria-label={`Heart rate zone distribution: ${summary}`}>
      {/* Visually hidden summary */}
      <span className="visually-hidden">{summary}</span>
      
      {/* Visual chart - hidden from AT */}
      <div aria-hidden="true">
        {zones.map((z, i) => ( /* visual bars */ ))}
      </div>
    </div>
  );
}

<!-- Fixed Code - ConsistencyHeatmap -->
function ConsistencyHeatmap({ data }: { data: ConsistencyDay[] }) {
  const totalMiles = data.reduce((sum, d) => sum + d.miles, 0).toFixed(1);
  const runDays = data.filter(d => d.miles > 0).length;
  
  return (
    <div>
      {/* Accessible summary */}
      <div className="visually-hidden" role="img" aria-label={`Training consistency: ${runDays} run days, ${totalMiles} total miles over ${data.length} days`}>
        {/* Summary for screen readers */}
      </div>
      
      {/* Visual heatmap hidden from AT */}
      <div aria-hidden="true">
        {/* Heatmap grid */}
      </div>
    </div>
  );
}
/* Add to CSS */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

Changes Made:

  1. Add role="img" and aria-label to container with meaningful description
  2. Hide decorative SVG from screen readers with aria-hidden="true"
  3. Provide text summary of chart data for screen readers
  4. Use .visually-hidden class for accessible summaries

Additional Instances

  • Recharts components in Analytics.tsx (line 128 - ChartCard)
  • Training plan progress bars
  • Weekly mileage visualizations
  • HR efficiency charts

Testing Instructions

  1. Use screen reader to navigate to data visualizations
  2. Verify meaningful summary is announced (not just "image" or nothing)
  3. Data values should be conveyed textually
  4. Test with NVDA/VoiceOver

Resources

Acceptance Criteria

  • All data visualizations have accessible text alternatives
  • Charts convey key data points via aria-label or hidden text
  • Decorative elements hidden from screen readers
  • Complex data has detailed descriptions available
  • Tested with screen reader - data is understandable

Metadata

Metadata

Assignees

No one assigned

    Labels

    accessibilityAccessibility improvementsseverity-mediumMedium severity - notable impactwcag-aWCAG Level A compliance

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions