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:

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