Sonifying Time-Series Charts
Permalink to "Sonifying Time-Series Charts"Sonification turns data into sound. For a time-series chart, the simplest mapping plays each point as a short tone whose pitch rises with the value, one after another along the x axis. A two-second sweep conveys the shape of a line — a steady climb, a dip in March, a spike, a plateau — faster than reading twelve values, and in a way that text summaries struggle to match for irregular patterns.
It is a supplement, not an alternative on its own: users cannot recover exact values from pitch, and some users cannot hear it or do not want it. This page covers a Web Audio implementation, the controls it needs, and where it fits among other alternatives. It belongs to data visualization & chart alternatives.
Spec reference
Permalink to "Spec reference"The Web Audio API provides AudioContext, OscillatorNode and GainNode, enough to schedule short tones precisely. Browsers require a user gesture before an AudioContext can produce sound, which conveniently enforces the most important accessibility rule: no autoplay.
WCAG does not require sonification. The relevant criteria constrain it: SC 1.4.2 Audio Control (audio that plays for more than 3 seconds must be pausable or have volume control independent of the system) — satisfied by starting only on request and providing pause; SC 2.1.1 Keyboard for the controls; SC 1.1.1, which the text summary and table satisfy, not the sound. There is no ARIA role for sonification; the controls are ordinary buttons.
When sonification helps — and when not to
Permalink to "When sonification helps — and when not to"It helps for trend-heavy data where shape matters more than exact values: time series, sparklines, a sensor stream, a price history. Screen reader users who have used audio graphs describe them as a fast way to decide whether a chart is worth exploring point by point.
It does not help for categorical comparisons (a bar chart of regions), for precise values, or for users who are deaf or hard of hearing, in noisy environments, or who simply prefer text. It is always an addition to the text summary and data table, never a replacement.
The misapplication to name is playing the sonification automatically when the chart receives focus. It competes with the screen reader’s own speech, cannot be stopped quickly by users who did not expect it, and — past three seconds — fails SC 1.4.2.
Annotated code example
Permalink to "Annotated code example"<div class="sonify" role="group" aria-label="Listen to Orders per month">
<!-- SC 1.4.2: user-initiated; the button toggles play and pause -->
<button type="button" id="play" aria-pressed="false">Play as sound</button>
<label for="speed">Speed</label>
<select id="speed"><option value="0.25">Slow</option><option value="0.15" selected>Normal</option><option value="0.08">Fast</option></select>
<p class="hint" id="sonify-hint">Low pitch is fewer orders, high pitch is more. Plays January to December.</p>
</div>
let ctx, playing = false, stopAt = 0;
const LOW = 220, HIGH = 880; // two octaves, comfortable range
function pitch(v, min, max) { // log mapping sounds more even
const t = (v - min) / (max - min || 1);
return LOW * Math.pow(HIGH / LOW, t);
}
async function play(values, step) {
ctx ??= new AudioContext(); // created on the click: no autoplay
await ctx.resume();
const min = Math.min(...values), max = Math.max(...values);
const t0 = ctx.currentTime + 0.1;
values.forEach((v, i) => {
const osc = ctx.createOscillator(), gain = ctx.createGain();
osc.type = 'triangle'; // softer than square or sawtooth
osc.frequency.value = pitch(v, min, max);
gain.gain.setValueAtTime(0, t0 + i * step);
gain.gain.linearRampToValueAtTime(0.2, t0 + i * step + 0.02); // no clicks, not loud
gain.gain.linearRampToValueAtTime(0, t0 + i * step + step * 0.9);
osc.connect(gain).connect(ctx.destination);
osc.start(t0 + i * step); osc.stop(t0 + (i + 1) * step);
});
stopAt = t0 + values.length * step;
// Announce the frame of reference before and after (SC 1.1.1 companion)
announce(`Playing ${values.length} months. Starts at ${fmt(values[0])}.`);
setTimeout(() => { if (playing) finish(values); }, (stopAt - ctx.currentTime) * 1000);
}
function finish(values) {
playing = false;
document.getElementById('play').setAttribute('aria-pressed', 'false');
announce(`Ended at ${fmt(values.at(-1))}. Highest ${fmt(Math.max(...values))} in November.`);
}
document.getElementById('play').addEventListener('click', async (e) => {
const btn = e.currentTarget;
if (playing) { await ctx.suspend(); playing = false; btn.setAttribute('aria-pressed', 'false'); return; }
playing = true; btn.setAttribute('aria-pressed', 'true');
play(orders, Number(document.getElementById('speed').value));
});
Pair playback with point stepping: when a user arrows through the chart as in adding keyboard navigation to SVG line charts, play that point’s tone alongside its announced value. Users then connect pitch to numbers, which makes the full sweep more meaningful.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Expected behaviour | Announcement |
|---|---|---|
Tab to Play |
Button focused | “Play as sound, toggle button, not pressed” |
Enter |
Tones play January to December | “Playing 12 months. Starts at 820.” |
Enter again |
Playback pauses | “pressed” → “not pressed” |
| Playback ends | Button resets | “Ended at 1,300. Highest 1,410 in November.” |
| Arrow through chart points | Each point’s tone plays with its value | “March: 760 orders” + tone |
Integration context
Permalink to "Integration context"The text summary remains the primary alternative; see generating accessible text alternatives for D3 charts. Sonification is especially effective for sparklines on KPI cards, where there is no room for a full table and a one-second sweep says “rising” or “volatile” — see accessible KPI cards and sparklines.
Multi-series charts can play series one after another, or use stereo panning to place series left and right; keep the default to one series at a time, as simultaneous tones are hard to separate.
Gotchas
Permalink to "Gotchas"Volume. Keep gain low and ramp every note to avoid clicks. Users may have their screen reader loud; a sudden full-scale tone is painful.
Zero-range data. A flat series maps every value to the same pitch; say “flat at 820” instead of playing a drone.
Background tabs. AudioContext keeps playing in background tabs. Suspend it on visibilitychange, as with live announcements in pausing live regions when the tab is hidden.
Design system notes
Permalink to "Design system notes"Offer sonification as an opt-in feature of the chart component — a sonify flag that adds the control group and wires point stepping to tones — with one shared audio module that enforces the pitch range, gain ceiling and suspend-on-hidden behaviour. Keep it off by default in dashboards with many charts, and let users enable it globally in their preferences.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"What is chart sonification?
Representing data as sound — typically playing each point of a line chart as a tone whose pitch rises with its value, in order along the x axis — so listeners hear the shape of the data.
Does sonification make a chart WCAG compliant?
No. WCAG is met by text alternatives such as a summary and a data table. Sonification is an optional supplement that helps some users grasp trends quickly.
Can a sonification play automatically?
It should not. Start it only when the user asks, provide pause, and remember that audio longer than three seconds must be pausable under SC 1.4.2.
Which charts are good candidates for sonification?
Time series and other ordered data where the shape matters — trends, peaks, dips, volatility. It adds little to categorical comparisons or where exact values matter.
Related
Permalink to "Related"- Text alternatives for D3 charts — the primary alternative
- Keyboard navigation for SVG line charts — stepping points with sound
- KPI cards and sparklines — a short trend to play