feat: add amCharts 5 binder for MAIDR (#544)#560
Conversation
Add a new `maidr/amcharts` entry point that provides adapter functions to convert amCharts 5 chart instances into MAIDR-compatible JSON data. This enables audio sonification, text descriptions, braille output, and keyboard navigation for amCharts 5 visualizations. Supported chart types: bar/column, line, scatter, and candlestick. New files: - src/binder/amcharts/ - Binder module (types, extractors, selectors, adapter) - src/amcharts-entry.ts - Package entry point for maidr/amcharts - vite.amcharts.config.ts - Build configuration - examples/amcharts-bar.html - Usage example https://claude.ai/code/session_01TB5aCm8C8y3aBP3UgTCGjV
- Replace unsafe `as unknown as` cast in readChartTitle with runtime duck-type checks - Replace unsafe cast in readAxisLabel with runtime typeof guard - Fix misleading JSDoc on extractLinePoints (returns flat array, not 2D) - Remove redundant `?? undefined` in extractLinePoints - Remove duplicate @packageDocumentation from internal index.ts - Remove unnecessary React externals from vite.amcharts.config.ts https://claude.ai/code/session_01TB5aCm8C8y3aBP3UgTCGjV
PR Review: amCharts 5 Binder for MAIDROverall, this is a well-structured addition with good documentation and clean separation of concerns (adapter, extractors, selectors, types). The duck-typed approach correctly avoids a hard dependency on amCharts. Here are my findings: Critical Issues1. Module-level mutable counter (
|
| Category | Count |
|---|---|
| Critical (correctness/reliability) | 3 |
| Significant | 4 |
| Minor | 5 |
The binder's structure is sound. The two most important blockers before merging are: (a) adding at least basic unit tests for the extraction functions, and (b) verifying the fill field semantics for line traces and the lineLayerSelectors index-to-series mapping for correct highlighting. The _counter module state should also be replaced with a stateless ID strategy.
Extends the amCharts 5 binder to support all chart types that have amCharts equivalents, covering all MAIDR trace types except box and smooth (which have no native amCharts counterparts). New extraction functions: - extractSegmentedPoints: extracts SegmentedPoint[] with fill group ID - extractHistogramPoints: extracts HistogramPoint[] with bin edges - extractHeatmapData: builds HeatmapData grid from dual-category series New detection logic: - classifySeriesKind now detects 'histogram' (ColumnSeries + openValueX) and 'heatmap' (ColumnSeries + both categoryX and categoryY) - detectStackMode reads ValueAxis stackMode for stacked/normalized - Multiple ColumnSeries automatically grouped into segmented layers https://claude.ai/code/session_01TB5aCm8C8y3aBP3UgTCGjV
Code Review: feat: add amCharts 5 binder for MAIDR (#544)This is a well-structured addition that keeps the binder cleanly isolated under Critical1. function toNumber(value: unknown): number {
const n = Number(value);
return Number.isFinite(n) ? n : 0; // ← silent corruption
}If amCharts returns 2. No tests There are no unit tests for any of the new code. The extraction logic — especially 3. return `#${CSS.escape(parentId)} > ${tag}`;
Significant4. Volume field in candlestick extraction is incorrect ( const volume = item.get('valueX'); // volume sometimes on XIn amCharts 5 candlestick charts, volume is a separate data field ( 5. Global mutable counter for ID generation ( let _counter = 0;
function uid(): string {
return String(++_counter);
}This module-level state persists across calls within the same session. In tests, IDs will differ between runs depending on call order. More importantly, if two charts call 6. Layer ordering does not match series ordering ( Bar series are collected in a side-list and appended last (after line layers), regardless of their original position in 7. Selectors are mostly no-ops for Canvas renderer ( The file's own comment notes that amCharts 5 uses Canvas by default, meaning Minor8. const subtitle = options?.subtitle;
// ...
return { id, title, subtitle, subplots: [[subplot]] };When 9. The function traverses 10. Dead aliases in The config includes 11. Example does not demonstrate the binder ( The example manually constructs the MAIDR JSON instead of calling 12. return `#${CSS.escape(parentId)} > ${tag}`;This matches all direct child 13. Horizontal bar detection uses only first series ( const isHorizontal = typeof barSeriesList[0].get('categoryYField') === 'string';For segmented charts with mixed orientations (edge case), this could be wrong. Should be consistent, but worth a comment explaining the assumption. SummaryThe architecture is clean and well-separated. The main blockers before merging are: adding tests, fixing the silent |
- toNumber() now returns null instead of silently converting invalid values to 0; all extractors skip items with non-convertible values - Add cssEscape() wrapper with Node.js/test fallback for CSS.escape - Example now uses fromAmCharts() via dynamic ES module import instead of hardcoding the MAIDR JSON - buildLineLayer accepts and forwards title from series names - Document that fill field follows ggplot2 semantics (group identifier) - Document counter ephemerality for generated IDs https://claude.ai/code/session_01TB5aCm8C8y3aBP3UgTCGjV
Code Review: amCharts 5 BinderOverall this is a well-structured addition — clean module boundaries, solid JSDoc, and a sensible duck-typed interface layer that avoids requiring amCharts as a hard dependency. The following notes are meant to help harden it before merge. Bugs / Correctness Issues1. Unguarded axis array access (adapter.ts) If a chart has no X or Y axis (unusual but possible with partially-initialized charts), chart.xAxes.values[0] will be undefined. While readAxisLabel handles undefined gracefully, using .at(0) makes the intent explicit and is easier to read. 2. Candlestick volume reads from valueX with a sometimes comment (extractors.ts) valueX is the primary numeric X field in amCharts; using it as a proxy for volume will silently produce wrong output for non-volume charts. Consider defaulting volume to 0 with a console.warn when a dedicated volume field is absent, or adding a volumeField option to AmChartsBinderOptions. 3. Segmented-bar selector is a union, not per-series (adapter.ts) A union CSS selector will match all bars from all series at once. SegmentedTrace likely needs selectors scoped per series to highlight individual segments. The combined selector could inadvertently highlight the entire chart on any navigation step. 4. classifySeriesKind silently defaults unknown types to bar (extractors.ts) The final return 'bar' will silently run extractBarPoints on completely unrelated series types that happen to have a categoryXField. Returning 'unknown' and emitting a console.warn would surface the issue rather than producing corrupted data. Design / Architecture Concerns5. Global mutable counter causes brittle tests (adapter.ts) This module-level counter persists across fromAmCharts calls within the same JS process. Two consecutive test runs (or snapshot tests) will see different IDs, making tests non-deterministic. uid() is also identical to counter() — the separation adds no value. Using crypto.randomUUID() (or a simpler per-invocation approach) would be more robust. 6. readChartTitle relies on amCharts internal class names (adapter.ts) These are internal amCharts class names, not guaranteed stable across patch versions. Any amCharts update that renames these will silently drop the chart title. At minimum, add a comment documenting which amCharts 5 version this was verified against. 7. CSS selector fallback matches all elements with a shared class (selectors.ts) This produces a selector that matches every element on the page with that class, not just the ones belonging to this series. MAIDR would then attempt to highlight the wrong elements. When a reliable per-element selector cannot be built, returning undefined is the correct behavior — outer callers already handle undefined correctly. Example Code Issue8. setTimeout in the example is an anti-pattern (examples/amcharts-bar.html) The comment says In production, listen to chart.events.on('datavalidated', ...) — but the example demonstrates the wrong approach. A fixed 1000 ms delay will fail on slow machines and unnecessarily delays fast ones. Since this is likely the first code a new user copies, please use the event-based approach instead. Missing Tests9. No tests for the binder module The data extraction logic — especially classifySeriesKind, extractBarPoints, extractHistogramPoints, and extractHeatmapData — is complex enough to warrant unit tests with mock amCharts-like objects (no real amCharts dependency needed). Without tests, regressions in the extraction logic will only be caught at runtime. Even a small suite covering happy-path and null/missing-field cases for each extractor would greatly improve confidence. Minor Nits10. Unused path aliases in vite.amcharts.config.ts The binder module imports nothing from @command, @model, @State, @service, or @ui. Those aliases appear copy-pasted from the main Vite config. Removing the unused ones keeps the build config lean. 11. src/binder/amcharts/index.ts duplicates src/amcharts-entry.ts Both files re-export the same symbols from adapter.ts. If index.ts is for internal use only, a short comment to that effect would clarify the intent. Otherwise, consider collapsing the two files. Summary
The core data extraction approach is sound. The highest-priority items before merge are the selector correctness issues (#3, #7), the example anti-pattern (#8), and adding minimal tests (#9). |
Add a new
maidr/amchartsentry point that provides adapter functionsto convert amCharts 5 chart instances into MAIDR-compatible JSON data.
This enables audio sonification, text descriptions, braille output, and
keyboard navigation for amCharts 5 visualizations.
Supported chart types: bar/column, line, scatter, and candlestick.
New files:
https://claude.ai/code/session_01TB5aCm8C8y3aBP3UgTCGjV