diff --git a/packages/layout-engine/painters/dom/src/features/chart/chart.test.ts b/packages/layout-engine/painters/dom/src/features/chart/chart.test.ts new file mode 100644 index 0000000000..2018690e59 --- /dev/null +++ b/packages/layout-engine/painters/dom/src/features/chart/chart.test.ts @@ -0,0 +1,91 @@ +/** + * Smoke tests for the chart feature module public API. + * Verifies that all exports are correctly re-exported through the feature + * barrel and that the module handles every registered chart type without + * throwing or returning a generic placeholder. + * + * Full rendering correctness is covered by chart-renderer.test.ts. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { JSDOM } from 'jsdom'; +import { createChartElement, createChartPlaceholder, formatTickValue } from './index.js'; +import type { ChartModel, DrawingGeometry } from '@superdoc/contracts'; + +let doc: Document; + +beforeEach(() => { + doc = new JSDOM('').window.document; +}); + +const geometry: DrawingGeometry = { width: 400, height: 300, rotation: 0, flipH: false, flipV: false }; + +const REGISTERED_CHART_TYPES: ChartModel['chartType'][] = [ + 'barChart', + 'lineChart', + 'stockChart', + 'areaChart', + 'scatterChart', + 'bubbleChart', + 'radarChart', + 'pieChart', + 'doughnutChart', + 'ofPieChart', +]; + +function makeChart(chartType: ChartModel['chartType']): ChartModel { + return { + chartType, + series: [ + { name: 'S1', categories: ['A', 'B', 'C'], values: [1, 2, 3], xValues: [1, 2, 3], bubbleSizes: [1, 2, 3] }, + ], + legendPosition: 'b', + barDirection: 'col', + }; +} + +describe('chart feature module exports', () => { + it('exports createChartElement as a function', () => { + expect(typeof createChartElement).toBe('function'); + }); + + it('exports createChartPlaceholder as a function', () => { + expect(typeof createChartPlaceholder).toBe('function'); + }); + + it('exports formatTickValue as a function', () => { + expect(typeof formatTickValue).toBe('function'); + }); +}); + +describe('createChartElement via feature module', () => { + it('returns a superdoc-chart element', () => { + const el = createChartElement(doc, makeChart('barChart'), geometry); + expect(el.classList.contains('superdoc-chart')).toBe(true); + }); + + it('shows placeholder when chart data is missing', () => { + const el = createChartElement(doc, undefined, geometry); + expect(el.textContent).toContain('No chart data'); + }); + + it.each(REGISTERED_CHART_TYPES)('renders %s without throwing', (chartType) => { + const el = createChartElement(doc, makeChart(chartType), geometry); + expect(el.classList.contains('superdoc-chart')).toBe(true); + expect(el.textContent).not.toContain(`Chart: ${chartType}`); + }); +}); + +describe('createChartPlaceholder via feature module', () => { + it('renders the label text', () => { + const container = doc.createElement('div'); + const el = createChartPlaceholder(doc, container, 'Test label'); + expect(el.textContent).toContain('Test label'); + }); +}); + +describe('formatTickValue via feature module', () => { + it('formats thousands', () => expect(formatTickValue(1_500)).toBe('1.5K')); + it('formats millions', () => expect(formatTickValue(2_000_000)).toBe('2.0M')); + it('formats plain numbers', () => expect(formatTickValue(42)).toBe('42')); +}); diff --git a/packages/layout-engine/painters/dom/src/features/chart/index.ts b/packages/layout-engine/painters/dom/src/features/chart/index.ts new file mode 100644 index 0000000000..540f09156e --- /dev/null +++ b/packages/layout-engine/painters/dom/src/features/chart/index.ts @@ -0,0 +1,26 @@ +/** + * Chart — rendering feature module + * + * Renders DrawingML chart blocks as inline SVG elements. + * Supports bar/column, line, area, pie, doughnut, scatter, bubble, + * radar, and stock charts, with a placeholder fallback for unsupported types. + * + * Performance guardrails: + * - Max 20 rendered series + * - Max 500 data points per series + * - Max 5,000 SVG elements per chart + * + * @ooxml c:barChart — bar and column charts (ECMA-376 §21.2.2.16) + * @ooxml c:lineChart — line charts (ECMA-376 §21.2.2.81) + * @ooxml c:stockChart — stock charts (ECMA-376 §21.2.2.157) + * @ooxml c:areaChart — area charts (ECMA-376 §21.2.2.1) + * @ooxml c:scatterChart — scatter charts (ECMA-376 §21.2.2.147) + * @ooxml c:bubbleChart — bubble charts (ECMA-376 §21.2.2.20) + * @ooxml c:radarChart — radar charts (ECMA-376 §21.2.2.132) + * @ooxml c:pieChart — pie charts (ECMA-376 §21.2.2.126) + * @ooxml c:doughnutChart — doughnut charts (ECMA-376 §21.2.2.50) + * @ooxml c:ofPieChart — bar-of-pie / pie-of-pie charts (ECMA-376 §21.2.2.111) + * @spec ECMA-376 §21.2 (DrawingML Charts) + */ + +export { createChartElement, createChartPlaceholder, formatTickValue } from '../../chart-renderer.js'; diff --git a/packages/layout-engine/painters/dom/src/features/feature-registry.ts b/packages/layout-engine/painters/dom/src/features/feature-registry.ts index f5fe64343f..d717b12dfd 100644 --- a/packages/layout-engine/painters/dom/src/features/feature-registry.ts +++ b/packages/layout-engine/painters/dom/src/features/feature-registry.ts @@ -76,4 +76,24 @@ export const RENDERING_FEATURES = { ], spec: '§22.1', }, + + // ─── Charts ─────────────────────────────────────────────────── + // @spec ECMA-376 §21.2 (DrawingML Charts) + 'c:chart': { + feature: 'chart', + module: './chart', + handles: [ + 'c:barChart', + 'c:lineChart', + 'c:stockChart', + 'c:areaChart', + 'c:scatterChart', + 'c:bubbleChart', + 'c:radarChart', + 'c:pieChart', + 'c:doughnutChart', + 'c:ofPieChart', + ], + spec: '§21.2', + }, } as const; diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index b1c9f832bf..6f36c15ae0 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -50,7 +50,7 @@ import { import { DATASET_KEYS, decodeLayoutStoryDataset, encodeLayoutStoryDataset } from '@superdoc/dom-contract'; import { getPresetShapeSvg } from '@superdoc/preset-geometry'; import { DOM_CLASS_NAMES } from './constants.js'; -import { createChartElement as renderChartToElement } from './chart-renderer.js'; +import { createChartElement as renderChartToElement } from './features/chart/index.js'; import { createRulerElement, ensureRulerStyles, generateRulerDefinitionFromPx } from './ruler/index.js'; import { CLASS_NAMES,