Files
Steffen Becker 2380ed699b feat: an ODF chart object is drawn too
<chart:chart> in the object's sub-document, read through the same picture as
the DOCX leg: series values by cell range out of its own local-table, colours
from the series style, bounds from the axis style.
2026-08-08 23:54:59 +00:00

115 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// A DrawingML chart part drawn as an SVG picture (import/chart.ts): series values,
// the file's own axis bounds, and the 1..n categories a chart without any gets.
import { describe, it, expect } from 'vitest';
import { chartDataUrl, odfChartDataUrl } from '../../src/lib/import/chart';
const C = 'xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"';
const A = 'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"';
const points = (vals: number[]) =>
`<c:numCache><c:ptCount val="${vals.length}"/>` +
vals.map((v, i) => `<c:pt idx="${i}"><c:v>${v}</c:v></c:pt>`).join('') + '</c:numCache>';
const chartXml = (plot: string, opts: { max?: string; cats?: string[] } = {}) =>
`<c:chartSpace ${C} ${A}><c:chart>` +
`<c:title><c:tx><c:rich><a:p><a:r><a:t>Phase Std</a:t></a:r></a:p></c:rich></c:tx></c:title>` +
`<c:plotArea><${plot}><c:ser><c:spPr><a:solidFill><a:schemeClr val="accent2"/></a:solidFill></c:spPr>` +
(opts.cats ? `<c:cat><c:strCache><c:ptCount val="${opts.cats.length}"/>` +
opts.cats.map((t, i) => `<c:pt idx="${i}"><c:v>${t}</c:v></c:pt>`).join('') + '</c:strCache></c:cat>' : '') +
`<c:val><c:numRef>${points([1, 2, 3])}</c:numRef></c:val></c:ser></${plot}>` +
`<c:catAx><c:title><c:tx><c:rich><a:p><a:r><a:t>interferogram</a:t></a:r></a:p></c:rich></c:tx></c:title></c:catAx>` +
`<c:valAx><c:scaling><c:orientation val="minMax"/>${opts.max ? `<c:max val="${opts.max}"/>` : ''}</c:scaling>` +
`<c:majorGridlines/></c:valAx>` +
`</c:plotArea></c:chart></c:chartSpace>`;
const svgOf = (xml: string, accents: string[] = []) => {
const url = chartDataUrl(xml, 400, 300, accents);
expect(url?.startsWith('data:image/svg+xml')).toBe(true);
return decodeURIComponent(url!.replace(/^data:image\/svg\+xml;charset=utf-8,/, ''));
};
describe('a chart part', () => {
it('draws its bars, title, axis title and the theme accent it names', () => {
const svg = svgOf(chartXml('c:barChart'), ['#111111', '#22AA33']);
expect(svg).toContain('Phase Std');
expect(svg).toContain('interferogram');
expect(svg).toContain('#22AA33');
expect((svg.match(/<rect /g) ?? []).length).toBe(4); // one background + three bars
});
it('takes the value axis bound from the file, not from the data', () => {
expect(svgOf(chartXml('c:barChart', { max: '10' }))).toContain('>10<');
expect(svgOf(chartXml('c:barChart'))).not.toContain('>10<');
});
it('numbers the categories 1..n when the file names none, and keeps the ones it does', () => {
const plain = svgOf(chartXml('c:barChart'));
expect(plain).toContain('>1<');
expect(plain).toContain('>3<');
expect(svgOf(chartXml('c:barChart', { cats: ['Mo', 'Di', 'Mi'] }))).toContain('>Di<');
});
it('draws a line and a pie chart, and declines a plot it cannot draw', () => {
expect(svgOf(chartXml('c:lineChart'))).toContain('<polyline');
expect(svgOf(chartXml('c:pieChart'))).toContain('<path');
expect(chartDataUrl(chartXml('c:surfaceChart'), 400, 300, [])).toBeNull();
expect(chartDataUrl('not xml at all', 400, 300, [])).toBeNull();
});
});
// ---- ODF charts: <chart:chart> plus the local table that holds its numbers ----
const NSDECL = 'xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0"'
+ ' xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"'
+ ' xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"'
+ ' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"'
+ ' xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"'
+ ' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"';
const row = (cells: string[]) => `<table:table-row>${cells.map((c) => c).join('')}</table:table-row>`;
const strCell = (s: string) => `<table:table-cell office:value-type="string"><text:p>${s}</text:p></table:table-cell>`;
const numCell = (v: number) => `<table:table-cell office:value-type="float" office:value="${v}"><text:p>${v}</text:p></table:table-cell>`;
const odfChart = (cls: string) =>
`<office:document-content ${NSDECL}><office:automatic-styles>` +
`<style:style style:name="ch6" style:family="chart"><style:chart-properties chart:maximum="4"/></style:style>` +
`<style:style style:name="ch9" style:family="chart"><style:graphic-properties draw:fill-color="#22AA33"/></style:style>` +
`</office:automatic-styles><office:body><office:chart>` +
`<chart:chart chart:class="${cls}"><chart:title><text:p>Phase Std</text:p></chart:title>` +
`<chart:plot-area>` +
`<chart:axis chart:dimension="x"><chart:title><text:p>interferogram</text:p></chart:title>` +
`<chart:categories table:cell-range-address="local-table.$A$2:.$A$4"/></chart:axis>` +
`<chart:axis chart:dimension="y" chart:style-name="ch6"><chart:grid chart:class="major"/></chart:axis>` +
`<chart:series chart:style-name="ch9" chart:values-cell-range-address="local-table.$B$2:.$B$4"` +
` chart:label-cell-address="local-table.$B$1"/>` +
`</chart:plot-area>` +
`<table:table table:name="local-table">` +
row([strCell(''), strCell('Series1')]) +
row([strCell('Mo'), numCell(1)]) + row([strCell('Di'), numCell(2)]) + row([strCell('Mi'), numCell(3)]) +
`</table:table></chart:chart></office:chart></office:body></office:document-content>`;
const odfSvg = (cls: string) => {
const doc = new DOMParser().parseFromString(odfChart(cls), 'application/xml');
const url = odfChartDataUrl(doc, 400, 300);
expect(url?.startsWith('data:image/svg+xml')).toBe(true);
return decodeURIComponent(url!.replace(/^data:image\/svg\+xml;charset=utf-8,/, ''));
};
describe('an ODF chart object', () => {
it('draws its series from the local table, in the file’s own colour', () => {
const svg = odfSvg('chart:bar');
expect(svg).toContain('Phase Std');
expect(svg).toContain('interferogram');
expect(svg).toContain('#22AA33');
expect(svg).toContain('>Di<');
expect((svg.match(/<rect /g) ?? []).length).toBe(4); // one background + three bars
});
it('takes the value axis bound from the axis style, and declines an unknown class', () => {
expect(odfSvg('chart:bar')).toContain('>4<');
const doc = new DOMParser().parseFromString(odfChart('chart:surface'), 'application/xml');
expect(odfChartDataUrl(doc, 400, 300)).toBeNull();
});
});