feat: add Chart.js charting library support (#538)#554
feat: add Chart.js charting library support (#538)#554jooyoungseo wants to merge 3 commits intomainfrom
Conversation
Add a Chart.js plugin that automatically makes canvas-based charts accessible through MAIDR's audio sonification, text descriptions, braille output, and keyboard navigation. Key changes: - New `maidr/chartjs` entry point with `maidrPlugin` and `extractMaidrData` - Data extraction for bar, line, scatter, pie, and doughnut chart types - Canvas highlighting via Chart.js `setActiveElements` API bridge - `onNavigate` callback on Maidr type for canvas-based highlight integration - Navigate callback observer registration in Controller - Vite build config for the Chart.js entry (`vite.chartjs.config.ts`) - chart.js v4+ as optional peer dependency https://claude.ai/code/session_01JWjCS4jngMUYvbcqRq7Qbn
PR Review: feat: add Chart.js charting library support (#538)This is a well-structured addition that opens MAIDR accessibility to a large ecosystem of canvas-based charts. The architecture (plugin → extractor → controller bridge) is clean and the documentation comments are thorough. Below are issues that should be addressed before merge. Bug: Wrong row/col values in
|
| Severity | Issue |
|---|---|
| Bug | Wrong row/col values from panning in navigate callback |
| Bug | Object-typed dataset values silently become 0 in bar extraction |
| Architecture | onNavigate mutates data schema object; pollutes public type |
| Correctness | Multi-dataset bar chart highlight not cleared on layer switch |
| UX | No support for chart data updates after initialization |
| Safety | No guard against duplicate React roots in initMaidrForChart |
| Safety | Unsafe plugin options cast from unknown |
| Minor | Fragile canvas DOM adoption via ref callback |
| Minor | Non-unique Date.now() fallback for chart ID |
| Missing | Tests for extraction and plugin lifecycle |
…dapter - Fix navigate callback to use braille.row/col instead of audio.panning (audio panning values are orientation-dependent and unreliable) - Fix silent data loss for object-typed bar values in extractor - Add duplicate React root guard in plugin - Replace unsafe cast with validated plugin options helper - Pass onNavigate callback at construction instead of mutating data - Use monotonic counter for unique IDs instead of Date.now() - Add stacked/dodged bar detection and SegmentedPoint extraction - Add radar, polarArea chart extraction (mapped to bar) - Add bubble chart extraction (mapped to scatter) - Add boxplot extraction (chartjs-chart-boxplot plugin) - Add candlestick/ohlc extraction (chartjs-chart-financial plugin) - Add heatmap/matrix extraction (chartjs-chart-matrix plugin) - Add horizontal bar support via indexAxis detection - Expand ChartJsDataValue type to cover all plugin data shapes https://claude.ai/code/session_01JWjCS4jngMUYvbcqRq7Qbn
Code ReviewGreat addition. Chart.js integration fills an important gap for canvas-based accessibility. The overall architecture is clean and using Chart.js native plugin lifecycle hooks is the right approach. Below are findings organized by severity. High Priority1. controller.ts — Navigate callback uses braille state for position
if (!state.empty && !state.braille.empty) {
callback({
layerId: state.layerId,
row: state.braille.row,
col: state.braille.col,
});
}Two problems here:
The callback should read row/col from the trace's navigation state directly, not from 2. plugin.tsx — DomNodeAdapter cleanup removes canvas from DOM } else {
node.parentNode?.removeChild(node);
}When the React subtree unmounts, this removes the canvas element entirely from the DOM. Chart.js still holds a reference to 3. plugin.tsx — Throwing from a Chart.js plugin hook can break the chart if (!parent) {
throw new Error('MAIDR Chart.js plugin: canvas must be in the DOM');
}Throwing from Medium Priority4. extractor.ts — ID collision when canvases share the same canvas.id
5. extractor.ts — Silent fallback for unknown chart types The 6. extractor.ts — Multi-series scatter plots lose dataset distinction All scatter datasets are merged into one flat array. A chart with two scatter series loses its series structure entirely. Consider mapping each dataset to a separate MAIDR layer, or document this limitation explicitly. 7. plugin.tsx — createHighlightCallback row/col mapping assumption chart.setActiveElements([{ datasetIndex: row, index: col }]);The mapping 8. extractor.ts — Hardcoded Volume is silently dropped and hardcoded to 0. If 9. extractor.ts — Math.max with empty spread produces -Infinity const numCategories = Math.max(labels.length, ...data.datasets.map(ds => ds.data.length));If Low Priority / Suggestions10. extractor.ts — Global mutable nextId module state Module-level mutable counters complicate unit testing (they do not reset between tests) and behave unexpectedly with HMR. 11. extractor.ts — isPointValue uses brittle negative predicates return v != null && typeof v === 'object' && 'x' in v && 'y' in v && !('o' in v) && !('v' in v) && !('median' in v);This relies on the absence of other properties to distinguish types. A future plugin data type with 12. extractor.ts — Heatmap composite key collision valueMap.set(`${x}|${y}`, point.v);If a label contains 13. grammar.ts — Non-serializable function field on a schema type
14. Missing tests This PR adds around 800 lines of new logic. The extractor functions are pure transformations that are straightforward to unit test. High-value cases:
Overall this is a solid, well-structured implementation. The three high-priority items (braille state dependency, canvas DOM cleanup, and throwing from a plugin hook) should be addressed before merge. The test coverage gap is also worth filling given the complexity of the extraction logic. |
- Fix canvas DOM cleanup: restore canvas to original parent before removing MAIDR container (prevents canvas deletion on chart destroy) - Use graceful degradation instead of throwing from plugin hook when canvas has no parent element - Fix highlight mapping for stacked/dodged bars: swap datasetIndex and index since MAIDR's row=category/col=group maps to Chart.js's index=category/datasetIndex=group - Fix multi-series scatter: preserve per-dataset layers instead of flattening into one array (maintains dataset distinction for correct highlighting) - Fix ID collision: always append monotonic counter to prevent DOM conflicts when multiple charts share the same canvas.id - Fix heatmap composite key: use null separator to prevent collision when label values contain the pipe character - Add developer warning for unknown chart types in fallback - Add Math.max safety floor for empty dataset edge case - Document candlestick volume limitation https://claude.ai/code/session_01JWjCS4jngMUYvbcqRq7Qbn
Code Review: Chart.js IntegrationThis is a solid feature that meaningfully expands MAIDR's reach to canvas-based charts. The plugin/extractor separation is clean, the type definitions are minimal and well-scoped, and the use of Chart.js's native plugin lifecycle hooks is the right approach. A few issues need attention before merge. Bug: Canvas disappears from DOM after Chart.js chart destructionFile: These two cleanup paths race against each other: // destroyMaidrForChart does these steps in order:
parent.insertBefore(canvas, binding.container); // (1) move canvas back
binding.root.unmount(); // (2) triggers React cleanup →
// DomNodeAdapter ref cleanup fires:
// node.parentNode?.removeChild(node); // (3) removes canvas again!
binding.container.remove(); // (4) too lateAfter Fix: Either call Bug: Canvas highlighting silently breaks when braille is disabledFile: if (!state.empty && !state.braille.empty) {
callback({
layerId: state.layerId,
row: state.braille.row,
col: state.braille.col,
});
}Two problems:
The callback should use the trace's position directly and guard only on Bug:
|
| Priority | Issue |
|---|---|
| Bug | Canvas removed from DOM after chart destruction (cleanup race) |
| Bug | Canvas highlighting breaks when braille is disabled |
| Bug | isBoxplotValue too permissive — only checks 'median' |
| Design | onNavigate adds runtime behavior to a data schema type |
| Correctness | No afterUpdate support; stale data after chart updates |
| Minor | isPointValue fragile negative predicates |
| Minor | volume: 0 silently drops candlestick volume |
| Minor | Module-level nextId complicates testing |
| Missing | Unit tests for extraction logic |
The canvas cleanup race and braille guard are the most impactful bugs to fix before merge. The rest can be addressed iteratively.
Add a Chart.js plugin that automatically makes canvas-based charts
accessible through MAIDR's audio sonification, text descriptions,
braille output, and keyboard navigation.
Key changes:
maidr/chartjsentry point withmaidrPluginandextractMaidrDatasetActiveElementsAPI bridgeonNavigatecallback on Maidr type for canvas-based highlight integrationvite.chartjs.config.ts)https://claude.ai/code/session_01JWjCS4jngMUYvbcqRq7Qbn