feat: add realtime/streaming data support#620
Conversation
Implements live data updates for accessible visualizations (closes #536): - setData: full data replacement via window.maidrLive.setData() (script tag), setMaidrData() (npm), or the data prop on live React charts. The Controller rebuilds the model layer in place (Context.replaceFigure), preserving the user's navigation position with clamping, and rewires observers without touching services, view models, or keybindings. - appendData: streaming point appends via window.maidrLive.appendData() / appendMaidrData(), with immutable data merging (flat and nested group layers), targeting by subplot/layerId/groupIndex, and a maxWidth sliding window that trims the oldest points while keeping the cursor on the same data point. - Monitor mode: 'M' key (TRACE and BRAILLE scopes) toggles monitoring on charts configured with live: true; newly appended points are auto-sonified and announced to screen readers without moving the user's position. New grammar options: Maidr.live (opt-in) and Maidr.maxWidth. Static charts are unaffected. Includes a LiveDataManager registry routing updates to mounted chart instances, a MonitorService, unit tests for data merging, monitoring, and figure replacement, and a runnable streaming demo (examples/live-line.html). https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
… live updates - Add docs/LIVE_DATA.md user manual covering setData, appendData, the maxWidth sliding window, monitor mode, and complete script-tag and React examples; register the page in the docs site build. - Cross-link live data docs from README, SCHEMA.md (live/maxWidth top-level properties), CONTROLS.md (Monitor Mode key), react.md (Live & Streaming Data section with examples), and llms.txt. - Fix AI chat staleness on live charts: ChatService.updateData() refreshes the serialized chart data held by each LLM model, and the Controller invokes it on every live data update so AI answers reflect the data currently on screen. Covered by unit tests. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
Code Review — feat: add realtime/streaming data supportThis is a well-scoped, ambitious feature. The opt-in High Severity1. Type safety bug:
appended: {
layerId: layer.id, // layer.id?: string | undefined, but layerId: string
...
}
2.
trace.isInitialEntry = false;
trace.row = appended.row;
trace.col = appended.col;
const state = trace.state; // ← could trigger side effects
trace.row = previous.row;
trace.col = previous.col;
trace.isInitialEntry = previous.isInitialEntry;This is a pattern to compute the state at an arbitrary position without moving the user. The try/catch means the restore still runs on throw, but any side effects triggered by Preferred approach: add a Medium Severity3.
const onAppendedTrace = onAppendedSubplot && activeSubplot.row === appended.layerIndex;
4.
if (data.live) {
liveDataManager.setData(data); // calls setData, not appendData
}
data: [[
...(prev.subplots[0][0].layers[0].data as { x: number; y: number }[][])[0],
{ x: Date.now(), y: 30 + Math.random() * 40 },
]],This will grow without bound. Either apply the sliding window inside 5.
For live charts, the Low Severity / Code Quality6. // controller.ts (was readonly)
private figure: Figure;
// context.ts (was readonly)
private figure: Figure;
// highContrast.ts (was readonly)
private figure: Figure;This is architecturally necessary for live updates, but the PR silently drops the 7. export const liveDataManager = new LiveDataManager();The unit tests correctly create fresh 8. Minor: try {
// ...
} catch {
return 0; // no logging
}When figuring out if a trace is the active one fails (e.g., unexpected model state), this silently returns 0 (no shift). This is safe behavior, but a Positive Highlights
SummaryThe two must-fix issues before merge are the |
There was a problem hiding this comment.
Pull request overview
Adds opt-in live/realtime chart updates to MAIDR, enabling full data replacement and streaming point appends while preserving navigation state and supporting “monitor mode” auto-announcements.
Changes:
- Introduces a live data registry + APIs (
window.maidrLive,setMaidrData,appendMaidrData) and a model-swap mechanism (Context.replaceFigure+Controller.updateData) to rebuild the model in place. - Adds Monitor Mode (key M) via a new
MonitorServiceandToggleMonitorCommand, plus keymap/docs updates. - Updates ChatService to refresh LLM chart context on live updates, and adds docs, examples, and Jest tests for the new behavior.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/service/monitor.test.ts | Adds unit tests for MonitorService toggling and appended-point handling. |
| test/service/liveData.test.ts | Adds unit tests for append/set behavior, sliding window, and LiveDataManager registry. |
| test/service/chat.test.ts | Adds unit test ensuring chat model JSON refreshes on update. |
| test/model/contextReplaceFigure.test.ts | Adds unit tests for navigation-state preservation/clamping during figure replacement. |
| src/type/grammar.ts | Extends MAIDR schema with live?: boolean and maxWidth?: number. |
| src/state/hook/useMaidrController.ts | Registers charts with LiveDataManager; wires React prop changes into live updates. |
| src/service/monitor.ts | Implements MonitorService (toggle + auto announce/sonify for appended points). |
| src/service/liveData.ts | Implements LiveDataManager, cloning helper, and append merge logic + sliding window. |
| src/service/keybinding.ts | Adds M binding (TOGGLE_MONITOR) for TRACE/BRAILLE scopes. |
| src/service/highContrast.ts | Makes figure replaceable and reapplies high-contrast after live updates. |
| src/service/formatter.ts | Adds formatter refresh hook after live data replacement. |
| src/service/chat.ts | Adds ChatService.updateData() and LLM model data refresh plumbing. |
| src/react-entry.ts | Re-exports live data helpers/types for React consumers. |
| src/model/plot.ts | Ensures subplot highlight clones are removed on dispose (avoids DOM accumulation on live updates). |
| src/model/context.ts | Adds replaceFigure() with navigation snapshot/restore and optional sliding-window shift. |
| src/index.tsx | Exposes window.maidrLive script-tag API for set/append live updates. |
| src/controller.ts | Adds updateData() model swap + observer rewire; wires monitor mode + appended-point announcement. |
| src/command/toggle.ts | Adds ToggleMonitorCommand. |
| src/command/factory.ts | Wires TOGGLE_MONITOR to ToggleMonitorCommand. |
| src/command/command.ts | Adds monitorService to CommandContext. |
| scripts/build-site.js | Registers “Live & Streaming Data” page in docs site build. |
| README.md | Adds Live & Streaming Data section and TOC entry. |
| examples/live-line.html | Adds runnable streaming demo using window.maidrLive.appendData. |
| docs/SCHEMA.md | Documents live/maxWidth schema properties and links to live guide. |
| docs/react.md | Documents React live updates via prop replacement and imperative helpers. |
| docs/llms.txt | Adds link to live/streaming documentation. |
| docs/LIVE_DATA.md | Adds full user guide for setData/appendData/monitor mode/sliding window. |
| docs/CONTROLS.md | Documents Monitor Mode keybinding and behavior. |
- Detect nested-data layers by trace type so appending into an initially empty layer (e.g. line with data: []) produces the correct LiveDataPoint[][] shape; a groupIndex equal to the current group count now starts a new series (covered by new unit and e2e tests). - Gate in-place live updates on live: true everywhere: setData/appendData on static charts now only store the new data for the next focus-in, matching the documented opt-in contract. Failed swaps dispose the controller so the next focus-in rebuilds cleanly. - Keep MonitorService's live flag in sync when a data update flips Maidr.live (turning it off also stops monitoring). - Serialize chart data once in ChatService.updateData and share the JSON string across LLM providers instead of stringifying per model. - Support selective bundle builds (node scripts/build.js core react) without emptying dist. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
Code Review — PR #620: feat: add realtime/streaming data supportOverviewThis is a substantial, well-conceived feature (2 171 additions, 32 deletions, 29 files) that adds realtime/streaming data support across three tiers: full The test coverage is solid: 36 new unit tests written red-first, 11 Playwright E2E checks, and the full Jest suite still passing. IssuesBug / Correctness1. Orphaned JSDoc comment in Lines 1437–1439 contain a JSDoc comment ( Fix: attach the first comment to 2. Temporary mutation in // src/controller.ts – announceAppendedPoint
trace.isInitialEntry = false;
trace.row = appended.row;
trace.col = appended.col;
const state = trace.state; // ← if this throws…
trace.row = previous.row; // …these three lines never run
trace.col = previous.col;
trace.isInitialEntry = previous.isInitialEntry;The try {
trace.isInitialEntry = false;
trace.row = appended.row;
trace.col = appended.col;
const state = trace.state;
this.monitorService.handleNewPoint(state);
} catch (error) {
console.warn('[maidr] Failed to announce appended data point:', error);
} finally {
trace.row = previous.row;
trace.col = previous.col;
trace.isInitialEntry = previous.isInitialEntry;
}Architecture / Design3.
Consider either: (a) skipping the keybinding registration when 4. public replaceFigure(createFigure: (() => Figure) | Figure, …)The union type is used only in tests (passing a pre-built Figure directly). Production code always passes a factory. Mixed-type overloads complicate the signature and blur the documented reason for using a factory (old figure disposed before the new one is created). The test helper should construct via a factory, or 5. Module-level // src/service/liveData.ts
export const liveDataManager = new LiveDataManager();This singleton is shared across every test file that imports from Code Quality6. return this.instances.keys().next().value as string;
7. Brittle private-field access in const models = (service as unknown as { models: Record<string, { json: string }> }).models;This reaches into the private implementation to read 8. The function handles a subtle edge case (peeling off Minor / Nits
SummaryThe implementation is architecturally sound, well-tested, and follows MAIDR's MVVC conventions. The two correctness items (#1 orphaned JSDoc, #2 Great work overall — this is a complex feature delivered cleanly. |
- Restore the trace cursor in a finally block in announceAppendedPoint so a throwing state getter cannot leave the cursor mutated. - Attach the LiveDataPoint JSDoc to its declaration (was orphaned above NESTED_DATA_TYPES). - Tighten Context.replaceFigure to accept only a factory, keeping the dispose-before-construct ordering guarantee explicit; tests updated. - Document the layer-index/trace-row 1:1 mapping in resolveActiveColShift and log instead of silently swallowing errors there. - Mark fields that intentionally lost readonly (controller/context/ highContrast figure) as mutable-by-design, note that tests should construct their own LiveDataManager, and replace the resolveId cast with a non-null assertion. - Add cloneMaidrData unit tests (deep clone semantics, onNavigate preservation). - Docs: bound the declarative React streaming example with an explicit window and steer streaming users to appendMaidrData (which applies maxWidth automatically); clarify the demo's parallel SVG point copy. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Addressed both review rounds in 7fa8177 (and the earlier Copilot threads in 7d8668d). Summary: Fixed
Not changed, with reasons
Verification: 243 unit tests passing (3 failures pre-exist on Generated by Claude Code |
Code Review — PR #620: Realtime/Streaming Data SupportOverall: This is a well-designed, substantial feature that follows MAIDR's MVVC architecture cleanly. The opt-in Potential Bugs1. Temporary trace mutation in trace.isInitialEntry = false;
trace.row = appended.row;
trace.col = appended.col;
const state = trace.state;
this.monitorService.handleNewPoint(state);Mutating A safer alternative: add a 2. private describeShape(figure: Figure): string {
return figure.subplots
.map(row => row.map(subplot => subplot.getSize()).join(','))
.join(';');
}
3. The Code Quality4. For high-frequency streaming the serialization cost accumulates. Lazy serialization (compute the string on the first LLM request after a data change, not on each append) would be more efficient and is a straightforward change: store 5. Test accesses private field via type-cast ( const models = (service as unknown as { models: Record<string, { json: string }> }).models;This ties the test to the private 6. instance.data = maidr; // stores the reference
instance.listener({ maidr });The stored 7. When the shape matches, Architecture Notes8. The PR correctly comments this with 9. This is excellent — accepting interfaces rather than concrete service types keeps Minor
Test Coverage Gaps
Summary: The core design is solid and well-tested. The three highest-priority items to address before merge are:
|
Summary
Implements realtime/streaming data support as proposed in #536, covering all three suggested phases plus user documentation.
Closes #536
What's included
Phase 1 —
setData(full data replacement)window.maidrLive.setData(maidrJson)setMaidrData()frommaidr/react, or simply pass a newdataprop on charts configured withlive: trueContext.replaceFigure()swaps the Figure/Subplot/Trace model in place. The old figure is disposed first (removing stale SVG highlight clones before the new model re-queries selectors), the navigation stack is rebuilt at the same depth, and the user's position is silently restored with clamping. Services, view models, and keybindings are untouched, so updates are cheap enough for streaming.Phase 2 —
appendData(streaming)window.maidrLive.appendData(point, { id?, layerId?, layerIndex?, groupIndex?, subplotRow?, subplotCol? })/appendMaidrData()LiveDataManagerregistry routes updates to mounted chart instances and merges points immutably — flat layers (bar, scatter, histogram, candlestick, box) and nested group layers (line, smooth, segmented) are both supported; heatmaps are rejected with a warningMaidr.maxWidthoption implements the sliding window: oldest points are trimmed on append, and the cursor follows the same data point as the window slidesPhase 3 — Monitor mode
Maidr.live: trueopt-in flagMonitorService, announcing "Monitoring on/off" (or explaining monitoring is only for live charts)ToggleMonitorCommand,CommandContext, keymaps)AI chat freshness
ChatService.updateData()refreshes the serialized chart data held by each LLM model on every live update, so AI answers reflect the data currently on screen.Documentation
docs/LIVE_DATA.md(Live & Streaming Data) with full API reference, script-tag and React examples, monitor mode, sliding window, and behavior details; registered in the docs site buildREADME.md,docs/SCHEMA.md(live/maxWidthproperties),docs/CONTROLS.md(Monitor Mode key),docs/react.md(Live & Streaming Data section), anddocs/llms.txtexamples/live-line.htmlDesign rationale
Rather than making all 15 trace classes mutable with cache invalidation (the high-risk path flagged in the issue's barrier table), updates rebuild the model layer through the existing, tested constructors and restore navigation state at the Context level. This works uniformly for every chart type, reuses the Observer wiring as suggested in the issue, and leaves static charts completely unaffected (live features are opt-in).
Testing
test/service/liveData.test.ts(data merging, sliding window, manager registry),test/service/monitor.test.ts,test/model/contextReplaceFigure.test.ts(position preservation/clamping/shape changes),test/service/chat.test.tsdisplay.test.ts/braille-escape.test.tspre-exist onmainsetDatawith position clamping, and themaxWidthsliding windowtsc --noEmit, repo-wide ESLint, full 12-bundle production build, and the docs site build all passhttps://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
Generated by Claude Code