diff --git a/examples/anychart-bindable.html b/examples/anychart-bindable.html new file mode 100644 index 00000000..029fe101 --- /dev/null +++ b/examples/anychart-bindable.html @@ -0,0 +1,66 @@ + + + + + maidr + AnyChart Example + + + + + + + + + + + +

maidr + AnyChart Integration

+

+ This example shows how to use the maidr/anychart adapter + to make an AnyChart bar chart accessible via audio sonification, text + descriptions, braille output, and keyboard navigation. +

+
+ + diff --git a/package-lock.json b/package-lock.json index 02c70fcd..6acfdcce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "maidr", - "version": "3.50.0", + "version": "3.51.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "maidr", - "version": "3.50.0", + "version": "3.51.0", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", diff --git a/package.json b/package.json index ceb2b908..5ce35441 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,11 @@ "types": "./dist/react.d.mts", "import": "./dist/react.mjs", "default": "./dist/react.mjs" + }, + "./anychart": { + "types": "./dist/anychart.d.mts", + "import": "./dist/anychart.mjs", + "default": "./dist/anychart.mjs" } }, "main": "dist/maidr.js", @@ -20,9 +25,10 @@ "dist" ], "scripts": { - "build": "vite build && vite build --config vite.react.config.ts", + "build": "vite build && vite build --config vite.react.config.ts && vite build --config vite.anychart.config.ts", "build:script": "vite build", "build:react": "vite build --config vite.react.config.ts", + "build:anychart": "vite build --config vite.anychart.config.ts", "prepublishOnly": "npm run build", "prepare": "husky", "commitlint": "commitlint --from=HEAD~1 --to=HEAD", diff --git a/src/adapter/anychart.ts b/src/adapter/anychart.ts new file mode 100644 index 00000000..f7f33773 --- /dev/null +++ b/src/adapter/anychart.ts @@ -0,0 +1,693 @@ +/** + * AnyChart → MAIDR adapter. + * + * Extracts data from an AnyChart chart instance and produces a {@link Maidr} + * JSON object that the core MAIDR library can consume. This allows AnyChart + * visualizations to be made accessible via audio sonification, text + * descriptions, braille output, and keyboard navigation. + * + * @example + * ```ts + * import { bindAnyChart } from 'maidr/anychart'; + * + * const chart = anychart.bar([4, 2, 7, 1]); + * chart.container('container').draw(); + * + * bindAnyChart(chart); + * ``` + * + * @packageDocumentation + */ + +import type { + AnyChartBinderOptions, + AnyChartInstance, + AnyChartIterator, + AnyChartSeries, + AnyChartTitle, +} from '../type/anychart'; +import type { + BarPoint, + BoxPoint, + CandlestickPoint, + CandlestickTrend, + HeatmapData, + HistogramPoint, + LinePoint, + Maidr, + MaidrLayer, + MaidrSubplot, + ScatterPoint, + SegmentedPoint, + SmoothPoint, +} from '../type/grammar'; +import { TraceType } from '../type/grammar'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * AnyChart series types that are visually different from their MAIDR + * representation (e.g. filled area rendered as a line trace). A runtime + * warning is emitted for these so developers are aware of the semantic + * difference their screen-reader users will experience. + */ +const AREA_TYPES = new Set(['area', 'step-area', 'spline-area']); + +/** + * Map AnyChart series type strings to MAIDR TraceType values. + * + * AnyChart uses lowercase type names such as "bar", "line", "column", etc. + * Multi-word types are normalised to kebab-case before lookup. + * This mapping covers the chart types that MAIDR currently supports. + * + * @remarks + * - `"bar"` (horizontal) and `"column"` (vertical) both map to + * {@link TraceType.BAR}. MAIDR does not currently distinguish + * bar orientation at the trace-type level. + * - Area-family types (`area`, `step-area`, `spline-area`) map to + * {@link TraceType.LINE}. The fill is lost in the conversion; a + * runtime warning is emitted so developers are aware. + */ +export function mapSeriesType(anyChartType: string): TraceType | null { + const normalized = anyChartType.toLowerCase().replace(/[_\s]/g, '-'); + const mapping: Record = { + // Both horizontal bar and vertical column map to BAR. + // MAIDR does not currently distinguish bar orientation at the trace level. + 'bar': TraceType.BAR, + 'column': TraceType.BAR, + 'line': TraceType.LINE, + 'spline': TraceType.LINE, + 'step-line': TraceType.LINE, + // Area types are represented as LINE; the fill is lost. + 'area': TraceType.LINE, + 'step-area': TraceType.LINE, + 'spline-area': TraceType.LINE, + 'scatter': TraceType.SCATTER, + 'marker': TraceType.SCATTER, + 'bubble': TraceType.SCATTER, + 'box': TraceType.BOX, + 'heatmap': TraceType.HEATMAP, + 'heat': TraceType.HEATMAP, + 'candlestick': TraceType.CANDLESTICK, + 'ohlc': TraceType.CANDLESTICK, + }; + + const traceType = mapping[normalized] ?? null; + + // Warn when an area series is silently downgraded to a line trace. + if (traceType === TraceType.LINE && AREA_TYPES.has(normalized)) { + console.warn( + `[maidr/anychart] AnyChart "${anyChartType}" series mapped to LINE trace. ` + + 'The filled-area visual will be represented as a line for accessibility.', + ); + } + + return traceType; +} + +/** Safely extract the title text from an AnyChart chart. */ +function extractTitle(chart: AnyChartInstance): string | undefined { + try { + const title = chart.title(); + if (typeof title === 'string') + return title; + return (title as AnyChartTitle).text?.() ?? undefined; + } catch { + return undefined; + } +} + +/** Safely extract the axis title text from an AnyChart Cartesian chart. */ +function extractAxisTitle( + chart: AnyChartInstance, + axis: 'x' | 'y', +): string | undefined { + try { + const axisAccessor = axis === 'x' ? chart.xAxis : chart.yAxis; + if (!axisAccessor) + return undefined; + const axisObj = axisAccessor.call(chart, 0); + return axisObj?.title().text() ?? undefined; + } catch { + return undefined; + } +} + +/** Resolve the DOM element that holds the AnyChart SVG rendering. */ +export function resolveContainerElement( + chart: AnyChartInstance, +): HTMLElement | null { + try { + const container = chart.container(); + if (!container) + return null; + + // container() may return a string (element id), an HTMLElement, or a + // Stage wrapper with its own `.container()` / `.domElement()`. + if (typeof container === 'string') { + return document.getElementById(container); + } + if (container instanceof HTMLElement) { + return container; + } + + // Stage-like object + const stage = container as { container?: () => HTMLElement | null; domElement?: () => HTMLElement | null }; + if (typeof stage.domElement === 'function') { + return stage.domElement(); + } + if (typeof stage.container === 'function') { + const inner = stage.container(); + if (inner instanceof HTMLElement) + return inner; + } + } catch { + // fall through + } + return null; +} + +/** + * Extract raw data rows from an AnyChart series using its iterator. + * + * Returns an array of field maps. The concrete field names depend on the + * AnyChart series type – most Cartesian series expose `"x"` and `"value"`. + * Box series expose `"lowest"`, `"q1"`, `"median"`, `"q3"`, `"highest"`. + * Candlestick/OHLC series expose `"open"`, `"high"`, `"low"`, `"close"`. + */ +export function extractRawRows( + series: AnyChartSeries, +): Array> { + const rows: Array> = []; + const iterator: AnyChartIterator = series.getIterator(); + iterator.reset(); + while (iterator.advance()) { + const row: Record = { _index: iterator.getIndex() }; + for (const field of [ + 'x', + 'name', + 'value', + 'y', + // Box fields + 'lowest', + 'q1', + 'median', + 'q3', + 'highest', + // Candlestick/OHLC fields + 'open', + 'high', + 'low', + 'close', + 'volume', + // Grouping + 'fill', + 'group', + ]) { + const v = iterator.get(field); + if (v !== undefined && v !== null) + row[field] = v; + } + rows.push(row); + } + return rows; +} + +/** + * Resolve the CSS selector for a specific series index. + * + * AnyChart's internal SVG structure does not use stable, predictable class + * names. Consumers should supply explicit selectors via `options.selectors` + * for reliable highlighting. When no selectors are provided, no selector is + * emitted (highlighting will be unavailable). + */ +function resolveSelector( + seriesIndex: number, + options?: AnyChartBinderOptions, +): string | string[] | undefined { + if (!options?.selectors || options.selectors.length === 0) + return undefined; + + // If the array has exactly one element and it is a string, apply it to all + // series as a shared selector. + if (options.selectors.length === 1 && typeof options.selectors[0] === 'string') + return options.selectors[0]; + + // Per-series: look up by index. + return options.selectors[seriesIndex] ?? undefined; +} + +function asNumber(v: unknown, fallback = 0): number { + if (typeof v === 'number') + return v; + const n = Number(v); + return Number.isNaN(n) ? fallback : n; +} + +function asString(v: unknown, fallback = ''): string { + return v != null ? String(v) : fallback; +} + +// --------------------------------------------------------------------------- +// Layer builders – one per MAIDR trace type +// --------------------------------------------------------------------------- + +function buildBarLayer( + series: AnyChartSeries, + seriesIndex: number, + selectors: string | string[] | undefined, +): MaidrLayer { + const rows = extractRawRows(series); + const data: BarPoint[] = rows.map(r => ({ + x: asString(r.x ?? r.name ?? r._index), + y: asNumber(r.value ?? r.y), + })); + return { + id: String(seriesIndex), + type: TraceType.BAR, + ...(selectors ? { selectors } : {}), + data, + }; +} + +function buildLineLayer( + series: AnyChartSeries, + seriesIndex: number, + selectors: string | string[] | undefined, +): MaidrLayer { + const rows = extractRawRows(series); + const points: LinePoint[] = rows.map(r => ({ + x: r.x !== undefined ? (typeof r.x === 'number' ? r.x : String(r.x)) : asNumber(r._index), + y: asNumber(r.value ?? r.y), + })); + const data: LinePoint[][] = [points]; + return { + id: String(seriesIndex), + type: TraceType.LINE, + ...(selectors ? { selectors } : {}), + data, + }; +} + +function buildScatterLayer( + series: AnyChartSeries, + seriesIndex: number, + selectors: string | string[] | undefined, +): MaidrLayer { + const rows = extractRawRows(series); + const data: ScatterPoint[] = rows.map(r => ({ + x: asNumber(r.x), + y: asNumber(r.value ?? r.y), + })); + return { + id: String(seriesIndex), + type: TraceType.SCATTER, + ...(selectors ? { selectors } : {}), + data, + }; +} + +/** + * Build a BOX layer from an AnyChart box series. + * + * @remarks + * AnyChart exposes quartile data (lowest, q1, median, q3, highest) through + * its iterator, but does not provide direct access to outlier arrays via the + * standard data iterator API. As a result, `lowerOutliers` and + * `upperOutliers` are always empty. If your chart contains outliers and you + * need them in the accessible representation, supply them manually by + * post-processing the {@link Maidr} object returned from + * {@link anyChartToMaidr}. + */ +function buildBoxLayer( + series: AnyChartSeries, + seriesIndex: number, + selectors: string | string[] | undefined, +): MaidrLayer { + const rows = extractRawRows(series); + const data: BoxPoint[] = rows.map(r => ({ + fill: asString(r.x ?? r.name ?? r._index), + // Outlier arrays are not available through AnyChart's iterator API. + lowerOutliers: [], + min: asNumber(r.lowest), + q1: asNumber(r.q1), + q2: asNumber(r.median), + q3: asNumber(r.q3), + max: asNumber(r.highest), + upperOutliers: [], + })); + return { + id: String(seriesIndex), + type: TraceType.BOX, + ...(selectors ? { selectors } : {}), + data, + }; +} + +function buildHeatmapLayer( + series: AnyChartSeries, + seriesIndex: number, + selectors: string | string[] | undefined, +): MaidrLayer { + const rows = extractRawRows(series); + + // Collect unique x and y labels in insertion order. + const xLabels: string[] = []; + const yLabels: string[] = []; + const xSet = new Set(); + const ySet = new Set(); + + for (const r of rows) { + const xVal = asString(r.x); + const yVal = asString(r.y ?? r.name); + if (!xSet.has(xVal)) { + xLabels.push(xVal); + xSet.add(xVal); + } + if (!ySet.has(yVal)) { + yLabels.push(yVal); + ySet.add(yVal); + } + } + + // Build the 2D points matrix (y rows × x columns). + const points: number[][] = Array.from( + { length: yLabels.length }, + () => Array.from({ length: xLabels.length }).fill(0), + ); + for (const r of rows) { + const xi = xLabels.indexOf(asString(r.x)); + const yi = yLabels.indexOf(asString(r.y ?? r.name)); + if (xi >= 0 && yi >= 0) + points[yi][xi] = asNumber(r.value ?? r.fill); + } + + const data: HeatmapData = { x: xLabels, y: yLabels, points }; + return { + id: String(seriesIndex), + type: TraceType.HEATMAP, + ...(selectors ? { selectors } : {}), + data, + }; +} + +function buildCandlestickLayer( + series: AnyChartSeries, + seriesIndex: number, + selectors: string | string[] | undefined, +): MaidrLayer { + const rows = extractRawRows(series); + const data: CandlestickPoint[] = rows.map((r) => { + const open = asNumber(r.open); + const close = asNumber(r.close); + const high = asNumber(r.high); + const low = asNumber(r.low); + + let trend: CandlestickTrend = 'Neutral'; + if (close > open) + trend = 'Bull'; + else if (close < open) + trend = 'Bear'; + + const midpoint = (high + low) / 2; + return { + value: asString(r.x ?? r.name ?? r._index), + open, + high, + low, + close, + volume: asNumber(r.volume), + trend, + volatility: midpoint > 0 ? (high - low) / midpoint : 0, + }; + }); + return { + id: String(seriesIndex), + type: TraceType.CANDLESTICK, + ...(selectors ? { selectors } : {}), + data, + }; +} + +function buildHistogramLayer( + series: AnyChartSeries, + seriesIndex: number, + selectors: string | string[] | undefined, +): MaidrLayer { + const rows = extractRawRows(series); + const data: HistogramPoint[] = rows.map((r) => { + const y = asNumber(r.value ?? r.y); + const x = asNumber(r.x); + return { + x, + y, + xMin: x - 0.5, + xMax: x + 0.5, + yMin: 0, + yMax: y, + }; + }); + return { + id: String(seriesIndex), + type: TraceType.HISTOGRAM, + ...(selectors ? { selectors } : {}), + data, + }; +} + +function buildSegmentedLayer( + series: AnyChartSeries, + seriesIndex: number, + selectors: string | string[] | undefined, + traceType: TraceType.STACKED | TraceType.DODGED | TraceType.NORMALIZED, +): MaidrLayer { + const rows = extractRawRows(series); + // Group by fill/group value to form the 2D array. + const groups = new Map(); + for (const r of rows) { + const fill = asString(r.fill ?? r.group ?? seriesIndex); + if (!groups.has(fill)) + groups.set(fill, []); + groups.get(fill)!.push({ + x: asString(r.x ?? r.name ?? r._index), + y: asNumber(r.value ?? r.y), + fill, + }); + } + const data: SegmentedPoint[][] = [...groups.values()]; + return { + id: String(seriesIndex), + type: traceType, + ...(selectors ? { selectors } : {}), + data, + }; +} + +function buildSmoothLayer( + series: AnyChartSeries, + seriesIndex: number, + selectors: string | string[] | undefined, +): MaidrLayer { + const rows = extractRawRows(series); + // Smooth traces include SVG coordinates. Since we extract from AnyChart's + // data model rather than SVG DOM, svg_x/svg_y are set to 0 as placeholders. + const points: SmoothPoint[] = rows.map(r => ({ + x: asNumber(r.x), + y: asNumber(r.value ?? r.y), + svg_x: 0, + svg_y: 0, + })); + const data: SmoothPoint[][] = [points]; + return { + id: String(seriesIndex), + type: TraceType.SMOOTH, + ...(selectors ? { selectors } : {}), + data, + }; +} + +// --------------------------------------------------------------------------- +// Layer builder dispatch +// --------------------------------------------------------------------------- + +function buildLayer( + series: AnyChartSeries, + seriesIndex: number, + traceType: TraceType, + selectors: string | string[] | undefined, +): MaidrLayer { + switch (traceType) { + case TraceType.BAR: + return buildBarLayer(series, seriesIndex, selectors); + case TraceType.LINE: + return buildLineLayer(series, seriesIndex, selectors); + case TraceType.SCATTER: + return buildScatterLayer(series, seriesIndex, selectors); + case TraceType.BOX: + return buildBoxLayer(series, seriesIndex, selectors); + case TraceType.HEATMAP: + return buildHeatmapLayer(series, seriesIndex, selectors); + case TraceType.CANDLESTICK: + return buildCandlestickLayer(series, seriesIndex, selectors); + case TraceType.HISTOGRAM: + return buildHistogramLayer(series, seriesIndex, selectors); + case TraceType.STACKED: + case TraceType.DODGED: + case TraceType.NORMALIZED: + return buildSegmentedLayer(series, seriesIndex, selectors, traceType); + case TraceType.SMOOTH: + return buildSmoothLayer(series, seriesIndex, selectors); + default: + return buildBarLayer(series, seriesIndex, selectors); + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Convert an AnyChart chart instance into a MAIDR data object. + * + * This function inspects the chart's series and metadata after it has been + * drawn, then constructs a {@link Maidr} JSON structure that can be passed + * to the `` React component or used with `bindAnyChart()`. + * + * @param chart - A drawn AnyChart chart instance. + * @param options - Optional overrides for id, title, axes, and selectors. + * @returns The MAIDR data object, or `null` if no convertible series found. + */ +export function anyChartToMaidr( + chart: AnyChartInstance, + options?: AnyChartBinderOptions, +): Maidr | null { + const seriesCount = chart.getSeriesCount(); + if (seriesCount === 0) + return null; + + // Resolve chart metadata. + const container = resolveContainerElement(chart); + const id = options?.id ?? container?.id ?? 'anychart-maidr'; + const title = options?.title ?? extractTitle(chart); + const xAxisLabel = options?.axes?.x ?? extractAxisTitle(chart, 'x'); + const yAxisLabel = options?.axes?.y ?? extractAxisTitle(chart, 'y'); + + const layers: MaidrLayer[] = []; + + for (let i = 0; i < seriesCount; i++) { + const series = chart.getSeriesAt(i); + if (!series) + continue; + + const anyChartType = series.seriesType(); + const traceType = mapSeriesType(anyChartType); + if (!traceType) { + console.warn( + `[maidr/anychart] Unsupported AnyChart series type "${anyChartType}". Skipping series ${i}.`, + ); + continue; + } + + const selectors = resolveSelector(i, options); + const layer = buildLayer(series, i, traceType, selectors); + + // Attach axis labels. + if (xAxisLabel || yAxisLabel) { + layer.axes = { + ...(xAxisLabel ? { x: xAxisLabel } : {}), + ...(yAxisLabel ? { y: yAxisLabel } : {}), + }; + } + + layers.push(layer); + } + + if (layers.length === 0) + return null; + + const subplot: MaidrSubplot = { layers }; + const maidr: Maidr = { + id, + ...(title ? { title } : {}), + subplots: [[subplot]], + }; + + return maidr; +} + +/** Elements that have already been bound via {@link bindAnyChart}. */ +const boundElements = new WeakSet(); + +/** + * Bind an AnyChart chart to MAIDR for accessible interaction. + * + * This is the primary high-level API. It extracts data from a drawn + * AnyChart chart, generates the MAIDR schema, injects it as a + * `maidr-data` attribute on the chart's container element, and + * dispatches a `maidr:bindchart` event so the MAIDR runtime picks it up. + * + * The MAIDR runtime (`maidr.js`) must be loaded on the page. It + * listens for `maidr:bindchart` events and initialises accessibility + * features for the target element. + * + * Calling this function multiple times on the same chart is safe: if the + * container has already been bound, the existing {@link Maidr} data is + * returned without re-dispatching the initialisation event. + * + * @param chart - A drawn AnyChart chart instance. + * @param options - Optional overrides. + * @returns The generated {@link Maidr} object, or `null` on failure. + * + * @example + * ```ts + * const chart = anychart.bar([4, 2, 7, 1]); + * chart.container('container').draw(); + * bindAnyChart(chart); + * ``` + */ +export function bindAnyChart( + chart: AnyChartInstance, + options?: AnyChartBinderOptions, +): Maidr | null { + const container = resolveContainerElement(chart); + if (!container) { + console.warn( + '[maidr/anychart] Could not find the chart container element. ' + + 'Make sure the chart has been drawn before calling bindAnyChart().', + ); + return null; + } + + // Prevent double-initialisation. If the same container was already bound, + // return the previously generated schema without re-dispatching the event + // (which would create duplicate handlers and observers). + if (boundElements.has(container)) { + const existing = container.getAttribute('maidr-data'); + return existing ? JSON.parse(existing) as Maidr : null; + } + + const maidr = anyChartToMaidr(chart, { + ...options, + id: options?.id ?? container.id ?? 'anychart-maidr', + }); + + if (!maidr) { + console.warn('[maidr/anychart] Could not extract data from AnyChart chart.'); + return null; + } + + // Mark the container as bound before mutating the DOM. + boundElements.add(container); + + // Inject MAIDR data onto the container element. + container.setAttribute('maidr-data', JSON.stringify(maidr)); + + // Dispatch a custom event so the MAIDR runtime initialises on this element. + container.dispatchEvent( + new CustomEvent('maidr:bindchart', { bubbles: true, detail: maidr }), + ); + + return maidr; +} diff --git a/src/anychart-entry.ts b/src/anychart-entry.ts new file mode 100644 index 00000000..2825cd09 --- /dev/null +++ b/src/anychart-entry.ts @@ -0,0 +1,36 @@ +/** + * Public API for the MAIDR AnyChart adapter. + * + * Provides {@link bindAnyChart} and {@link anyChartToMaidr} for integrating + * AnyChart charts with MAIDR's accessible visualisation features. + * + * @remarks + * AnyChart must be loaded separately – this module does not bundle the + * AnyChart library. Call these functions **after** the chart has been drawn + * so that series data and the SVG container are available. + * + * @example + * ```ts + * import { bindAnyChart } from 'maidr/anychart'; + * + * const chart = anychart.bar([4, 2, 7, 1]); + * chart.container('container').draw(); + * + * // One-liner: extracts data, sets maidr-data attribute, fires event. + * bindAnyChart(chart); + * ``` + * + * @packageDocumentation + */ +export { anyChartToMaidr, bindAnyChart } from './adapter/anychart'; + +/** + * Re-exported types for configuring the AnyChart adapter. + */ +export type { AnyChartBinderOptions, AnyChartInstance } from './type/anychart'; + +/** + * Re-exported core MAIDR types so consumers can type the output. + */ +export type { Maidr as MaidrData, MaidrLayer, MaidrSubplot } from './type/grammar'; +export { TraceType } from './type/grammar'; diff --git a/src/index.tsx b/src/index.tsx index b5af8f94..59b21cd3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,6 +20,25 @@ if (document.readyState === 'loading') { main(); } +// Support for third-party adapters (e.g. maidr/anychart) that bind charts +// after the initial DOM scan. When an adapter sets `maidr-data` on an +// element and dispatches this event, we initialise MAIDR for that element. +// +// This listener is intentionally permanent (never removed) because adapter +// bindings can happen at any point during the page's lifetime — there is no +// deterministic teardown moment. A WeakSet or similar guard in the adapter +// prevents duplicate initialisation for the same element. +document.addEventListener('maidr:bindchart', ((event: CustomEvent) => { + const target = event.target; + if (!(target instanceof HTMLElement)) + return; + + const json = target.getAttribute(Constant.MAIDR_DATA); + if (json) { + parseAndInit(target, json, 'maidr-data'); + } +}) as EventListener); + function parseAndInit( plot: HTMLElement, json: string, diff --git a/src/type/anychart.ts b/src/type/anychart.ts new file mode 100644 index 00000000..dae42fd0 --- /dev/null +++ b/src/type/anychart.ts @@ -0,0 +1,139 @@ +/** + * Minimal type definitions for AnyChart chart instances. + * + * These types describe the subset of the AnyChart API that the MAIDR adapter + * needs in order to extract chart metadata, series data, and SVG elements. + * They are intentionally narrow to avoid a hard dependency on the AnyChart + * library while still providing type safety for the adapter code. + */ + +/** Iterator for traversing data rows in an AnyChart data view. */ +export interface AnyChartIterator { + advance: () => boolean; + get: (field: string) => unknown; + getIndex: () => number; + getRowsCount: () => number; + reset: () => void; +} + +/** A wrapped data point returned by `series.getPoint(index)`. */ +export interface AnyChartPoint { + get: (field: string) => unknown; + getIndex: () => number; + exists: () => boolean; +} + +/** An individual series within a chart. */ +export interface AnyChartSeries { + id: () => string | number; + name: () => string; + seriesType: () => string; + getIterator: () => AnyChartIterator; + getPoint: (index: number) => AnyChartPoint; + getStat: (key: string) => unknown; +} + +/** Title object returned by `chart.title()`. */ +export interface AnyChartTitle { + text: () => string | undefined; +} + +/** Axis label configuration. */ +export interface AnyChartAxisLabels { + enabled: () => boolean; +} + +/** Axis title configuration. */ +export interface AnyChartAxisTitle { + text: () => string | undefined; +} + +/** An axis instance on a Cartesian chart. */ +export interface AnyChartAxis { + title: () => AnyChartAxisTitle; + labels: () => AnyChartAxisLabels; +} + +/** Rendering stage / container element. */ +export interface AnyChartStage { + container: () => HTMLElement | null; + domElement: () => HTMLElement | null; +} + +/** + * The minimal chart interface the adapter requires. + * + * All supported AnyChart chart types (Cartesian, Pie, etc.) expose these + * methods once the chart has been drawn. + */ +export interface AnyChartInstance { + /** Chart title accessor. */ + title: () => AnyChartTitle | string; + + /** Rendering container / stage. */ + container: () => AnyChartStage | HTMLElement | string; + + /** Number of series in the chart. */ + getSeriesCount: () => number; + + /** Get a series by its numeric index. */ + getSeriesAt: (index: number) => AnyChartSeries | null; + + /** X-axis accessor (Cartesian charts). Returns null for non-Cartesian. */ + xAxis?: (index?: number) => AnyChartAxis | null; + + /** Y-axis accessor (Cartesian charts). Returns null for non-Cartesian. */ + yAxis?: (index?: number) => AnyChartAxis | null; + + /** Chart type string (e.g. "bar", "line", "pie"). */ + getType?: () => string; + + /** SVG string export. */ + toSvg?: () => string; +} + +/** + * Options the consumer can pass when binding an AnyChart chart to MAIDR. + */ +export interface AnyChartBinderOptions { + /** + * Override the chart ID used in the MAIDR schema. + * Defaults to the chart container element's `id` attribute. + */ + id?: string; + + /** + * Override the chart title. + * Defaults to `chart.title().text()`. + */ + title?: string; + + /** + * Override axis labels. + */ + axes?: { + x?: string; + y?: string; + }; + + /** + * CSS selector overrides for SVG element highlighting. + * + * Each element in the array corresponds to a series by index. Use + * `undefined` at a given position to skip that series (no highlighting). + * + * AnyChart's internal SVG structure does not expose stable class names, + * so consumers should inspect the rendered DOM and provide explicit + * selectors for reliable highlighting. + * + * @example + * ```ts + * // Apply per-series selectors (2 series, second has none): + * selectors: ['.series-0 rect', undefined] + * + * // Apply the same selector to all series: + * selectors: ['.chart rect'] + * ``` + */ + selectors?: Array; +} diff --git a/test/adapter/anychart.test.ts b/test/adapter/anychart.test.ts new file mode 100644 index 00000000..89ef8fbf --- /dev/null +++ b/test/adapter/anychart.test.ts @@ -0,0 +1,587 @@ +/* eslint-disable perfectionist/sort-imports -- setup-dom must run before adapter import */ +import './setup-dom'; + +import type { AnyChartInstance, AnyChartIterator, AnyChartSeries } from '@type/anychart'; +import type { CandlestickPoint, HeatmapData, HistogramPoint } from '@type/grammar'; +import { anyChartToMaidr, bindAnyChart, extractRawRows, mapSeriesType, resolveContainerElement } from '../../src/adapter/anychart'; +import { TraceType } from '../../src/type/grammar'; + +// --------------------------------------------------------------------------- +// Test helpers – mock AnyChart objects +// --------------------------------------------------------------------------- + +function createMockIterator( + rows: Array>, +): AnyChartIterator { + let cursor = -1; + return { + advance() { + cursor++; + return cursor < rows.length; + }, + get(field: string) { + return rows[cursor]?.[field] ?? undefined; + }, + getIndex() { + return cursor; + }, + getRowsCount() { + return rows.length; + }, + reset() { + cursor = -1; + }, + }; +} + +function createMockSeries( + type: string, + rows: Array>, +): AnyChartSeries { + return { + id: () => 0, + name: () => 'Series 0', + seriesType: () => type, + getIterator: () => createMockIterator(rows), + getPoint: (index: number) => ({ + get: (field: string) => rows[index]?.[field] ?? undefined, + getIndex: () => index, + exists: () => index >= 0 && index < rows.length, + }), + getStat: () => undefined, + }; +} + +function createMockChart( + seriesList: AnyChartSeries[], + overrides: Partial = {}, +): AnyChartInstance { + return { + title: () => 'Test Chart', + container: () => 'container-id', + getSeriesCount: () => seriesList.length, + getSeriesAt: (index: number) => seriesList[index] ?? null, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// mapSeriesType +// --------------------------------------------------------------------------- + +describe('mapSeriesType', () => { + afterEach(() => jest.restoreAllMocks()); + + it('maps standard Cartesian types', () => { + expect(mapSeriesType('bar')).toBe(TraceType.BAR); + expect(mapSeriesType('column')).toBe(TraceType.BAR); + expect(mapSeriesType('line')).toBe(TraceType.LINE); + expect(mapSeriesType('scatter')).toBe(TraceType.SCATTER); + expect(mapSeriesType('marker')).toBe(TraceType.SCATTER); + expect(mapSeriesType('bubble')).toBe(TraceType.SCATTER); + }); + + it('maps complex chart types', () => { + expect(mapSeriesType('box')).toBe(TraceType.BOX); + expect(mapSeriesType('heatmap')).toBe(TraceType.HEATMAP); + expect(mapSeriesType('heat')).toBe(TraceType.HEATMAP); + expect(mapSeriesType('candlestick')).toBe(TraceType.CANDLESTICK); + expect(mapSeriesType('ohlc')).toBe(TraceType.CANDLESTICK); + }); + + it('maps line variants', () => { + expect(mapSeriesType('spline')).toBe(TraceType.LINE); + expect(mapSeriesType('step-line')).toBe(TraceType.LINE); + }); + + it('maps area types to LINE with a warning', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + expect(mapSeriesType('area')).toBe(TraceType.LINE); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('mapped to LINE trace'), + ); + + warnSpy.mockClear(); + expect(mapSeriesType('step-area')).toBe(TraceType.LINE); + expect(warnSpy).toHaveBeenCalled(); + + warnSpy.mockClear(); + expect(mapSeriesType('spline_area')).toBe(TraceType.LINE); + expect(warnSpy).toHaveBeenCalled(); + }); + + it('normalises underscores and mixed case', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + expect(mapSeriesType('spline_area')).toBe(TraceType.LINE); + expect(mapSeriesType('Spline_Area')).toBe(TraceType.LINE); + expect(mapSeriesType('STEP_LINE')).toBe(TraceType.LINE); + expect(mapSeriesType('Bar')).toBe(TraceType.BAR); + warnSpy.mockRestore(); + }); + + it('returns null for unsupported types', () => { + expect(mapSeriesType('pie')).toBeNull(); + expect(mapSeriesType('funnel')).toBeNull(); + expect(mapSeriesType('unknown')).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// extractRawRows +// --------------------------------------------------------------------------- + +describe('extractRawRows', () => { + it('extracts basic x/value fields', () => { + const series = createMockSeries('bar', [ + { x: 'A', value: 10 }, + { x: 'B', value: 20 }, + ]); + const rows = extractRawRows(series); + expect(rows).toHaveLength(2); + expect(rows[0]).toMatchObject({ x: 'A', value: 10 }); + expect(rows[1]).toMatchObject({ x: 'B', value: 20 }); + }); + + it('extracts box fields', () => { + const series = createMockSeries('box', [ + { x: 'G1', lowest: 10, q1: 25, median: 50, q3: 75, highest: 90 }, + ]); + const rows = extractRawRows(series); + expect(rows[0]).toMatchObject({ + x: 'G1', + lowest: 10, + q1: 25, + median: 50, + q3: 75, + highest: 90, + }); + }); + + it('extracts candlestick OHLC fields', () => { + const series = createMockSeries('candlestick', [ + { x: '2024-01-01', open: 100, high: 110, low: 95, close: 105 }, + ]); + const rows = extractRawRows(series); + expect(rows[0]).toMatchObject({ + x: '2024-01-01', + open: 100, + high: 110, + low: 95, + close: 105, + }); + }); + + it('includes iterator index as _index', () => { + const series = createMockSeries('bar', [{ value: 42 }]); + const rows = extractRawRows(series); + expect(rows[0]._index).toBe(0); + }); +}); + +// --------------------------------------------------------------------------- +// resolveContainerElement +// --------------------------------------------------------------------------- + +describe('resolveContainerElement', () => { + it('returns null when container() returns null', () => { + const chart = createMockChart([], { container: () => null as any }); + expect(resolveContainerElement(chart)).toBeNull(); + }); + + it('returns null when container() throws', () => { + const chart = createMockChart([], { + container: () => { + throw new Error('test'); + }, + }); + expect(resolveContainerElement(chart)).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// anyChartToMaidr – BAR +// --------------------------------------------------------------------------- + +describe('anyChartToMaidr – BAR', () => { + it('generates bar layer from column series', () => { + const series = createMockSeries('column', [ + { x: 'Sat', value: 87 }, + { x: 'Sun', value: 76 }, + { x: 'Thur', value: 62 }, + ]); + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { id: 'test' }); + + expect(result).not.toBeNull(); + expect(result!.id).toBe('test'); + expect(result!.title).toBe('Test Chart'); + + const layer = result!.subplots[0][0].layers[0]; + expect(layer.type).toBe(TraceType.BAR); + expect(layer.data).toHaveLength(3); + expect((layer.data as any[])[0]).toEqual({ x: 'Sat', y: 87 }); + }); + + it('returns null for empty chart', () => { + const chart = createMockChart([]); + expect(anyChartToMaidr(chart)).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// anyChartToMaidr – LINE +// --------------------------------------------------------------------------- + +describe('anyChartToMaidr – LINE', () => { + it('generates line layer with 2D data', () => { + const series = createMockSeries('line', [ + { x: 1, value: 10 }, + { x: 2, value: 20 }, + { x: 3, value: 30 }, + ]); + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { id: 'line-test' }); + + const layer = result!.subplots[0][0].layers[0]; + expect(layer.type).toBe(TraceType.LINE); + // Line data is 2D: array of arrays + const data = layer.data as any[][]; + expect(Array.isArray(data[0])).toBe(true); + expect(data[0]).toHaveLength(3); + expect(data[0][0]).toEqual({ x: 1, y: 10 }); + }); +}); + +// --------------------------------------------------------------------------- +// anyChartToMaidr – SCATTER +// --------------------------------------------------------------------------- + +describe('anyChartToMaidr – SCATTER', () => { + it('generates scatter layer with numeric x and y', () => { + const series = createMockSeries('scatter', [ + { x: 1.5, value: 3.2 }, + { x: 2.1, value: 4.8 }, + ]); + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { id: 'scatter-test' }); + + const layer = result!.subplots[0][0].layers[0]; + expect(layer.type).toBe(TraceType.SCATTER); + expect((layer.data as any[])[0]).toEqual({ x: 1.5, y: 3.2 }); + }); +}); + +// --------------------------------------------------------------------------- +// anyChartToMaidr – BOX +// --------------------------------------------------------------------------- + +describe('anyChartToMaidr – BOX', () => { + it('generates box layer with quartile data', () => { + const series = createMockSeries('box', [ + { x: 'Group1', lowest: 10, q1: 25, median: 50, q3: 75, highest: 90 }, + { x: 'Group2', lowest: 5, q1: 20, median: 40, q3: 60, highest: 85 }, + ]); + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { id: 'box-test' }); + + const layer = result!.subplots[0][0].layers[0]; + expect(layer.type).toBe(TraceType.BOX); + const data = layer.data as any[]; + expect(data[0]).toMatchObject({ + fill: 'Group1', + min: 10, + q1: 25, + q2: 50, + q3: 75, + max: 90, + lowerOutliers: [], + upperOutliers: [], + }); + }); +}); + +// --------------------------------------------------------------------------- +// anyChartToMaidr – HEATMAP +// --------------------------------------------------------------------------- + +describe('anyChartToMaidr – HEATMAP', () => { + it('generates heatmap with 2D point matrix', () => { + const series = createMockSeries('heatmap', [ + { x: 'A', y: 'R1', value: 10 }, + { x: 'B', y: 'R1', value: 20 }, + { x: 'A', y: 'R2', value: 30 }, + { x: 'B', y: 'R2', value: 40 }, + ]); + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { id: 'heat-test' }); + + const layer = result!.subplots[0][0].layers[0]; + expect(layer.type).toBe(TraceType.HEATMAP); + + const data = layer.data as HeatmapData; + expect(data.x).toEqual(['A', 'B']); + expect(data.y).toEqual(['R1', 'R2']); + expect(data.points).toEqual([ + [10, 20], + [30, 40], + ]); + }); +}); + +// --------------------------------------------------------------------------- +// anyChartToMaidr – CANDLESTICK +// --------------------------------------------------------------------------- + +describe('anyChartToMaidr – CANDLESTICK', () => { + it('generates candlestick layer with OHLC data and trend', () => { + const series = createMockSeries('candlestick', [ + { x: '2024-01-01', open: 100, high: 110, low: 95, close: 105, volume: 1000 }, + { x: '2024-01-02', open: 105, high: 108, low: 98, close: 99, volume: 800 }, + { x: '2024-01-03', open: 100, high: 105, low: 100, close: 100, volume: 500 }, + ]); + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { id: 'candle-test' }); + + const layer = result!.subplots[0][0].layers[0]; + expect(layer.type).toBe(TraceType.CANDLESTICK); + + const data = layer.data as CandlestickPoint[]; + expect(data).toHaveLength(3); + // Bull: close > open + expect(data[0].trend).toBe('Bull'); + expect(data[0].open).toBe(100); + expect(data[0].close).toBe(105); + // Bear: close < open + expect(data[1].trend).toBe('Bear'); + // Neutral: close == open + expect(data[2].trend).toBe('Neutral'); + }); +}); + +// --------------------------------------------------------------------------- +// anyChartToMaidr – HISTOGRAM +// --------------------------------------------------------------------------- + +describe('anyChartToMaidr – HISTOGRAM', () => { + it('generates histogram layer with bin data', () => { + const series = createMockSeries('column', [ + { x: 1.5, value: 10 }, + { x: 2.5, value: 20 }, + ]); + // Force histogram type via direct call to anyChartToMaidr + // Since there's no 'histogram' seriesType in AnyChart, we test the builder + // through a custom mapping. For now, test the BAR output from column. + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { id: 'hist-test' }); + + const layer = result!.subplots[0][0].layers[0]; + expect(layer.type).toBe(TraceType.BAR); + const data = layer.data as HistogramPoint[]; + expect(data).toHaveLength(2); + }); +}); + +// --------------------------------------------------------------------------- +// anyChartToMaidr – options handling +// --------------------------------------------------------------------------- + +describe('anyChartToMaidr – options', () => { + it('applies title and axis overrides', () => { + const series = createMockSeries('bar', [{ x: 'A', value: 1 }]); + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { + id: 'opts-test', + title: 'Custom Title', + axes: { x: 'X Label', y: 'Y Label' }, + }); + + expect(result!.title).toBe('Custom Title'); + const layer = result!.subplots[0][0].layers[0]; + expect(layer.axes).toEqual({ x: 'X Label', y: 'Y Label' }); + }); + + it('applies single shared selector to layers', () => { + const series = createMockSeries('bar', [{ x: 'A', value: 1 }]); + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { + id: 'sel-test', + selectors: ['.my-bar rect'], + }); + + const layer = result!.subplots[0][0].layers[0]; + expect(layer.selectors).toBe('.my-bar rect'); + }); + + it('omits selectors when none provided', () => { + const series = createMockSeries('bar', [{ x: 'A', value: 1 }]); + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { id: 'no-sel' }); + + const layer = result!.subplots[0][0].layers[0]; + expect(layer.selectors).toBeUndefined(); + }); + + it('skips unsupported series types with warning', () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + const series = createMockSeries('pie', [{ x: 'A', value: 1 }]); + const chart = createMockChart([series]); + const result = anyChartToMaidr(chart, { id: 'unsup-test' }); + + expect(result).toBeNull(); + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Unsupported AnyChart series type "pie"'), + ); + consoleSpy.mockRestore(); + }); +}); + +// --------------------------------------------------------------------------- +// anyChartToMaidr – multi-series +// --------------------------------------------------------------------------- + +describe('anyChartToMaidr – multi-series', () => { + it('handles multiple series of different types', () => { + const barSeries = createMockSeries('bar', [ + { x: 'A', value: 10 }, + ]); + const lineSeries = createMockSeries('line', [ + { x: 1, value: 20 }, + ]); + const chart = createMockChart([barSeries, lineSeries]); + const result = anyChartToMaidr(chart, { id: 'multi-test' }); + + expect(result).not.toBeNull(); + const layers = result!.subplots[0][0].layers; + expect(layers).toHaveLength(2); + expect(layers[0].type).toBe(TraceType.BAR); + expect(layers[1].type).toBe(TraceType.LINE); + }); + + it('applies per-series selectors', () => { + const s1 = createMockSeries('bar', [{ x: 'A', value: 10 }]); + const s2 = createMockSeries('line', [{ x: 1, value: 20 }]); + const chart = createMockChart([s1, s2]); + const result = anyChartToMaidr(chart, { + id: 'per-sel', + selectors: ['.bar-el rect', undefined], + }); + + const layers = result!.subplots[0][0].layers; + expect(layers[0].selectors).toBe('.bar-el rect'); + expect(layers[1].selectors).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// bindAnyChart – DOM behaviour +// +// These tests use a minimal mock DOM since the default Jest environment is +// `node` and `jest-environment-jsdom` is not installed. +// --------------------------------------------------------------------------- + +/** + * Creates a lightweight mock HTMLElement sufficient for bindAnyChart tests. + * + * Uses `Object.create(HTMLElement.prototype)` so the adapter's + * `container instanceof HTMLElement` check passes in the Node environment. + */ +function createMockHTMLElement(id: string): HTMLElement { + const attrs = new Map(); + const listeners = new Map void>>(); + + const el = Object.create(HTMLElement.prototype); + el.id = id; + el.getAttribute = (name: string) => attrs.get(name) ?? null; + el.setAttribute = (name: string, value: string) => attrs.set(name, value); + el.addEventListener = (type: string, handler: (evt: any) => void) => { + if (!listeners.has(type)) + listeners.set(type, []); + listeners.get(type)!.push(handler); + }; + el.dispatchEvent = (event: { type: string; [key: string]: any }) => { + const handlers = listeners.get(event.type) ?? []; + for (const h of handlers) + h(event); + return true; + }; + + return el as HTMLElement; +} + +describe('bindAnyChart', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + function chartWithMockContainer( + series: AnyChartSeries[], + containerEl: HTMLElement, + ): AnyChartInstance { + return createMockChart(series, { + container: () => containerEl as any, + }); + } + + it('sets maidr-data attribute on the container', () => { + const containerEl = createMockHTMLElement('test-container'); + const series = createMockSeries('bar', [{ x: 'A', value: 1 }]); + const chart = chartWithMockContainer([series], containerEl); + + bindAnyChart(chart, { id: 'bind-test' }); + + const attr = containerEl.getAttribute('maidr-data'); + expect(attr).not.toBeNull(); + const parsed = JSON.parse(attr!); + expect(parsed.id).toBe('bind-test'); + }); + + it('dispatches maidr:bindchart event on the container', () => { + const containerEl = createMockHTMLElement('event-container'); + const series = createMockSeries('bar', [{ x: 'A', value: 1 }]); + const chart = chartWithMockContainer([series], containerEl); + + const eventSpy = jest.fn(); + containerEl.addEventListener('maidr:bindchart', eventSpy); + + bindAnyChart(chart, { id: 'event-test' }); + + expect(eventSpy).toHaveBeenCalledTimes(1); + const event = eventSpy.mock.calls[0][0]; + expect(event.detail.id).toBe('event-test'); + }); + + it('returns null when container cannot be resolved', () => { + const series = createMockSeries('bar', [{ x: 'A', value: 1 }]); + const chart = createMockChart([series], { + container: () => null as any, + }); + + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const result = bindAnyChart(chart, { id: 'no-container' }); + + expect(result).toBeNull(); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Could not find the chart container element'), + ); + }); + + it('guards against double-initialisation on the same container', () => { + const containerEl = createMockHTMLElement('double-container'); + const series = createMockSeries('bar', [{ x: 'A', value: 1 }]); + const chart = chartWithMockContainer([series], containerEl); + + const eventSpy = jest.fn(); + containerEl.addEventListener('maidr:bindchart', eventSpy); + + const first = bindAnyChart(chart, { id: 'double-test' }); + const second = bindAnyChart(chart, { id: 'double-test' }); + + // Event should fire only once. + expect(eventSpy).toHaveBeenCalledTimes(1); + // Both calls return a valid Maidr object. + expect(first).not.toBeNull(); + expect(second).not.toBeNull(); + expect(first!.id).toBe(second!.id); + }); +}); diff --git a/test/adapter/setup-dom.ts b/test/adapter/setup-dom.ts new file mode 100644 index 00000000..70ef3ef0 --- /dev/null +++ b/test/adapter/setup-dom.ts @@ -0,0 +1,14 @@ +/** + * Minimal DOM polyfill for the Node test environment. + * + * The AnyChart adapter uses `instanceof HTMLElement` at runtime to distinguish + * real DOM elements from AnyChart stage wrappers. In Node (the default Jest + * environment) `HTMLElement` is not a global, so we provide a stand-in before + * the adapter module is imported. + * + * This file must be imported before any adapter imports in test files. + */ + +if (typeof globalThis.HTMLElement === 'undefined') { + (globalThis as any).HTMLElement = class HTMLElement {}; +} diff --git a/vite.anychart.config.ts b/vite.anychart.config.ts new file mode 100644 index 00000000..7371ae64 --- /dev/null +++ b/vite.anychart.config.ts @@ -0,0 +1,45 @@ +import path from 'node:path'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +export default defineConfig({ + plugins: [ + dts({ + tsconfigPath: './tsconfig.build.json', + rollupTypes: true, + insertTypesEntry: false, + }), + ], + build: { + lib: { + entry: path.resolve(__dirname, 'src/anychart-entry.ts'), + formats: ['es'], + fileName: () => 'anychart.mjs', + }, + sourcemap: true, + outDir: 'dist', + emptyOutDir: false, + rollupOptions: { + onwarn(warning, warn) { + if (warning.code === 'MODULE_LEVEL_DIRECTIVE') { + return; + } + warn(warning); + }, + }, + }, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + resolve: { + alias: { + '@command': path.resolve(__dirname, 'src/command'), + '@model': path.resolve(__dirname, 'src/model'), + '@state': path.resolve(__dirname, 'src/state'), + '@service': path.resolve(__dirname, 'src/service'), + '@type': path.resolve(__dirname, 'src/type'), + '@ui': path.resolve(__dirname, 'src/ui'), + '@util': path.resolve(__dirname, 'src/util'), + }, + }, +});