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 ofrole="group"orrole="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 anaria-label, or byaria-activedescendanton 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).
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 |
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.
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.
Related
Permalink to "Related"- Keyboard navigation for SVG line charts — the ordered-series case
- Accessible tooltips for data points — showing the focused point
- Show-as-table toggles — the table view