feat: add AnyChart charting library adapter (#545)#557
feat: add AnyChart charting library adapter (#545)#557jooyoungseo wants to merge 3 commits intomainfrom
Conversation
Add a new `maidr/anychart` entry point that provides an adapter for integrating AnyChart charts with MAIDR's accessible visualization features. The adapter extracts series data and metadata from drawn AnyChart chart instances and generates the MAIDR JSON schema. - Add `bindAnyChart()` and `anyChartToMaidr()` public API functions - Add TypeScript type definitions for the AnyChart API subset used - Support bar, column, line, area, scatter, box, heatmap, and candlestick series type mapping - Add Vite build config for the new entry point - Add `./anychart` package export with types - Add example HTML demonstrating AnyChart bar chart integration Closes #545 https://claude.ai/code/session_018asBR8TGBxyy9oCrkQ7Yhk
PR Review: feat: add AnyChart charting library adapter (#545)Thank you for adding adapter support for AnyChart — this is a valuable addition for chart library integrations. I have reviewed the code carefully and found a few issues ranging from critical bugs to minor style concerns. Critical Issues1. Runtime never handles the maidr:bindchart eventThe bindAnyChart() function sets the maidr-data attribute and dispatches a maidr:bindchart custom event. However, the MAIDR runtime (src/index.tsx) never listens for this event. The runtime main() function runs exactly once (at DOMContentLoaded or immediately if already complete), scans the DOM for [maidr-data] attributes, and then exits. Since chart.draw() and bindAnyChart() necessarily happen after DOMContentLoaded (the chart container must exist first), the attribute is set too late — main() has already run and found nothing. The maidr:bindchart event is fired into the void because no listener is registered anywhere in the MAIDR runtime: // src/index.tsx — no listener for maidr:bindchart exists anywhere
if (document.readyState === 'loading') {
document.addEventListener(DomEventType.DOM_LOADED, main);
} else {
main();
}Impact: bindAnyChart() will appear to succeed (returns a Maidr object), but MAIDR will never initialise accessibility features for the chart. Fix options:
The example HTML has the same problem: MAIDR script runs before bindAnyChart() is called, so the runtime has already scanned the DOM and found no charts. 2. BOX, HEATMAP, and CANDLESTICK series silently produce wrong data structuresmapSeriesType() correctly maps box -> TraceType.BOX, heatmap/heat -> TraceType.HEATMAP, and candlestick -> TraceType.CANDLESTICK. However, the switch in anyChartToMaidr() only handles LINE and SCATTER explicitly — all other types (including BOX, HEATMAP, CANDLESTICK) fall through to buildBarLayer(): switch (traceType) {
case TraceType.LINE:
layer = buildLineLayer(series, i, options?.selectors);
break;
case TraceType.SCATTER:
layer = buildScatterLayer(series, i, options?.selectors);
break;
default:
// BOX/HEATMAP/CANDLESTICK also land here incorrectly
layer = buildBarLayer(series, i, options?.selectors);
break;
}The data structures MAIDR expects are completely different:
These are silently produced as BarPoint[] instead, which will cause downstream failures (wrong audio, incorrect navigation, potential runtime errors in the trace models). Either add the missing layer builders (buildBoxLayer, buildHeatmapLayer, buildCandlestickLayer) or skip unsupported types with a console.warn rather than silently falling back to an incompatible structure. Major Issues3. CSS selectors almost certainly will not match AnyChart DOMbuildSeriesSelector(seriesIndex) generates a selector using .anychart-series-N class names. AnyChart SVG DOM does not use these class names — it uses internal class names with renderer-specific identifiers. This selector will match nothing, making SVG element highlighting non-functional for all users. The comment acknowledges this is a guess ('A reasonable default'), but it should be validated against AnyChart actual DOM or removed in favour of requiring users to supply options.selectors explicitly. 4. selectors option incorrectly shared across all seriesWhen options.selectors is provided, the same value is applied to every layer in a multi-series chart: layer = buildBarLayer(series, i, options?.selectors); // same value for all iThis means every series targets the same SVG elements. Per-series selector overrides would need a different API shape such as selectors: string | string[] | Array<string | string[]> or a callback indexed by series. Minor Issues5. Unnecessary double call to resolveContainerElementanyChartToMaidr() calls resolveContainerElement(chart) solely to extract the container id. Then bindAnyChart() calls it a second time. If the container changes between calls (unlikely but possible), the two calls could yield different elements. Consider passing the resolved element between functions or accepting it as a parameter. 6. Inconsistent key casing in mapSeriesTypeThe mapping mixes kebab-case and snake_case for multi-word keys: 'step-line': TraceType.LINE, // kebab-case
'step-area': TraceType.LINE, // kebab-case
'spline_area': TraceType.LINE, // snake_case — inconsistentVerify what AnyChart seriesType() actually returns for spline area and normalise accordingly. 7. No testsPer the project code review checklist in CLAUDE.md, tests must pass before merging. This PR adds ~640 lines of new logic with zero test coverage. Unit tests for mapSeriesType(), anyChartToMaidr(), and the data extraction functions would be a baseline expectation. 8. Debug console.log in example fileexamples/anychart-bindable.html includes a console.log for generated MAIDR data. This is fine during development but should be removed or commented out in the final example. What Works Well
Summary: The critical integration bug (issue 1) means this adapter will not work at all in its current form — bindAnyChart() fires an event no listener will ever handle. The silent data structure mismatch for BOX/HEATMAP/CANDLESTICK (issue 2) would cause subtle failures for those chart types. I recommend addressing those two before merging. |
- Fix Critical #1: Add maidr:bindchart event listener in src/index.tsx so the runtime initialises charts bound after DOMContentLoaded - Fix Critical #2: Add proper layer builders for BOX, HEATMAP, and CANDLESTICK trace types with correct data structures - Add support for all remaining MAIDR trace types: HISTOGRAM, STACKED, DODGED, NORMALIZED, and SMOOTH - Fix Major #3: Remove fake CSS selectors; omit selectors when none provided since AnyChart SVG lacks stable class names - Fix Major #4: Support per-series selector overrides via indexed array - Fix Minor #5: Resolve container once in bindAnyChart and pass id - Fix Minor #6: Normalise underscores to hyphens in mapSeriesType - Fix Minor #8: Remove debug console.log from example HTML - Add 25 unit tests covering mapSeriesType, extractRawRows, resolveContainerElement, and anyChartToMaidr for all supported types https://claude.ai/code/session_018asBR8TGBxyy9oCrkQ7Yhk
|
PR Review: feat: add AnyChart charting library adapter HIGH PRIORITY
MEDIUM PRIORITY
LOW PRIORITY
WHAT IS DONE WELL
Reviewed against CLAUDE.md: MVVC architecture, KISS principle, Disposable pattern, observer/emitter conventions. |
- Guard against double-initialization with WeakSet in bindAnyChart - Add console.warn for area-to-LINE semantic downgrade - Document box plot outlier limitation in JSDoc - Add 4 bindAnyChart DOM tests with Node HTMLElement polyfill - Document permanent maidr:bindchart listener rationale - Comment bar/column orientation collapse in mapSeriesType - Fix process.env.NODE_ENV define in vite.anychart.config.ts - Simplify selectors type to Array<string | string[] | undefined> - Remove outdated type=text/javascript from example HTML https://claude.ai/code/session_018asBR8TGBxyy9oCrkQ7Yhk
PR Review: AnyChart Adapter (#557)This is a well-structured addition that follows a clean adapter pattern. The documentation is excellent, the type definitions are narrow and intentional, and the test coverage is solid. Below are issues ranging from bugs to nits. Bugs / Correctness1. Histogram builder is unreachable dead code
// Since there's no 'histogram' seriesType in AnyChart, we test the builder
// through a direct call to anyChartToMaidr
// For now, test the BAR output from column.Either add 2. default:
return buildBarLayer(series, seriesIndex, selectors); // silent fallbackThis masks programming errors. Any unhandled default:
throw new Error(`[maidr/anychart] No layer builder for TraceType: ...`);3. if (boundElements.has(container)) {
const existing = container.getAttribute('maidr-data');
return existing ? JSON.parse(existing) as Maidr : null; // can throw
}If the attribute is tampered with between calls (or malformed), this throws an uncaught exception. Wrap in try/catch and return 4. Histogram bin widths are hardcoded to ±0.5 xMin: x - 0.5,
xMax: x + 0.5,This is arbitrary and will be wrong for any histogram whose bins are not exactly 1 unit wide. Actual bin boundaries should be derived from the data (e.g., half-distance to adjacent bin centers). Even if this is a known limitation, it should emit a warning. 5. Smooth trace emits placeholder SVG coordinates const points: SmoothPoint[] = rows.map(r => ({
x: asNumber(r.x),
y: asNumber(r.value ?? r.y),
svg_x: 0, // placeholder
svg_y: 0, // placeholder
}));
Performance6. export function mapSeriesType(anyChartType: string): TraceType | null {
const normalized = ...;
const mapping: Record<string, TraceType> = { ... }; // new object every callMove 7. Heatmap building is O(n²) due to const xi = xLabels.indexOf(asString(r.x)); // O(n) per row
const yi = yLabels.indexOf(asString(r.y ...)); // O(n) per row
const xIndex = new Map(xLabels.map((v, i) => [v, i]));
const yIndex = new Map(yLabels.map((v, i) => [v, i]));
const xi = xIndex.get(asString(r.x)) ?? -1;Design / Architecture8. Event listener re-reads the attribute instead of using In document.addEventListener('maidr:bindchart', ((event: CustomEvent<Maidr>) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const json = target.getAttribute(Constant.MAIDR_DATA); // redundant read + re-parse
if (json) { parseAndInit(target, json, 'maidr-data'); }
}));The adapter already serialized the object into 9. const axisAccessor = axis === 'x' ? chart.xAxis : chart.yAxis;
if (!axisAccessor) return undefined;
const axisObj = axisAccessor.call(chart, 0); // verbose and fragileGiven that const axisObj = axis === 'x' ? chart.xAxis?.(0) : chart.yAxis?.(0);10. The adapter always uses the iteration index as the layer ID. Either remove Test Coverage Gaps
Nits
Summary
The adapter is well-documented and the overall approach is sound. Most important to address before merging: #1 (dead histogram code with misleading test), #2 (silent default fallback), #3 (unguarded JSON.parse), and #5 (smooth trace broken highlighting). The performance items (#6, #7) are straightforward wins. |
Add a new
maidr/anychartentry point that provides an adapter forintegrating AnyChart charts with MAIDR's accessible visualization
features. The adapter extracts series data and metadata from drawn
AnyChart chart instances and generates the MAIDR JSON schema.
bindAnyChart()andanyChartToMaidr()public API functionscandlestick series type mapping
./anychartpackage export with typesCloses #545
https://claude.ai/code/session_018asBR8TGBxyy9oCrkQ7Yhk