Skip to content

Sonification ​

A sonifier plays the visible series as sound, so people who cannot see the chart can hear its shape, trend and outliers. It adds to the text summary and live regions described under screen readers.

Pitch follows the value: the lowest value in view sounds at the low end of the pitch range and the highest at the high end. Up and down bars also differ in timbre (a pure tone for up, a softer buzz for down) and stereo side (right for up, left for down), so the direction is heard without judging the pitch. Short cues mark bar markers, crossed alert lines and the first bar of a new trading day.

ts
import { createChart, createSonifier } from '@chart-on/core';

const chart = createChart(container);
const sonifier = createSonifier(chart, { durationMs: 5000 });

playButton.addEventListener('click', () => sonifier.toggle());

The sonifier is a separate module: the chart never imports it, so apps that do not call createSonifier do not bundle it. It uses the Web Audio API and needs no assets or network. Importing it is safe on a server: nothing touches the browser until the first play or step.

Starting the sound ​

Nothing sounds until play() or step() runs, and browsers only allow sound after a user gesture, so call them from a click or key handler. The audio context is made on that first call. Where there is no Web Audio, both return false.

Keys ​

With the plot focused (and keyboard on):

KeyDoes
PPlay the visible range, or pause
EscapeStop, while playing or paused
Ctrl+← / →The chart moves the crosshair one bar; the sonifier sounds that bar and reads its values

Any other key, a pointer press or a wheel turn on the chart pauses the sound. Modifier keys pressed on their own (as screen readers do) do not.

The stepped bar's time, open, high, low, close and change are read in a polite live region, for example:

2024-05-09 11:00. Open 158.00, high 159.50, low 157.80, close 159.00, up 1.00 (0.63%).

While the chart's own accessibility.crosshairReadout is on, the sonifier leaves the reading to it.

The words come from the chart's localization messages (sonifyBar, sonifyPoint, sonifyValue, the sonifyCue* keys and describeUp / describeDown / describeUnchanged), so they follow the chart's language.

Options ​

OptionDefaultMeaning
seriesmain seriesThe candle, line or histogram series to play
durationMs4000Time to play the whole visible range; each bar gets an equal share, held between 10 and 500 ms
pitchRange{ low: 220, high: 880 }Frequencies (Hz) for the lowest and highest value in view
mode'close''close': one steady tone at the close. 'range': a glide across the bar's low and high, rising on up bars and falling on down bars
volume0.2Loudness, 0 to 1
cuesall true{ markers, alerts, sessions }: which events get a cue
followCrosshairtrueMove the crosshair along with the sound
keyboardtrueThe keys above
announcetrueRead the stepped bar's values
createAudioContextbrowser AudioContextMakes the audio context on first use; tests pass a fake

sonifier.applyOptions(patch) merges into the current options. Equal value steps give equal musical intervals; on a log price scale, equal ratios of value do instead.

valueToPitch(value, { min, max, low, high, log? }) is the mapping on its own, for apps that make their own sound.

Methods and state ​

MemberDoes
play()Play the visible range, oldest bar first, or go on from where pause stopped
pause()Stop the sound and keep the position
stop()Stop the sound and forget the position
toggle()Pause while playing, else play
step(1) / step(-1)Sound the next or previous bar on its own, move the crosshair there and announce it. The first step starts on the newest bar in view
state'idle', 'playing' or 'paused'
positionData index of the bar played last, or null
on('state:change', fn)Subscribe to state changes; returns an unsubscribe function
destroy()Stop, close the audio context and detach

The sonifier registers itself as a chart plugin (named sonifier-<n>), so chart.destroy() destroys it too.

ts
sonifier.on('state:change', ({ state }) => {
  playButton.setAttribute('aria-pressed', String(state === 'playing'));
});