Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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('<!DOCTYPE html><html><body></body></html>').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'));
});
26 changes: 26 additions & 0 deletions packages/layout-engine/painters/dom/src/features/chart/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion packages/layout-engine/painters/dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down