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:
|
- Move the compute-state-without-moving pattern into AbstractTrace.getStateAt(row, col), making it the single owner of the temporary cursor mutation (restored in finally) and unit-testable; Controller.announceAppendedPoint now just calls it. - Include trace types in Context's shape signature so a same-count layer type change (e.g. bar -> line) resets navigation instead of restoring a position onto an incompatible data structure. - Discriminate the depth-1 stack rebuild exactly like initializePlotContext (figure state size) instead of a parallel subplot-count heuristic, and document that scope is intentionally preserved on same-shape updates. - Make chat data serialization lazy and shared: ChatService stores the latest Maidr and serializes on the first LLM request after a change (cached), so streaming appends never pay for JSON.stringify; models receive a supplier instead of a serialized string, and tests assert the public getDataJson surface instead of private fields. - Document that LiveDataManager.setData stores the caller's reference (no mutation after handoff), clarify why VIOLIN_KDE is nested data, and drop a redundant aria-hidden default from the demo. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Third review round addressed in 5c98acb: Fixed
Not changed
Verification: 250 unit tests passing (3 pre-existing failures on Generated by Claude Code |
Code Review: feat: add realtime/streaming data support (PR 620)OverviewThis PR implements a full realtime/streaming data pipeline for MAIDR across three phases: full data replacement (setData), incremental streaming (appendData with sliding window), and monitor mode (auto-sonification of live data points via the M key). It is a large, well-structured addition that follows the MVVC architecture and ships with 36 new unit tests and comprehensive documentation. Strengths
Issues and SuggestionsCorrectness1. resolveActiveColShift relies on Subplot.row being the layer index In controller.ts, the comment says: Subplot traces are built as one single-trace row per layer (traces[layerIndex][0]), so the subplot active row IS the layer index. This ties correctness to an internal layout invariant of Subplot. If Subplot ever supports multiple traces per row, this silently breaks sliding-window cursor tracking. Consider exposing an activeLayerIndex getter on Subplot to make the dependency explicit. 2. Verify Figure.dispose() clears the observer list Controller.updateData() calls this.registerObservers() after the new figure is built. The new figure starts empty, so re-registering is correct -- but only if AbstractObservableElement.dispose() actually clears this.observers (or if old-figure references become unreachable). Worth confirming the dispose chain to ensure no stale observer accumulation across high-frequency streaming updates. 3. Depth-1 restore in restoreNavigation -- keep in sync with initializePlotContext For a single-layer chart (atFigureLevel false), only trace is pushed with no subplot on the stack. The existing tests confirm this mirrors initializePlotContext behavior today. A comment cross-referencing initializePlotContext would help ensure the two stay in sync as the codebase evolves. Performance4. structuredClone on every live update for large datasets cloneMaidrData deep-clones the entire Maidr config on every updateData call. At 1Hz (as shown in the demo) this is fine. For users who might push updates at higher rates, consider noting in the docs that maxWidth is the essential mitigation -- it keeps the serialized payload bounded and each clone cheap. Design5. ChatService.getDataJson() is public only for testing The existing test already spies on JSON.stringify to verify laziness, which works without exposing this method. Making it private would avoid coupling the public API to the caching strategy. 6. LiveDataManager.updateStoredData missing ownership caveat in JSDoc setData documents callers must not mutate the object after handing it over but updateStoredData (same contract) does not. Worth adding the same note for consistency. 7. Silent M key at SUBPLOT scope The M key is added to TRACE_KEYMAP and BRAILLE_KEYMAP but not SUBPLOT_KEYMAP. On a multi-panel chart, a user at subplot level pressing M gets no response and no feedback. A notification like Monitor mode is available when navigating a trace would help screen reader users understand why M is silent there. Minor8. Group-out-of-range warning could include current group count The warning [maidr] appendData: no group at index N would be more actionable if it also said chart has M groups, helping callers debug misconfigured streaming pipelines quickly. Test CoverageThe four new test files are solid. Missing coverage worth considering: announceAppendedPoint with cross-layer appends: currently untested at the unit level. The guard (if trace is null) is exercised by E2E but a unit test would confirm the off-by-one cases. useMaidrController hook data.id change path: exercised by E2E; a React Testing Library test would add unit-level confidence. Documentationdocs/LIVE_DATA.md is thorough. Two additions worth considering: Highlight rebinding: the note if your SVG re-renders with the same selectors is brief. For D3/Vega integrations where element keys change on re-render, stable selector requirements are a common gotcha. A short example or note would help. Performance guidance: a note that maxWidth is essential above ~5Hz (to keep clone cost bounded) would help users size their configs. SummaryThis is a high-quality implementation of a complex feature. The core architecture -- immutable merges, in-place model swap via replaceFigure, monitor mode through the existing Command/Observer pipeline -- is sound. The main items to address before merging:
The rest are suggestions rather than blockers. |
- Expose Subplot.activeLayerIndex so the layer-index/trace-row invariant is owned by Subplot instead of assumed in the Controller's sliding-window cursor tracking. - Add a regression test confirming the dispose chain empties observer lists on figure replacement (no stale observer accumulation across streaming updates). - Register the M monitor toggle in SUBPLOT scope too, so multi-panel users get feedback instead of a silent dead key. - Document the reference-ownership contract on updateStoredData and include the group count in the out-of-range append warning. - Docs: add selector-stability guidance for highlight rebinding and per-update cost/maxWidth performance guidance. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Fourth review round addressed in 522098c: Fixed
No change needed
Verification: 251 unit tests passing (3 pre-existing failures on Generated by Claude Code |
|
Code review for PR 620 - realtime/streaming data support. OVERVIEW: The PR adds setData (full replacement), appendData (streaming), and monitor mode. Architecture is sound - rebuilds model from new data through existing constructors, restores navigation at Context level. Good MVVC compliance and test coverage. ISSUES FOUND:
POSITIVES: Minimal blast radius (live:true opt-in), DOM cleanup in Subplot.dispose via highlightValue.remove(), immutable append logic, lazy JSON cache in ChatService, excellent navigation snapshot/restore implementation, good test coverage. SUMMARY: Approve after addressing (1) layerId type unsafety and (3) getStateAt re-entrancy comment. Everything else is minor. Review generated with Claude Code |
- Warn about the re-entrancy hazard in AbstractTrace.getStateAt (safe only while state getters stay synchronous and notification-free). - Document why describeShape excludes per-layer series counts (new-series appends must preserve position; clamping bounds restored indices). - Note per-update cost scaling and the maxWidth mitigation on AppendDataOptions for high-frequency streams. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Fifth review round addressed in 572f23c: Fixed (documentation items)
Not changed, with verification
Verification: 251 unit tests passing (3 pre-existing failures on Generated by Claude Code |
Code Review — feat: add realtime/streaming data supportOverviewSubstantial and well-executed feature PR. The architecture follows MAIDR's MVVC pattern cleanly — Verdict: Approve with minor suggestions. One potential bug worth confirming before merge. Potential Bug —
|
…dges - Type MonitorService's audio/text collaborators as Observer<TraceState>, matching exactly what handleNewPoint feeds them (PlotState-accepting services remain assignable via contravariance). - Sharpen the getStateAt re-entrancy note: safety depends on the whole call chain being synchronous, so timers such as autoplay ticks cannot interleave before the finally-restore. - Docs: note that violin KDE appends do not recompute the density distribution (prefer setData with fresh densities), and that setData removing the user's series clamps the cursor rather than resetting. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Sixth review round addressed in 033fab3: Confirmed (no change needed)
Fixed
Not changed, with reasons
Verification: 251 unit tests passing (3 pre-existing failures on Generated by Claude Code |
Code Review — feat: add realtime/streaming data supportOverview: This PR adds realtime/streaming data support via three phases: Correctness / Potential Bugs1. export interface AppendedPointInfo {
layerId: string; // typed as string...But the source is: layerId: layer.id, // ...but layer.id may be undefined
2. const trace = subplot?.traces[appended.layerIndex]?.[0];For multi-group traces the append info already carries 3. Formatter refresh on every streaming update ( this.formatterService.refresh(maidr);This calls Architecture4. public handleNewPoint(state: TraceState): void {
this.audio.update(state);
this.text.update(state);
}This is a deliberate, well-justified deviation from the Observer pattern (we need to announce without moving the cursor). The 5. window.maidrLive = {
setData: maidr => liveDataManager.setData(maidr),
appendData: (point, options) => liveDataManager.appendData(point, options),
};No guard against an existing Design / API Surface6. This exposes an implementation detail. The tests validate lazy-serialization behavior correctly, but a Tests7. return { empty: false, type: 'trace' } as TraceState;The 8. No test for the
Minor
SummaryThis is a high-quality feature implementation. The immutable data-merge approach in |
…and test - Warn when window.maidrLive is being redefined (script loaded twice or user-defined global clobbered). - Add a ToggleMonitorCommand unit test and inline rationale comments: the traces[layerIndex][0] invariant at the monitor announce site, the deliberate direct update() calls in MonitorService (announce without moving the cursor or fanning out to other observers), the partial-stub intent in the monitor test, and getDataJson's non-stable-API status. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Seventh review round addressed in 1f8a7e5: Fixed
Not changed, with reasons
Verification: 252 unit tests passing (3 pre-existing failures on Generated by Claude Code |
Code Review: feat: add realtime/streaming data supportOverviewThis PR implements a well-designed realtime/streaming data system for MAIDR across three phases: full data replacement ( Architecture Compliance
IssuesMedium:
|
- Note that per-update cost covers all subplots/layers of multi-panel figures, not just the layer receiving the point. - State the no-mutation-after-handoff contract on the public MaidrLiveApi setData JSDoc where script-tag consumers read it. - Make the demo's local-array mutation boundary explicit (never mutate data already handed to MAIDR). https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Eighth review round addressed in 9380ec0: Fixed (documentation)
Not changed, with reasons
Verification: 252 unit tests passing (3 pre-existing failures on Generated by Claude Code |
…ample Candlestick state getters read currentPointIndex/currentSegmentType, which only candlestick's own navigation methods update — so monitor mode announced the cursor's candle instead of the appended one. Candlestick now overrides getStateAt to map and restore those fields, and appendPointToMaidr targets the new candle's close section for announcements (vertical and horizontal layouts). The sliding-window cursor shift also assumed trace rows are series indices; for candlestick (and other flat layers) rows are OHLC sections. AppendedPointInfo now carries nested/trimShift so the shift applies to every section row of a flat layer and is skipped where columns don't index points (horizontal candlesticks). Adds examples/live-candlestick.html (streaming stock ticker with monitor mode), a candlestick section in docs/LIVE_DATA.md, unit tests for candlestick appends, sliding window, and getStateAt, and four end-to-end checks covering the ticker flow. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
Code Review — feat: add realtime/streaming data supportOverall this is a well-architected, impressively thorough feature addition. The design rationale (rebuild-through-constructors rather than making 15 trace classes mutable) is sound, the test coverage is commendable (36 unit + 11 E2E), and the documentation is comprehensive. Below are targeted observations and suggestions. Architecture & DesignStrengths
Concerns
Performance
Correctness / Edge Cases
Security / API Surface
Test CoverageStrengths
Gaps
Minor / Style
Summary
Great feature — the accessibility angle (monitor mode mirroring chart2music) and the documentation completeness are especially appreciated. |
Code Review: feat: add realtime/streaming data supportThis is a substantial, well-engineered feature that implements live/streaming data in a sound architectural way. The core design decision — rebuilding the model through existing constructors and restoring navigation state in Context, rather than making all 15 trace classes mutable — is excellent and keeps the blast radius small for static charts. OverviewThe PR delivers three coordinated capabilities:
The architecture is clean, MVVC layer boundaries are respected, and test coverage is comprehensive (50+ unit tests, 17 E2E checks). Potential Issues1. getStateAt temporary-mutation hazard (medium)AbstractTrace.getStateAt temporarily mutates this.row, this.col, and this.isInitialEntry, relying on JS single-threaded execution to prevent interleaving. The comment documents this well, but the assumption is load-bearing. The autoplay service uses setInterval/setTimeout, but because those are macrotasks they cannot preempt this synchronous block, so the current code is safe. However, any future change that makes state getters async (or adds observer notifications inside them) would silently corrupt the cursor. Consider a guard flag (e.g., _computingStateAt) that throws if set when observers are notified, to catch regressions early. 2. replaceFigure disposes old figure before factory runs (low–medium)If createFigure() throws (e.g., malformed live data), the Context holds a disposed figure and an empty plot stack. The catch in useMaidrController correctly calls disposeController(), so the user recovers on the next focus-in. The disposal-first order is intentional (to remove stale SVG highlight clones before new selectors are queried), but adding a short comment in updateData noting that the error path is handled at the hook level would help future readers. 3. VIOLIN_KDE silently accepted by appendData without a runtime warning (low)Heatmap layers emit a clear console.warn when appended. Violin KDE layers are listed in NESTED_DATA_TYPES so they do not error, but the docs note that appending raw observations does not recompute the distribution. Adding a console.warn (similar to the heatmap one) would make this limitation discoverable at runtime. 4. window.maidrLive unconditionally reassigned (low)The warning fires but the reassignment happens regardless. If a consumer intentionally pre-populated window.maidrLive before the script loaded (e.g., to shim the API in a test environment), it will be silently replaced. A simple guard (window.maidrLive ??= { ... }) or explicit early return would avoid this. Minor Notes
Strengths
SummaryThis is high-quality work. Issues 1–2 are worth addressing before merge: the getStateAt mutation contract is safe today but fragile, and the replaceFigure partial-failure path could leave the context unrecoverable without the current try/catch in the hook. Issues 3–4 are low-risk but easy to fix. Overall the implementation is solid, the architecture choices are sound, and the test coverage is reassuring. |
…tion - Move the MaidrLiveApi interface into the liveData service and re-export it from maidr/react, so TypeScript consumers can type window.maidrLive without importing the side-effectful script-tag entry. - Extract Context.isFigureLevel as the single source of truth for the figure/trace level discrimination used at construction, instruction resolution, and depth-1 stack restoration. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Tenth review round — two items taken in a7550a9, the rest previously settled: Fixed
Previously settled (no change)
Verification: 259 unit tests passing (3 pre-existing on Generated by Claude Code |
Upgrades examples/live-candlestick.html to mirror py-maidr's multilayer candlestick chart — candlestick, volume bar, and moving-average line layers in one subplot — streaming one point into each layer per tick via per-layer appendData(layerId). Adds a mixed-type multilayer unit test covering active-layer and position preservation across streaming updates, documents the per-layer streaming pattern in LIVE_DATA.md, and extends the e2e suite (20 checks) with layer switching, per-layer append reachability, and monitor announcements across layers. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
Code Review — feat: add realtime/streaming data supportOverall: This is a large, well-engineered feature. The architecture choices are sound — rebuilding the model from tested constructors rather than adding per-class mutability, restoring navigation state at the Context level, keeping static charts completely unaffected, and backing everything with 50+ unit tests and 17 Playwright E2E tests. The documentation (inline JSDoc, LIVE_DATA.md, cross-linked references) is exemplary. The findings below are mostly polish and a few structural edge cases that warrant attention before merge. Bugs / Correctness1.
if (window.maidrLive) { ... }
window.maidrLive = { ... };This runs unconditionally at module load. In SSR environments (Next.js, Astro), if (typeof window !== 'undefined') {
if (window.maidrLive) { ... }
window.maidrLive = { ... };
}Given the PR explicitly targets React consumers, this could be a real blocker for SSR-based apps. 2. Zombie figure reference if
this.figure.dispose(); // old figure disposed
const figure = createFigure(); // if this throws → this.figure is dead, unreplaced
this.figure = figure;If 3. Stale JSDoc block in
/**
* Builds the plot context stack for a fresh figure, ... ← intended for initializePlotContext
* @returns The instruction context element
*/
/**
* Whether navigation for this figure starts at figure level ... ← the actual isFigureLevel doc
*/
private isFigureLevel(figure: Figure): boolean {The first block was left over from Architecture / Design4.
* this instance, since state registered here is shared module-wide.
*/
export const liveDataManager = new LiveDataManager();The JSDoc fragment (which appears to be a truncated "Do not export this instance…") contradicts the 5. Other toggle commands omit the 6.
const NESTED_DATA_TYPES: ReadonlySet<TraceType> = new Set([
...
TraceType.VIOLIN_KDE, // ← structurally accepted but semantically wrong
]);The comment and 7. No rate-limiting surface for high-frequency appends Each Test Coverage8. No test for the SSR/module-load scenario There's no test verifying the 9.
Minor
Summary
The SSR Great work on the overall scope, test coverage, and documentation quality. |
…store order The isFigureLevel extraction left initializePlotContext's JSDoc orphaned above the new helper; move it back to its declaration. Also note the inside-out finally ordering in Candlestick.getStateAt for readers. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Eleventh round triaged — fixed in d1dca55/99240db: Fixed
Verified, no change
Verification: 260 unit tests passing (3 pre-existing on Generated by Claude Code |
Code Review: feat: add realtime/streaming data supportOverviewThis is a substantial, well-designed feature PR implementing live/streaming data support across three phases: full data replacement ( Test coverage is excellent. 50+ TDD unit tests covering edge cases (sliding window, candlestick coordinate mapping, shape mismatches, observer cleanup) plus 17 E2E checks give strong confidence. Architecture & DesignStrong compliance with MVVC:
Concern — // src/model/context.ts
} else {
this.scopeContext.clear();
this._instructionContext = this.initializePlotContext(figure);
hotkeys.setScope(this.scope); // <- direct side-effect on keybinding state from model layer
}
Potential Bugs / Risks1. public replaceFigure(createFigure: () => Figure, options = {}): Figure {
const snapshot = this.captureNavigationSnapshot();
this.figure.dispose(); // <- old figure is gone
const figure = createFigure(); // <- if this throws (e.g. bad maidr data)...
this.figure = figure; // <- never reached; this.figure now points to disposed object
this.plotContext.clear(); // <- never reached
...
}The caller in 2. // src/service/liveData.ts
appended: {
...
layerId: layer.id, // typed as string, but MaidrLayer.id may be optional
...
}If 3. The // src/model/abstract.ts
// Re-entrancy hazard: this is only safe because the entire call chain is
// synchronous (no await points)...The documentation is accurate and the current use is safe, but a future maintainer adding an async getter could silently break monitor mode. Consider adding a boolean guard that throws on re-entry, so the invariant is enforced rather than only documented. Code QualityImmutability in Lazy serialization in The Minor: // src/service/liveData.ts
return this.instances.keys().next().value!;Since Performance
Test CoverageThe test suite is comprehensive. Highlights:
Gap: No test for Summary
The Overall this is high-quality, well-tested code that delivers on the feature spec. |
Adds examples/live-coinbase.html: real BTC-USD trades from Coinbase Exchange's keyless public WebSocket feed, aggregated client-side into 10-second OHLC candles and streamed into a candlestick + volume chart via appendData, with a REST-seeded first candle bound through the maidr-data attribute + maidr:bindchart event (dispatched from an HTML host element). Documents the real-feed integration pattern in LIVE_DATA.md: append only closed candles, equity feeds need API keys while crypto exchange feeds are keyless, and bind after the first candle exists. Verified live in a browser against the real feed. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
Code Review: feat: add realtime/streaming data supportOverviewThis PR implements realtime/streaming data support for MAIDR across three phases: full data replacement ( Architecture: StrongThe layering is clean and follows MVVC:
The immutable-append pattern in Issues to AddressMedium:
|
notifyStateUpdate now throws if fired while getStateAt computes state at a temporary cursor position, turning a would-be silent cursor corruption (e.g. a future state getter notifying observers) into an immediate, loud failure. Covered by a unit test along with a new test pinning figure-level (depth-1) position restoration for multi-panel figures. Also surfaces the maxWidth performance guidance on the public appendMaidrData and MaidrLiveApi.appendData JSDoc. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Thirteenth round addressed in 0761eba: Fixed
No change, with reasons
Verification: 262 unit tests passing (3 pre-existing on Generated by Claude Code |
|
PR review posted via Claude Code review agent - see detailed analysis below. |
|
Code Review: feat: add realtime/streaming data support Overview This PR implements realtime/streaming data support across three phases: full data replacement (setData), point streaming (appendData), and monitor mode (auto-sonification via M key). The design rationale of rebuilding the model through existing constructors rather than making all 15 trace classes mutable is sound and avoids a large regression surface. The test suite is thorough (50+ unit tests, 17 E2E checks) and the documentation is excellent. Architecture and MVVC Compliance ✅ The implementation fits cleanly into the existing architecture:
The conversion of private readonly figure to private figure in both Controller and Context is documented with a clear comment. Potential Bugs 1. Type mismatch: layerId may be undefined In src/service/liveData.ts, AppendedPointInfo.layerId is typed as string, but MaidrLayer.id is string | undefined:
If a layer has no id, this silently assigns undefined to a string field. Suggested fix: layerId: layer.id ?? '' 2. Candlestick.getStateAt — pre-super code runs without isComputingStateAt guard isComputingStateAt is set inside super.getStateAt(), but computeIndexAndSegment runs before the super call. If computeIndexAndSegment or a subclass override ever called notifyStateUpdate(), the guard would miss it. A defensive fix is to set the flag at the top of the override before any side-effecting code runs. 3. ws.onmessage without try/catch in the Coinbase example The WebSocket feed can send protocol-level messages (e.g., subscriptions confirmation) whose shape does not match the expected ticker format. JSON.parse(e.data) is unguarded and would throw on malformed input. Wrapping with try/catch or an explicit type-check guard would make the demo more robust. Design Observations 4. Dual-ref tracking in useMaidrController is complex The hook now uses both latestDataRef and previousDataRef to track the data prop, alongside updates flowing from liveDataManager. latestDataRef is updated by both the registration effect AND the data-change effect, while previousDataRef is only updated in the data-change effect. A brief inline comment explaining why both refs are needed (and how they differ) would help future maintainers understand the ownership chain. 5. No built-in rate limiting for high-frequency streams Each appendData call rebuilds the entire chart model (all subplots and layers). At greater than 10 Hz on a multi-layer chart this can become a real bottleneck. This is documented correctly, but a follow-up adding a minimum update interval or batching option to the API would be valuable. 6. isComputingStateAt is protected — accessible to subclasses Since this field is an invariant enforced by notifyStateUpdate() and should only be written by getStateAt, marking it protected lets subclasses accidentally set it to the wrong value. A private field with a protected getter for the guard check would be safer. Minor Issues
Test Coverage ✅ Test coverage is excellent:
The test for notifyStateUpdate firing inside getStateAt (verifying the guard throws loudly) is a great defensive addition. Documentation ✅ docs/LIVE_DATA.md is comprehensive and well-organized. The candlestick streaming section and the Notes for real feeds callouts are particularly useful. The React doc updates and inline API examples in docs/react.md are clean and accurate. Summary
The layerId type mismatch (string vs string|undefined) is the only issue worth fixing before merge. The rest are minor polish or future concerns. Overall this is a well-designed, well-tested addition to MAIDR. |
|
(Deleting the placeholder comment posted earlier — the full review is in the comment above.) |
Raise isComputingStateAt at the top of Candlestick.getStateAt so the structural guard covers the section/index mapping that runs before the base-class call (and is cleared on pre-super throws). Also guard JSON.parse against non-JSON frames in the Coinbase demo. https://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN
|
Fourteenth round addressed in d888328: Fixed
No change, with reasons
Verification: 262 unit tests passing (3 pre-existing on Generated by Claude Code |
Code Review: feat: add realtime/streaming data supportOverviewThis is a substantial, well-designed PR that adds realtime/streaming data support to MAIDR in three phases: full data replacement (setData), point-by-point streaming (appendData), and a monitor mode that auto-sonifies new points without moving the user cursor. The approach of rebuilding the model layer on each update and restoring navigation positions is the right call vs. making all 15 trace classes mutable. Positives
IssuesBug: Missing null-check for optional layer.id in AppendedPointInfosrc/service/liveData.ts in the appendPointToMaidr return block: layer.id is typed as optional (MaidrLayer.id?: string). If a layer without an explicit id is appended to, this field will be undefined at runtime even though AppendedPointInfo.layerId is typed as string. Consider falling back to: layer.id ?? `layer-${layerIndex}` SSR / module-evaluation guard missing on window.maidrLivesrc/index.tsx runs this at module evaluation time: If the bundle is imported in an SSR or test environment where window is not defined, this throws. A simple guard is worth adding: Warning message uses -1 as layer id when layerId lookup failsIn src/service/liveData.ts, when layerId is provided but not found, findIndex returns -1, and the subsequent warning prints "no layer for index -1" instead of the id. It should read Re-entrancy hazard documented but not self-enforcingThe comment in abstract.ts correctly notes the synchronicity assumption for getStateAt, but the isComputingStateAt flag could also block re-entrant calls to getStateAt itself (throw or bail early) so the invariant is enforced by code, not documentation alone. Structural fragility: Subplot.activeLayerIndex relies on a layout invariantThe accessor returns this.row relying on the traces[layerIndex][0] layout. The comment documents this correctly, but it breaks silently if the Subplot layout ever changes. Consider adding a dev assertion: console.assert(this.traces[this.row]?.length === 1, 'activeLayerIndex invariant violated'). Test Coverage Gaps
Code Quality
SummaryHigh-quality, well-tested work that adds a significant capability while respecting the existing architecture. The identified issues are mostly minor. The layer.id null-check and the SSR window guard are the most concrete correctness fixes worth addressing before merge; the others can be follow-ups. |
|
Fifteenth round — one valid item, taken in c3a4192: Fixed
Verified false / already covered
Verification: 263 unit tests passing (3 pre-existing on Generated by Claude Code |
|
test comment from claude bot - please ignore |
Contributing to maidrThank you for your interest in contributing to maidr project! We welcome contributions from everyone. Pre-requisitesTo contribute to this project, you need to have the following installed on your local machine:
Getting StartedInstalling NodeCheck if node is available and above version 20 or higher on your system using the following command: node -vThis command should display the version of node installed on your system. In case this command does not provide the expected version number, please download node from here, depending on your development environment. Installing NPMCheck if NPM is available on your system and above version 10 using the following command: npm -vThis command should display the version of node installed on your system. In case this command does not provide the expected version number, please download npm from here, depending on your development environment. Setting up the Repository:To locally set up the repository, please follow these steps in order:
Setting up the Development environment:Once the above steps are complete, we set up the development environment. The repository comes with the ability to spawn a dev container so that you get all necessary dependancies for the project in an isolated development environment exclusive to maidr and devoid of any interference from local environment configuration. Docker Desktop that was installed as part of the pre-requisites will be used for accessing the spawned dev container. The Dev Container can be used via Visual Studio Code or Github Codespaces To enable this dev container and use it for project development, please follow the steps in order: Accessing Dev Container through Visual Studio Code
Accessing Dev Container through Github Codespaces
Publishing Changes to Forked RepositoryAs contributors, you can publish changes for to your forked repository through the following procedure:
Publishing Changes for ReviewOnce the changes and concerned branch are updated in your remote forked repository, you can prepare them for review by maintainers by the following procedure:
GuidelinesPlease follow these guidelines when contributing to the project:
Code Style and FormattingWe use ESLint to enforce a consistent code style and formatting. Please run TestsWe use Playwright for end-to-end testing. Please refer to the documentation if you are new to Playwright.
Committing Your ChangesWe use conventional commits to maintain a clear and consistent commit history. Here's how to write a conventional commit message: FormatEach commit message should follow this format:
Denoting a Breaking ChangeTo denote a breaking change, include an exclamation mark
Mentioning Issue NumbersIf your commit addresses a specific issue, mention the issue number at the end of the commit message.
Examples
Commit Message LintingWhen you commit your changes, Husky and Commitlint will automatically check your commit messages. If your message does not meet the conventional commit format, the commit will be rejected, and you'll need to modify the message. Maintaining the websiteHere's how to update the MAIDR documentation:
Edit these files for different sections:
npm run docsThis runs the build script which:
npm run docs:serveThis builds the docs and starts a local HTTP server. Alternatively, open
The documentation will automatically deploy when you push to:
React Component DevelopmentMAIDR exposes a reusable Build Commandsnpm run build # Builds both vanilla JS bundle and React library
npm run build:script # Builds only the vanilla JS bundle (dist/maidr.js)
npm run build:react # Builds only the React library (dist/react.mjs + dist/react.d.mts)The React build uses a separate Vite config ( Key Files
Testing with the Test AppA local test app exists at # 1. Build the maidr library first
npm run build
# 2. If the test app doesn't exist yet, create it:
npm create vite@latest test-react-app -- --template react-ts
cd test-react-app
npm install
npm install .. # Install local maidr as a dependency
# 3. If the test app already exists:
cd test-react-app
npm install # Re-links the local maidr package
# 4. Run the test app
npm run devThe test app imports Architecture Notes
Code of ConductPlease note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. |
|
Line 1 |
…-exasrq # Conflicts: # src/service/chat.ts # test/service/chat.test.ts
Code Review: feat: add realtime/streaming data support (#620)Summary: This is a substantial and well-architected feature adding realtime/streaming data support across three phases: full data replacement ( Architecture & DesignStrengths:
Concerns: 1. Type safety gap — In Suggestion: Either type 2. The approach works today and the re-entrancy caveat is documented. A future async path in any state getter would silently violate the invariant (that Suggestion: Acceptable for now. As a belt-and-suspenders measure, consider asserting 3. The override sets Suggestion: A protected Performance
Security
Test CoverageGood overall — unit tests cover key edge cases (empty layer, new series, sliding window, shape changes, type changes). A few gaps worth considering:
Minor Nits
SummaryThis is production-quality work. Architecture is clean, observer/command integration is correct, and test coverage is solid. The main actionable items before merge:
The rest are suggestions, not blockers. |
|
🎉 This PR is included in version 3.70.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
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 (including across layer-type changes, which reset navigation via the shape signature). 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, including initially empty layers and creating a new series atgroupIndex === count; 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 slides (section-aware for candlesticks)layerId, preserving the user's active layer and x-position across ticksPhase 3 — Monitor mode
Maidr.live: trueopt-in flagMonitorService, announcing "Monitoring on/off" (or explaining monitoring is only for live charts)AbstractTrace.getStateAt(row, col), which computes state at a position without notifying observers (now structurally enforced:notifyStateUpdatethrows during computation). Candlesticks announce the new candle's close price (with trend), overridinggetStateAtto map their internal section/point stateToggleMonitorCommand,CommandContext, keymaps)AI chat freshness
ChatService.updateData()stores the latest data and lazily re-serializes on the first LLM request after a change (cached viagetDataJson()), so AI answers reflect the data currently on screen with zero serialization cost on streaming appends.Documentation & examples
docs/LIVE_DATA.md(Live & Streaming Data) with full API reference, script-tag and React examples, a candlestick/stock-ticker section, a real-data-feed integration guide, monitor mode, sliding window, performance guidance, 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.html(simulated sensor stream),examples/live-candlestick.html(simulated multi-layer stock ticker: candlestick + volume + moving average), andexamples/live-coinbase.html(real live market data — BTC-USD trades from Coinbase's keyless public WebSocket feed, aggregated into 10-second candles; requires network)Design 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 incl. candlestick/empty-layer/new-series edges, sliding window, clone semantics, manager registry),test/service/monitor.test.ts,test/model/contextReplaceFigure.test.ts(position preservation/clamping/shape & type changes/multi-layer mixed types/figure-level depth-1/observer cleanup),test/model/traceStateAt.test.ts+test/model/candlestickStateAt.test.ts(cursor-free state computation incl. throw-path restore and the structural no-notify guard),test/service/chat.test.ts(lazy serialization),test/command/toggleMonitor.test.tsdisplay.test.ts/braille-escape.test.tspre-exist onmainsetDatawith position clamping, themaxWidthsliding window, new-series creation, and the full multi-layer candlestick ticker flow (layer switching with x-sync, per-layer appends, monitor announcing the new candle's close price). The Coinbase real-data demo was additionally verified live in a browser against the actual feed.tsc --noEmit, repo-wide ESLint, full 12-bundle production build, and the docs site build all passhttps://claude.ai/code/session_01KuiTyhDAEWtQX9oTKGatzN