Keyboard Navigation for Scatter Plots

Permalink to "Keyboard Navigation for Scatter Plots"

A line or bar chart has an obvious reading order: left to right along the x axis. A scatter plot does not. Its points are positioned in two dimensions, often overlap, and carry meaning mostly through their distribution — clusters, gaps, outliers, correlation. Keyboard navigation for a scatter plot therefore has to impose an order that makes sense, provide ways to move in both dimensions, and announce enough about each point that users can build the picture sighted users get at a glance.

This page designs that model for SVG and canvas scatter plots. It belongs to canvas & SVG chart keyboard interaction and extends the single-series model in adding keyboard navigation to SVG line charts.

Spec reference

Permalink to "Spec reference"

There is no ARIA chart role with built-in navigation. The robust pattern treats the plot as a composite widget:

  • The plot container (the SVG element or a wrapper around the canvas) is one tab stop, with role="application" avoided in favour of role="group" or role="img" plus a focusable interior.
  • The focused point is exposed either by moving DOM focus to an element per point (SVG) with role="img" and an aria-label, or by aria-activedescendant on the container pointing at hidden per-point elements (canvas).
  • A polite status region can carry extra context such as “Cluster 2 of 3”.

Criteria: SC 1.1.1 Non-text Content (a text alternative describing the plot), SC 2.1.1 Keyboard (point details available by keyboard, not only on hover), SC 1.3.1 (values and groups conveyed programmatically), SC 1.4.11 Non-text Contrast (the focused-point indicator).

One keypress on a scatter plot Flow of a Right Arrow press on a scatter plot: find the next point in x order, move the focus ring, update the tooltip, and announce label and both values. One keypress on a scatter plotRight Arrownext in x orderFind pointsorted index + 1Focus ringdrawn on the pointTooltipvisible label andvaluesAnnounce"Contoso: spend42k, margin 18%"
The x-sorted order gives Left and Right a meaning; Up and Down use nearest neighbours.

When to add point navigation — and when a summary is enough

Permalink to "When to add point navigation — and when a summary is enough"

Add point-by-point navigation when individual points matter: users need to find a specific account, compare two named items, or inspect outliers. With a few dozen to a few hundred labelled points, stepping through is practical.

For plots of thousands of anonymous points, point navigation is noise. What users need is the pattern: “Strong positive correlation; 3 outliers above 90% margin; 2 clusters.” Provide that as a description and a data table, and navigate by cluster rather than by point, if at all — see generating accessible text alternatives for D3 charts.

The misapplication to name is making every point a separate tab stop. A 200-point plot becomes 200 Tab presses between the chart and the rest of the page.

Annotated code example

Permalink to "Annotated code example"
// Scatter plot keyboard model: x-order with Left/Right, nearest vertical neighbour with Up/Down
const pts = data
  .map((d, i) => ({ ...d, i }))
  .sort((a, b) => a.x - b.x || a.y - b.y);     // stable, meaningful order
let cur = 0;

plot.setAttribute('tabindex', '0');
plot.setAttribute('role', 'group');
plot.setAttribute('aria-roledescription', 'scatter plot');   // container only, not a widget
plot.setAttribute('aria-label', 'Accounts: annual spend versus margin, 120 points');
plot.setAttribute('aria-describedby', 'plot-summary');       // SC 1.1.1

plot.addEventListener('keydown', (e) => {
  let next = null;
  if (e.key === 'ArrowRight') next = Math.min(cur + 1, pts.length - 1);
  if (e.key === 'ArrowLeft')  next = Math.max(cur - 1, 0);
  if (e.key === 'Home') next = 0;
  if (e.key === 'End')  next = pts.length - 1;
  if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
    const dir = e.key === 'ArrowUp' ? 1 : -1;
    next = nearestVertical(pts, cur, dir);        // closest point above / below in screen space
  }
  if (e.key === 'PageDown' || e.key === 'PageUp') next = jumpCluster(pts, cur, e.key === 'PageDown' ? 1 : -1);
  if (next === null) return;
  e.preventDefault();
  cur = next;
  focusPoint(pts[cur]);
});

function focusPoint(p) {
  drawFocusRing(p);                                // SC 1.4.11: 3:1 against the plot area
  showTooltip(p);                                  // same text, visible
  // SC 1.3.1: label, both values with units, and group
  live(`${p.label}: spend ${fmtK(p.x)} euros, margin ${p.y}%, ${p.group}.`);
}

function nearestVertical(points, from, dir) {
  const a = points[from];
  let best = null, bestD = Infinity;
  points.forEach((p, j) => {
    if (j === from || Math.sign(p.y - a.y) !== dir) return;
    const d = Math.hypot((p.x - a.x) * X_SCALE, (p.y - a.y) * Y_SCALE);
    if (d < bestD) { bestD = d; best = j; }
  });
  return best ?? from;
}

For SVG plots with fewer than a few hundred points, give each point a <circle> inside a <g role="img" aria-label="…"> and move real focus with a roving tabindex; screen readers then announce the point’s name directly without a live region. For canvas, the live region (or aria-activedescendant on hidden elements) is the only route.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Key Action Expected announcement
Tab to plot Focus plot “Accounts: annual spend versus margin, 120 points, scatter plot” then summary
Right Arrow Next point by x “Contoso: spend 42k euros, margin 18%, Retail.”
Up Arrow Nearest point above “Litware: spend 45k euros, margin 31%, Retail.”
Page Down Next cluster “Cluster 2 of 3: 38 points, spend 80–140k.”
Home / End Lowest / highest x First / last point announced
T (optional) Toggle data table view Table caption announced
Keyboard order laid over a scatter plot Mock grid representing plot regions, with the focused point and its Right Arrow and Up Arrow neighbours numbered. Keyboard order laid over a scatter plotMargin \ Spend0–40k40–80k80–120k30–40%·Litware ●2·20–30%Fabrikam ●·Tailspin ●10–20%·Contoso ●1Northwind ●30–10%Adatum ●··1Focused: Contoso (42k, 18%)2Up Arrow: nearest point above — Litware3Right Arrow: next by spend — Northwind
Right Arrow follows x order; Up Arrow jumps to the closest point above — two different neighbours.

Integration context

Permalink to "Integration context"

The visible tooltip and the announcement should carry the same text, and the tooltip must follow keyboard focus, not only the pointer — see accessible tooltips for chart data points. A data table view gives users who do not want to step through points a way to sort and filter them, as in show-as-table toggles for charts.

For colour-coded groups, the group must be in the announcement and in the visual encoding by shape as well as colour — patterns and shapes for colour-independent charts.

Tab stop per point versus one composite stop Comparison of making every scatter point a separate Tab stop against treating the plot as one stop with arrow-key navigation. Tab stop per point versus one composite stop✗ Every point a Tab stop120 Tab presses to pass the chartOrder is DOM order, often arbitraryNo vertical movementNo way to jump between clusters✓ One composite stopOne Tab in, one Tab outLeft and Right in x orderUp and Down to nearest neighbourPage keys between clusters
One tab stop in, arrows inside — the same contract as every other composite widget.

Gotchas

Permalink to "Gotchas"

Overlapping points. Points with identical or near-identical coordinates are indistinguishable visually and in navigation. Announce “2 points here” and step through them, or jitter them in the navigation order.

Log scales. Announce values in data units, not screen units; a log-scaled axis should still say “spend 1.2 million”.

Hover-only tooltips. If the tooltip appears only on mouseover, keyboard users see the focus ring move with no values. Drive the tooltip from the same focusPoint function.

Design system notes

Permalink to "Design system notes"

A chart library’s scatter component should accept a pointLabel(d) and announce(d) formatter, compute the x order and nearest-neighbour graph once per data change, and draw the focus ring with a design token that meets 3:1 against the plot background in both themes. Cluster navigation can be optional, fed by the same clustering used for the text summary.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
How should keyboard users move through a scatter plot?

Make the plot one Tab stop, then use Left and Right Arrow to step through points ordered by their x value and Up and Down Arrow to jump to the nearest point above or below. Page keys can jump between clusters.

What should be announced for a scatter plot point?

Its label, its x and y values with units, and its group if the plot is grouped — for example “Contoso: spend 42k euros, margin 18 percent, Retail”.

Should every point in a scatter plot be focusable?

Not as separate Tab stops. Use one Tab stop for the plot and arrow keys inside it, so users can pass the chart in one keypress.

What about scatter plots with thousands of points?

Stepping through thousands of points is not useful. Describe the distribution — correlation, clusters, outliers — in text, offer a sortable data table, and navigate by cluster if you provide keyboard access at all.

Permalink to "Related"

← Back to Canvas & SVG Chart Keyboard Interaction