feat: add realtime/streaming data support via setData API (Phase 1)#548
feat: add realtime/streaming data support via setData API (Phase 1)#548jooyoungseo wants to merge 6 commits intomainfrom
Conversation
Implements Phase 1 of issue #536 by adding a `setData` API that allows chart data to be replaced at runtime without unmounting the component. Changes: - Convert `<Maidr>` component to `forwardRef` and expose `MaidrRef` handle with `setData()` method via `useImperativeHandle` - Support prop-driven data updates: changing the `data` prop while the chart is focused transparently rebuilds the Controller - Add `useMaidrController` effect to detect data changes and recreate the Controller while preserving the Redux store - Expose `window.maidrPlots` global registry for script-tag consumers to call `setData()` by plot ID - Export `MaidrRef` type from `maidr/react` public API https://claude.ai/code/session_011ooVhoME1PLoimCg71q9rW
Add a JavaScript binder that extracts data from D3.js-rendered SVG charts and generates the MAIDR JSON schema for accessible chart interaction. Supported chart types: - Bar charts (bindD3Bar) - Line charts, single and multi-line (bindD3Line) - Scatter plots (bindD3Scatter) - Heatmaps (bindD3Heatmap) - Box plots (bindD3Box) - Histograms (bindD3Histogram) The binder leverages D3's __data__ property on DOM elements to extract bound data, and generates CSS selectors for SVG highlighting. Available as both ES module (maidr/d3) and UMD script tag (dist/d3.js). Closes #537 https://claude.ai/code/session_01XrcqYMsCcxfUwhLDpzEwr3
PR Review: feat: add realtime/streaming data support via setData API (Phase 1)The overall design is sound — using Bugs / Correctness1. No screen-reader announcement when data updates mid-focus This is the most important issue for an accessibility-focused library. When Consider firing a dedicated announcement (e.g., "Chart data updated") via 2. // useMaidrController.ts
useEffect(() => {
if (prevDataRef.current === data) // always false when this effect runs
return;
prevDataRef.current = data;
...
}, [data, disposeController]);React only runs a 3.
Performance4. // maidr-component.tsx
const [data, setData] = useState<MaidrData>(dataProp);
useEffect(() => {
setData(dataProp);
}, [dataProp]);This is the derived state from props anti-pattern. When React's recommendation is to update state during render when the prop changes: const [data, setData] = useState<MaidrData>(dataProp);
const [prevDataProp, setPrevDataProp] = useState<MaidrData>(dataProp);
if (prevDataProp !== dataProp) {
setPrevDataProp(dataProp);
setData(dataProp); // prop wins over any previous imperative setData call
}This collapses two renders into one and eliminates the intermediate stale frame. Design / Maintainability5. // index.tsx
maidrPlots.set(maidr.id, handle); // never removedFor the script-tag entry point, plots are typically static so this is likely acceptable for Phase 1. But document this limitation so it is not forgotten for Phase 2. There is also no guard against duplicate IDs — calling 6. window.maidrPlots = maidrPlots; // runs on import
Test Coverage7. No tests for the new behavior The new code path (data-change effect, proxy handle, registry) is complex enough to warrant at least:
The existing E2E specs in Minor / Nits
Summary
The |
… claude/fix-issue-536-zYzVN
PR review fixes: - Make resolveAccessor return T | undefined for type safety - Remove extractBarDataFromDOM (used pixel values as data) - Fix line chart point selector scoping to per-line parent <g> - Replace heatmap string delimiter with nested Map to avoid collisions - Add warning for missing heatmap cells and NaN histogram boundaries - Remove silent try-catch in box plot, use direct undefined checks New chart types: - bindD3Candlestick for OHLC/candlestick charts - bindD3Segmented for stacked, dodged, and normalized bar charts - bindD3Smooth for smooth/regression line charts - Add example HTML files for all new chart types https://claude.ai/code/session_011ooVhoME1PLoimCg71q9rW
Code Review: feat: add realtime/streaming data support via setData API (Phase 1)Overall, this is a well-structured implementation with clear separation of concerns. The forwardRef conversion, proxy handle pattern, and useMaidrController data-change effect are all solid. Below are my findings ordered by severity. Must Fix1. Memory leak in window.maidrPlots (src/index.tsx) Entries are added to maidrPlots when plots initialise but are never removed when they unmount. Over time this leaks both the MaidrRef handle and any React ref closures it holds. The initMaidr function should register a cleanup that calls maidrPlots.delete(maidr.id) on focusout/unmount. Or better: expose a destroy() method on MaidrRef and call it from the unmount path so the caller can also clean up. 2. No tests for new functionality The PR adds ~2,700 lines of new production code but zero tests. Given this is an accessibility library where regressions have direct user impact, the following should be covered:
Should Fix3. Unsafe cast in resolveAccessor (src/d3/util.ts) The line 4. Equal-distribution assumption in bindLine.ts fallback When pointSelector is not provided, the fallback uses 5. generateSelector may not produce a unique selector (src/d3/util.ts) The function walks the DOM using tag names and :nth-child indices. In D3 grouped bar charts with many structural siblings, the generated path can match multiple elements. Consider always anchoring the path with the container's id. Suggestions / Consider6. Derived state double-render in maidr-component.tsx The pattern 7. Rapid data updates in useMaidrController.ts If setData is called in quick succession (e.g., every 100 ms), each call disposes and recreates the Controller synchronously with no debounce. Consider documenting that callers are responsible for throttling, or add an optional debounce parameter. 8. Document data prop vs setData precedence The JSDoc should state explicitly that a prop change clears any imperative override (prop wins). This is the current behaviour but is not documented. 9. generateId collision risk (src/d3/util.ts) Date.now() has millisecond precision; two charts initialising in the same millisecond are only separated by Math.random(). crypto.randomUUID() is available in all target environments and is a zero-cost improvement. What is Done Well
Summary: The core React integration (forwardRef, useImperativeHandle, useMaidrController data-change effect) is sound. Priority items before merging: memory leak in the global registry (#1), adding tests (#2), and fixing the unsafe resolveAccessor cast (#3). The remaining points are lower-priority improvements. |
Bugs fixed: - Fix multi-line extraction when lines share a parent <g> by tracking processed parents to avoid duplicate data - Fix generateSelector returning empty string when element equals container - Add d3.stack() format auto-detection in bindSegmented (handles [y0, y1] array entries with .data property) - Fix bindSmooth legend extraction to use per-line fill values instead of re-querying all points for each line Reliability: - Add CSS.escape() SSR fallback for Node.js environments (Next.js, Gatsby) - Add monotonic counter to generateId() to prevent collisions during synchronous multi-chart initialization - Add warning when queryD3Elements finds no matching elements Code quality: - Extract duplicated axes spread pattern into shared buildAxes() helper (was duplicated across all 9 binders) - Normalize selectors format: single-series binders use string, multi-series (line, smooth) use string | string[] - Fix scatter binder wrapping single selector in unnecessary array https://claude.ai/code/session_011ooVhoME1PLoimCg71q9rW
PR Review: feat: add realtime/streaming data support via setData API (Phase 1)This is a well-structured PR that adds two independent capabilities: (1) an imperative Architecture & DesignStrengths:
Bugs / Issues1. Memory leak in Plots are registered in No removal of the entry when the plot is destroyed -- 2. Selector bug in multi-line path-based extraction ( In the path-based extraction branch (no // bindLine.ts - selectors.push is called once, outside the for-loop
for (const { datum } of lineElements) {
if (lineData.length > 0) {
data.push(lineData);
// Missing: selectors.push(scopeSelector(svg, selector)) here inside the loop
}
}
selectors.push(scopeSelector(svg, selector)); // only one entry, but data may have N entriesThe same issue exists in 3. The 4. No debouncing for rapid data updates The Even a simple leading-edge debounce (e.g., 16ms) or a Code Quality5. const record = datum as Record<string, unknown>;
return record[accessor] as T | undefined;If 6. Module-level
7. Fallback line partitioning assumes equal point counts ( const pointsPerLine = Math.floor(allPoints.length / lineElements.length);This fallback silently produces incorrect data if lines have different numbers of points (very common with real datasets). A Missing TestsThis PR adds 2,807 lines of new logic with zero new tests. Key areas that need coverage:
Minor Observations
Summary
The core idea is sound and the API surface is well-designed. I'd recommend addressing the selector bug and the missing tests before merging, and ideally adding a debouncing strategy given that streaming is the primary motivation for this PR. |
Implements Phase 1 of issue #536 by adding a
setDataAPI that allowschart data to be replaced at runtime without unmounting the component.
Changes:
<Maidr>component toforwardRefand exposeMaidrRefhandle with
setData()method viauseImperativeHandledataprop while thechart is focused transparently rebuilds the Controller
useMaidrControllereffect to detect data changes and recreatethe Controller while preserving the Redux store
window.maidrPlotsglobal registry for script-tag consumersto call
setData()by plot IDMaidrReftype frommaidr/reactpublic APIhttps://claude.ai/code/session_011ooVhoME1PLoimCg71q9rW