Live & Streaming Data
MAIDR supports realtime and streaming data updates, enabling accessible sonification and navigation of live-updating visualizations such as stock tickers, sensor dashboards, and live metrics.
Three capabilities work together:
| Capability | What it does |
|---|---|
setData |
Replaces all chart data at once (full refresh) |
appendData |
Appends a single point to a series (streaming) |
Monitor mode (M key) |
Auto-sonifies and announces newly appended points |
Data updates are applied in place: the user's current position, active modes (text, braille, sonification), and keyboard focus are all preserved while axes ranges, navigation, braille, and text descriptions reflect the new data immediately.
Complete runnable demos: examples/live-line.html (streaming line chart), examples/live-candlestick.html (multi-layer simulated stock ticker: candlestick + volume + moving average), and examples/live-coinbase.html (real live market data from Coinbase's public WebSocket feed).
Enabling Live Mode
Add live: true to your top-level maidr object. Optionally set maxWidth to cap the number of visible points per series (a sliding window for streaming):
var maidr = {
id: 'live-sensor',
title: 'Live Sensor Readings',
live: true, // enable live mode (and the 'M' monitor key)
maxWidth: 20, // optional: keep only the latest 20 points per series
subplots: [[{
layers: [{
id: 'sensor-layer',
type: 'line',
axes: { x: { label: 'Tick' }, y: { label: 'Reading' } },
data: [[
{ x: 0, y: 50 },
{ x: 1, y: 52 },
{ x: 2, y: 49 },
]],
}],
}]],
};
Static charts are completely unaffected: in-place updates are opt-in via live: true. On charts without the flag, setData / appendData still store the new data, but it is only picked up the next time the chart is activated (focused), and monitor mode is unavailable.
Script-Tag API: window.maidrLive
When MAIDR is loaded via a <script> tag, the realtime API is available globally as window.maidrLive.
window.maidrLive.appendData(point, options?)
Appends one data point to a chart layer. Returns true when the point was merged and the chart notified.
// Stream a new point into the only chart on the page.
window.maidrLive.appendData({ x: 42, y: 3.14 });
// Target a specific chart, layer, and series (group).
window.maidrLive.appendData(
{ x: 42, y: 3.14 },
{ id: 'live-sensor', layerId: 'sensor-layer', groupIndex: 1 },
);
| Option | Type | Default | Description |
|---|---|---|---|
id |
string | the only registered chart | Which chart to update. Required when multiple charts are on the page. |
layerId |
string | — | Target layer by id. Takes precedence over layerIndex. |
layerIndex |
number | 0 |
Target layer by position within the subplot. |
groupIndex |
number | 0 |
Series index for nested data (e.g. which line of a multiline chart). Passing the current group count starts a new series; appending into an empty layer (data: []) creates the first series automatically. |
subplotRow / subplotCol |
number | 0 / 0 |
Target subplot in multi-panel figures. |
The shape of point matches the layer's data format (see the Data Schema): { x, y } for bar/line/scatter points, a full OHLC object for candlestick, and so on.
Supported layer types: any layer whose data is an array — bar, line (and multiline), scatter, histogram, candlestick, box, smooth, and segmented bar charts. Heatmaps (object-shaped data) do not support appending; use setData instead. Violin KDE layers accept appends structurally, but KDE points are pre-computed density samples — appending raw observations does not recompute the distribution, so prefer setData with freshly computed densities for violins.
Streaming candlesticks (stock ticker)
Candlestick layers stream the same way — append a full OHLC object per candle. While monitoring (M), each new candle's close price is sonified and announced; users can still arrow Up/Down through the open/high/low/close/volatility segments of any candle:
var maidr = {
id: 'live-ticker',
title: 'Live Stock Ticker',
live: true,
maxWidth: 15, // keep the latest 15 candles
subplots: [[{
layers: [{
id: 'ticker-layer',
type: 'candlestick',
axes: { x: { label: 'Time' }, y: { label: 'Price' } },
data: [
{ value: '09:30', open: 100, high: 102.5, low: 99, close: 101.5 },
],
}],
}]],
};
// Each tick: stream one OHLC candle.
window.maidrLive.appendData(
{ value: '09:31', open: 101.5, high: 103, low: 100.5, close: 100.75 },
{ id: 'live-ticker' },
);
trend and volatility are computed by MAIDR from the OHLC values, so they can be omitted from streamed candles.
Multi-layer tickers (like the py-maidr candlestick example with candle + volume + moving-average layers) stream the same way: give each layer an id and append one point per layer per tick. Layer navigation (PageUp/PageDown), x-position sync across layers, and the user's active layer are all preserved across updates; while monitoring, each appended layer's point is announced:
// One tick: stream into all three layers of the same subplot.
window.maidrLive.appendData(candle, { id: 'live-ticker', layerId: 'candle-layer' });
window.maidrLive.appendData(volumeBar, { id: 'live-ticker', layerId: 'volume-layer' });
window.maidrLive.appendData(maPoint, { id: 'live-ticker', layerId: 'ma-layer' });
See the full multi-layer demo at examples/live-candlestick.html.
window.maidrLive.setData(maidr)
Replaces all data for the chart identified by maidr.id. Use this for full refreshes or when the chart structure (e.g. number of points) changes wholesale:
var updated = structuredClone(maidr);
updated.subplots[0][0].layers[0].data = [[
{ x: 100, y: 11 },
{ x: 101, y: 22 },
{ x: 102, y: 33 },
]];
window.maidrLive.setData(updated);
If the figure keeps the same shape (same subplot grid and layer counts), the user's position is preserved and clamped into the new data bounds. If the shape changes, navigation resets to the initial state.
Complete streaming example
<!doctype html>
<html lang="en">
<head>
<script src="maidr.js"></script>
<script>
var maidr = {
id: 'live-sensor',
title: 'Live Sensor Readings',
live: true,
maxWidth: 20,
subplots: [[{
layers: [{
id: 'sensor-layer',
type: 'line',
axes: { x: { label: 'Tick' }, y: { label: 'Reading' } },
data: [[{ x: 0, y: 50 }]],
}],
}]],
};
</script>
</head>
<body>
<svg id="live-sensor" width="640" height="320"><!-- your chart --></svg>
<script>
let tick = 0;
setInterval(() => {
tick += 1;
const point = { x: tick, y: 30 + Math.random() * 40 };
// 1. Redraw your SVG however your charting code does it.
// 2. Push the same point into MAIDR:
window.maidrLive.appendData(point, { id: 'live-sensor' });
}, 1000);
</script>
</body>
</html>
React API
React consumers have two equivalent paths.
1. Update the data prop (declarative)
For charts with live: true, passing a new data prop replaces the chart data in place — the React-idiomatic equivalent of setData:
import { Maidr, type MaidrData } from 'maidr/react';
import { useEffect, useState } from 'react';
function LiveChart() {
const [data, setData] = useState<MaidrData>(initialData); // live: true
useEffect(() => {
const socket = connectToSensor();
socket.onReading = (reading) => {
setData(prev => appendReading(prev, reading)); // produce a new object
};
return () => socket.close();
}, []);
return (
<Maidr data={data}>
<MyChartSvg data={data} />
</Maidr>
);
}
For static charts (no live flag), prop changes keep the existing behavior: the new data is picked up the next time the chart is focused.
2. Imperative helpers (streaming)
setMaidrData and appendMaidrData mirror the script-tag API. Prefer appendMaidrData for streaming: it applies the maxWidth sliding window automatically, whereas prop updates and setMaidrData replace data verbatim:
import { appendMaidrData, setMaidrData } from 'maidr/react';
// Stream a point into the chart with id 'live-sensor'.
appendMaidrData({ x: Date.now(), y: reading }, { id: 'live-sensor' });
// Replace everything.
setMaidrData(updatedMaidrJson);
Using Real Data Feeds
Any realtime source works with the same pattern: receive the provider's payload, translate it to the layer's point shape, and call appendData. The examples/live-coinbase.html demo streams real BTC-USD trades from Coinbase Exchange's public WebSocket feed (no API key required), aggregating them client-side into 10-second OHLC candles:
const ws = new WebSocket('wss://ws-feed.exchange.coinbase.com');
ws.onopen = () => ws.send(JSON.stringify({
type: 'subscribe',
product_ids: ['BTC-USD'],
channels: ['ticker'],
}));
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.type === 'ticker') {
// Aggregate trades into a candle bucket; when the bucket closes:
window.maidrLive.appendData(
{ value: bucketTime, open, high, low, close },
{ id: 'live-btc', layerId: 'candle-layer' },
);
}
};
Notes for real feeds:
- Append closed candles, not in-progress updates. Provider kline streams (e.g. Binance's
@kline_*) emit many updates for the same candle while it forms; onlyappendDatawhen the candle is final (Binance:k.x === true), or aggregate raw trades into your own buckets as the Coinbase demo does. UsesetDataif you want to revise the still-forming candle in place. - Stock (equity) feeds need an API key. Free keyless realtime equity data effectively doesn't exist; providers like Finnhub, Polygon, or Twelve Data offer free-tier keys with WebSocket trade streams that plug into the exact same pattern. Crypto exchange feeds (Coinbase, Kraken; Binance where not geo-blocked) are public and keyless, which is why the demo uses one.
- Bind after the first candle exists. A candlestick layer can't start with
data: []; the demo binds MAIDR via themaidr-dataattribute +maidr:bindchartevent once a seed candle is available (from the provider's REST snapshot or the first closed bucket). Dispatch the event from an HTML element (e.g. a host<div>), not the SVG itself.
Monitor Mode
On live charts (live: true), pressing M toggles monitor mode. While monitoring is on:
- Every newly appended data point is automatically sonified (a tone at the point's pitch) and announced to screen readers (e.g. "Tick is 42, Reading is 3.14").
- The user's current position does not move — they can keep exploring historical data while hearing new points arrive, then jump to the live edge with
Ctrl/Cmd + Right Arrow. - Toggling announces "Monitoring on" / "Monitoring off". On non-live charts, pressing M explains that monitoring is only available for live charts.
| Function | Key (Windows) | Key (Mac) |
|---|---|---|
| Toggle Monitor Mode (live charts) | M | M |
Sliding Window (maxWidth)
For unbounded streams, set maxWidth on the top-level maidr object. When an appendData call pushes a series past maxWidth points, the oldest points are dropped:
- The window applies per series (each line of a multiline chart is capped independently).
- If the user is positioned inside the trimmed series, their cursor follows the same data point as it shifts left (until it falls out of the window, in which case it clamps to the oldest visible point).
setDatais not affected bymaxWidth; it replaces data verbatim.
Behavior Details
- Silent updates: a data update by itself never speaks or plays audio — only monitor mode produces output, and only for appended points. This keeps screen reader users in control.
- Position preservation: updates restore the user's subplot, layer, and point position, clamped into the new data's bounds. This includes series-count changes: a
setDatathat removes the series the user was on clamps the cursor to the nearest remaining series rather than resetting navigation. - Modes: text, braille, sonification, and review modes stay enabled across updates. Braille content refreshes on the next navigation.
- Visual highlight: highlights re-bind on each update by re-querying the layer's
selectors. The selectors must remain stable across re-renders — if your charting library regenerates elements with different classes or ids each frame (common with keyed D3/Vega re-renders), pin a stable class on the data elements and use that inselectors. When the selector matches a different number of elements than data points, highlighting is disabled for that update (everything else keeps working). - Performance: each update rebuilds the chart model from the full data, so the cost scales with total chart size — including all subplots and layers of a multi-panel figure, not just the one receiving the point. For streams faster than a few updates per second, set
maxWidth— it bounds the data (and therefore the per-update cost) regardless of how long the stream runs. - AI chat: chart descriptions and the AI assistant use the latest data as of the question being asked.
Keyboard Controls
See the full Keyboard Controls reference. Keys most relevant to live charts:
| Function | Key |
|---|---|
| Toggle Monitor Mode | M |
| Jump to newest point | Ctrl/Cmd + Right Arrow |
| Replay current point | Space |
| Toggle Sonification | S |
| Toggle Text mode | T |
| Toggle Braille mode | B |