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). |
…lestick/scatter - Move src/binder/amcharts -> src/adapters/amcharts; extractors.ts -> extractor.ts - Fold amcharts-entry.ts into adapters/amcharts/index.ts as the build entry - Integrate amcharts into scripts/build.js; remove vite.amcharts.config.ts - Fix grammar drift: axes use AxisConfig objects; line/segmented points use `z` - Fix stacked/normalized detection to read am5 series (stacked/valueYShow) instead of the non-existent ValueAxis.stackMode - Remove candlestick and scatter (no reliable am5 series representation) - Replace single bar example with consolidated examples/amcharts.html - Add docs/amcharts.md and wire it into the docs site (nav, build, SEO) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a UMD build (dist/amcharts.js, `maidrAmCharts` global) alongside the ESM build, matching the d3/google-charts/chartjs/vegalite adapters. ESM imports are blocked on file:// origins, so the example needed a server; the UMD global lets examples/amcharts.html load via a classic <script> tag and open directly from disk. Update the example and docs to use it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
amCharts 5 renders to canvas, so MAIDR's SVG-based highlighting can't target
data points (selectors return undefined). Add a programmatic bindAmCharts(root)
/ bindXYChart that mounts the MAIDR React app over the chart and drives a DOM
overlay box on the active data point through the onNavigate callback, mirroring
the Chart.js adapter. The JSON/maidr-attribute path cannot carry the callback,
so fromAmCharts stays as the data-only (no-highlight) option.
- overlay.ts: HighlightOverlay anchored to root.dom + per-type pixel geometry
via am5 sprite.toGlobal()
- navmap.ts: maps {layerId,row,col} -> am5 series/dataItem per type
(bar/segmented/line/histogram/heatmap, with (numY-1)-row heatmap reversal)
- binder.tsx: React mount over root.dom, ResizeObserver replay, dispose()
- build: amcharts entry useReact:true so React bundles into the UMD; exports
default -> dist/amcharts.js
- example + docs updated to use bindAmCharts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The canvas highlight box overflowed below the axis baseline and was wider than the bar, and used a hardcoded orange. Read the element rect via am5 sprite.globalBounds() (CSS px) and clip it to chart.plotContainer.globalBounds() so the box hugs the visible bar even when the value axis min > 0. Style it with MAIDR's configured general.highlightColor (read from the persisted setting via LocalStorageService, default fallback) to match the SVG adapters, applied via color-mix at draw time so a settings change shows on the next navigation. - overlay.ts: globalBounds + plot-area intersect; color provider + color-mix fill - types.ts: AmBounds, AmSprite.globalBounds, AmXYChart.plotContainer - binder.tsx: thread chart -> plotContainer bounds; configured-color provider - highlightColor.ts: read general.highlightColor from localStorage - settings.ts: export SETTINGS_KEY Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
sprite.globalBounds() returned null at runtime, so the overlay drew nothing.
Revert column/point geometry to the proven toGlobal() API with corrected local
corners ({0,0} / {width,height}); keep globalBounds() only as a fallback. Add
overflow:hidden on the overlay container and a robust readPlotBounds() (globalBounds
-> toGlobal+size -> null) so the box clips to the plot area instead of overflowing
below the axis baseline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…harts - binder: clear the highlight overlay when focus leaves the chart (focusout whose relatedTarget is outside the container), matching the SVG adapters whose HighlightService clears when the Controller disposes. - example: add am5.Legend to the multi-series charts (dodged/stacked/ normalized/line) so each series is visually labeled. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- bar / histogram: single-series am5.Legend (labels the one series). - heatmap: am5.HeatLegend (gradient color scale) matching the heatRules colors, with start/end values wired from the series aggregates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add an amCharts 5 section to the examples page (build-site.js), linking the consolidated examples/amcharts.html via loadHTML plus the integration guide. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Code Review: amCharts 5 Binder (#544)OverviewThis PR adds a complete amCharts 5 adapter that converts amCharts 5 Bug / Correctness Issues1. 2. // Default to bar for category-based series.
return 'bar';A series that is neither 3. Series grouping is duplicated between 4. 5. Module-level mutable Code Quality6. 7. const fill = `color-mix(in srgb, ${color} 22%, transparent)`;
8. Naming inconsistency: 9. Long duplicated branch in Performance10. 11. Test Coverage12. No tests are included. The PR adds ~2,600 lines of new functionality. The pure extraction logic in Minor / Nits
SummaryThe feature is well-designed and architecturally consistent with the rest of the adapter layer. The main risks to address before merging are:
Everything else is low severity or polish-level. Great work overall on a complex integration. |
|
🎉 This PR is included in version 3.69.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
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