Last updated

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), step, 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, only the focused layer's appended point is announced — the other layers' ticks stay silent so overlapping tones never bury the signal:

// 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.

window.maidrLive.navigateTo(target, options?)

Moves a chart's cursor to a position the host chose, and announces it — the inbound half of the onNavigate callback, spelled in the same currency. Hand back a { layerId, row, col } the callback reported, or one of its pointIndices as { layerId, pointIndex }, and the reader lands on that mark: another subplot or another layer if that is where it is, with text, braille, audio and highlight catching up together. It is what lets a click in the host's own chart — a canvas hit-test, a Tableau mark a sighted colleague pointed at — put the reader on the same mark.

// The reader lands on the third bar of layer 'sales'.
window.maidrLive.navigateTo({ layerId: 'sales', row: 0, col: 2 });

// A scatter is addressed by data index: the point at data[7].
window.maidrLive.navigateTo({ layerId: 'dots', pointIndex: 7 }, { id: 'scatter-chart' });

// The host's selection was cleared: withdraw a target the chart is holding.
window.maidrLive.navigateTo(null, { id: 'scatter-chart' });
Option Type Default Description
id string the only registered chart Which chart to move. Required when multiple charts are on the page.

Returns true when a registered chart accepted the target. A chart the reader is inside moves at once. A chart the reader is not focused on — or one on a page that has lost the browser's focus, since the reader would not hear the move — keeps the target and lands on it when they next focus in — so a colleague can point at a mark before the reader arrives — and null withdraws a kept target. A kept target is also discarded when the chart's data changes, since it addressed the figure that data described. The call answers false, and moves nothing, for a layer the figure does not have, a cell off its grid, or a point index the layer does not carry; a rotor mode (grid, point, intersection) is left first, since the target is spelled in the trace's data coordinates.

In-browser AI agents reach the same method. Where the browser offers the experimental WebMCP tools, the maidr_navigate tool calls navigateTo on the reader's behalf, so an agent's move is announced, kept for the next focus-in, or refused exactly as described here. navigateTo itself never moves keyboard focus; the agent's tool does, into the chart, only when it passes focus: true because the reader asked to be taken there, and the kept target is then applied by the focus-in that follows.

window.maidrLive.version and window.maidrLive.features

What the script on the page is, and what it does for a chart that shares a page with other content: a Quarto or R Markdown slide, a site with a search box, a dashboard card. A binding that carries its own copy of the same work — py-maidr's and r-maidr's inline scripts do, for the maidr.js releases before these — reads them to stand its copy down.

window.maidrLive.version;   // the release, such as '4.15.0'
window.maidrLive.features;  // { hostGuards: 1, focusRing: 1 }, frozen

const live = window.maidrLive;
if (live && live.features && live.features.hostGuards >= 1) {
  // maidr.js keeps the page's shortcuts off the chart's keys itself.
}
Feature Since revision 1
hostGuards While a chart has focus, the page's own shortcuts leave its keys alone: reveal.js's slide keys and search plugin (Ctrl/Cmd+Shift+F, unless the reader has bound that key in MAIDR), Quarto's site search, the Escape of a full-screen dashboard card, gitbook's Mousetrap shortcuts, ioslides, slidy, a flexdashboard storyboard's arrow keys and pkgdown's /. Shift+Tab from a chart that is the first thing on a reveal.js, ioslides or slidy slide hands focus to the slide.
focusRing A visible focus ring on the chart and on MAIDR's fields and buttons, !important for a chart bound from the page so a stylesheet that removes outlines does not take it, and themeable through --maidr-focus-ring-color and --maidr-focus-ring-inner.

Each feature is a revision number that only goes up, when what it covers grows, so test with >=; a feature an older build does not have reads as undefined. Only the core script bundle (maidr.js) sets window.maidrLive: the adapter bundles do not, so a page that loads only an adapter bundle shows no flags, and a binding there keeps its own copies. The guards mark what they wrap, and honour the marks the bindings' copies leave, so the two never wrap the same thing twice.

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, appendMaidrData and navigateMaidr 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, navigateMaidr, 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);

// Put the reader on the mark the user just clicked in your own chart.
navigateMaidr({ layerId: 'sales', row: 0, col: 2 }, { id: 'live-sensor' });

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' },
    );
  }
};

A chart drawn with TradingView Lightweight Charts needs none of this wiring: its binder follows every series.update() and series.setData() itself, appending new bars and replacing the figure for revised ones (see Live and Streaming Data).

Notes for real feeds:

Monitor Mode

On live charts (live: true), pressing M toggles monitor mode. While monitoring is on:

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:

Behavior Details

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