This is an automated email from the ASF dual-hosted git repository. ovilia pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/echarts-from-mermaid.git
commit 68f6c12150959e900ab2f5a1259932509914db4b Author: Ovilia <[email protected]> AuthorDate: Thu Mar 20 17:14:27 2025 +0800 feat(xy-chart); basic xy-chart done --- src/__tests__/config.test.ts | 170 --------------------- src/__tests__/xychart.test.ts | 135 ---------------- src/base/parseAxis.test.ts | 6 +- src/base/parseAxis.ts | 7 +- src/base/util.test.ts | 34 ++++- src/base/util.ts | 33 ++++ .../pie.test.ts => mermaid/pie/PieChart.test.ts} | 2 +- src/mermaid/xy/XYChart.test.ts | 132 ++++++++++++++++ src/mermaid/xy/XYChart.ts | 41 +++-- 9 files changed, 227 insertions(+), 333 deletions(-) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts deleted file mode 100644 index 4342cfb..0000000 --- a/src/__tests__/config.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -// import { XYChart } from '../mermaid/xychart'; - -// describe('Config', () => { -// it('should work with width and height', () => { -// const definition = ` -// --- -// config: -// xyChart: -// width: 900 -// height: 600 -// themeVariables: -// xyChart: -// titleColor: "#ff0000" -// --- -// xychart-beta -// bar [1, 4, 2] -// `; -// const chart = new XYChart(definition); -// expect(chart.getOption()).toEqual({ -// grid: { -// width: 900, -// height: 600, -// }, -// title: { -// text: 'This is a simple example', -// textStyle: { -// color: '#ff0000', -// }, -// }, -// xAxis: { -// type: 'value', -// }, -// yAxis: { -// type: 'value', -// }, -// series: [ -// { -// type: 'bar', -// data: [ -// [1, 1], -// [2, 4], -// [3, 2], -// ], -// }, -// ], -// }); -// }); - -// it('should work with axis configurations', () => { -// const definition = ` -// --- -// config: -// xyChart: -// xAxis: -// labelFontSize: 16 -// labelPadding: 8 -// showTitle: false -// tickLength: 8 -// showAxisLine: false -// yAxis: -// showLabel: false -// titleFontSize: 18 -// showTick: false -// axisLineWidth: 3 -// --- -// xychart-beta -// bar [1, 4, 2] -// `; -// const chart = new XYChart(definition); -// expect(chart.getOption()).toEqual({ -// xAxis: { -// type: 'value', -// show: true, -// axisLabel: { -// show: true, -// fontSize: 16, -// margin: 8, -// }, -// axisTick: { -// show: true, -// length: 8, -// }, -// axisLine: { -// show: false, -// }, -// name: '', // due to showTitle: false -// nameShow: false, -// }, -// yAxis: { -// type: 'value', -// show: true, -// axisLabel: { -// show: false, -// }, -// name: '', -// nameTextStyle: { -// fontSize: 18, -// }, -// axisTick: { -// show: false, -// }, -// axisLine: { -// show: true, -// lineStyle: { -// width: 3, -// }, -// }, -// }, -// series: [ -// { -// type: 'bar', -// data: [ -// [1, 1], -// [2, 4], -// [3, 2], -// ], -// }, -// ], -// }); -// }); - -// it('should work with theme variables', () => { -// const definition = ` -// --- -// config: -// themeVariables: -// xyChart: -// backgroundColor: "#f0f0f0" -// xAxisLabelColor: "#333" -// yAxisLineColor: "#666" -// plotColorPalette: "#f3456,#43445" -// --- -// xychart-beta -// bar [1, 4, 2] -// `; -// const chart = new XYChart(definition); -// expect(chart.getOption()).toEqual({ -// backgroundColor: '#f0f0f0', -// xAxis: { -// type: 'value', -// axisLabel: { -// show: true, -// textStyle: { -// color: '#333', -// }, -// }, -// }, -// yAxis: { -// type: 'value', -// axisLine: { -// show: true, -// lineStyle: { -// color: '#666', -// }, -// }, -// }, -// color: ['#f3456', '#43445'], -// series: [ -// { -// type: 'bar', -// data: [ -// [1, 1], -// [2, 4], -// [3, 2], -// ], -// }, -// ], -// }); -// }); -// }); diff --git a/src/__tests__/xychart.test.ts b/src/__tests__/xychart.test.ts deleted file mode 100644 index 439244d..0000000 --- a/src/__tests__/xychart.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { EChartsFromMermaid } from '../index'; - -const chartTypes = [ - 'line', - // 'bar' -]; - -describe('XYChart', () => { - for (const chartType of chartTypes) { - describe(chartType, () => { - describe('orientation', () => { - // it('should render vertical chart', () => { - // const definition = ` - // xychart-beta - // ${chartType} [1, 4, 3] - // `; - // const option = EChartsFromMermaid.getOption(definition); - // expect(option).toEqual({ - // xAxis: { - // type: 'value', - // }, - // yAxis: { - // type: 'value', - // }, - // series: [ - // { - // type: chartType, - // data: [ - // [1, 1], - // [2, 4], - // [3, 3], - // ], - // }, - // ], - // }); - // }); - // it('should render horizontal chart', () => { - // const definition = ` - // xychart-beta horizontal - // ${chartType} [1, 4, 3] - // `; - // const option = EChartsFromMermaid.getOption(definition); - // expect(option).toEqual({ - // xAxis: { type: 'value' }, - // yAxis: { type: 'value' }, - // series: [ - // { - // type: chartType, - // data: [ - // [1, 1], - // [4, 2], - // [3, 3], - // ], - // }, - // ], - // }); - // }); - }); - - const axisTypes = ['x' /*, 'y'*/]; - for (const axisType of axisTypes) { - describe(axisType, () => { - const otherAxis = axisType === 'x' ? 'y' : 'x'; - // it(`should render category ${axisType}Axis`, () => { - // const definition = ` - // xychart-beta - // ${axisType}-axis [cat1, "cat2 with space", cat3] - // ${chartType} [1, 4, 2] - // `; - // const option = EChartsFromMermaid.getOption(definition); - // expect(option).toEqual({ - // [axisType + 'Axis']: { - // type: 'category', - // data: ['cat1', 'cat2 with space', 'cat3'], - // }, - // [otherAxis + 'Axis']: { type: 'value' }, - // series: [ - // { - // type: chartType, - // data: [1, 4, 2], - // }, - // ], - // }); - // }); - - it(`should render value ${axisType}Axis with range`, () => { - const definition = ` - xychart-beta - ${axisType}-axis 100 --> 200 - ${chartType} [1, 4, 2] - `; - const option = EChartsFromMermaid.getOption(definition); - expect(option).toEqual({ - [axisType + 'Axis']: { type: 'value', min: 100, max: 200 }, - [otherAxis + 'Axis']: { type: 'value' }, - series: [ - { - type: chartType, - data: [ - [100, 1], - [150, 4], - [200, 2], - ], - }, - ], - }); - }); - - // it(`should work with ${axisType}Axis title`, () => { - // const definition = ` - // xychart-beta - // ${axisType}-axis "Cats" [cat1, "cat2 with space", cat3] - // ${chartType} [1, 4, 2] - // `; - // const option = EChartsFromMermaid.getOption(definition); - // expect(option).toEqual({ - // [axisType + 'Axis']: { - // type: 'category', - // name: 'Cats', - // value: ['cat1', 'cat2 with space', 'cat3'], - // }, - // [otherAxis + 'Axis']: { type: 'value' }, - // series: [ - // { - // type: chartType, - // data: [1, 4, 2], - // }, - // ], - // }); - // }); - }); - } - }); - } -}); diff --git a/src/base/parseAxis.test.ts b/src/base/parseAxis.test.ts index dd3c24d..44c5acc 100644 --- a/src/base/parseAxis.test.ts +++ b/src/base/parseAxis.test.ts @@ -6,7 +6,7 @@ describe('parseAxis', () => { expect(axis).toEqual({ key: 'x', type: 'category', - categories: ['cat1', 'cat2', 'cat3'], + data: ['cat1', 'cat2', 'cat3'], }); }); @@ -15,7 +15,7 @@ describe('parseAxis', () => { expect(axis).toEqual({ key: 'x', type: 'category', - categories: ['cat1', 'cat2', 'cat3'], + data: ['cat1', 'cat2', 'cat3'], }); }); @@ -35,7 +35,7 @@ describe('parseAxis', () => { key: 'x', type: 'category', name: 'Title', - categories: ['cat1', 'cat2', 'cat3'], + data: ['cat1', 'cat2', 'cat3'], }); }); diff --git a/src/base/parseAxis.ts b/src/base/parseAxis.ts index d8ad583..d44742b 100644 --- a/src/base/parseAxis.ts +++ b/src/base/parseAxis.ts @@ -12,7 +12,6 @@ export interface AxisDefinition { const axisKeys = ['x', 'y'] as const; export function parseAxis(line: string): AxisDefinition | null { - console.log('parseAxis', line); let key: 'x' | 'y' | undefined; for (const axisKey of axisKeys) { if (line.startsWith(axisKey + '-axis')) { @@ -31,9 +30,9 @@ export function parseAxis(line: string): AxisDefinition | null { * y-axis "Time trained (minutes)" 0 --> 300 */ const rest = line.substring((key + '-axis').length).trim(); - const titleRe = /^\s*"[^"]*"\s+/.exec(rest); - const title = titleRe ? titleRe[0].trim() : undefined; - const data = title ? rest.substring(title.length) : rest; + const titleRe = /^\s*"([^"]*)"\s+/.exec(rest); + const title = titleRe ? titleRe[1] : undefined; + const data = titleRe ? rest.substring(titleRe[0].length) : rest; const arrayResult = parseArray(data); const result: AxisDefinition = { diff --git a/src/base/util.test.ts b/src/base/util.test.ts index 64f5c54..bf084ba 100644 --- a/src/base/util.test.ts +++ b/src/base/util.test.ts @@ -1,24 +1,24 @@ -import { parseArray } from './util'; +import { convert2dData, parseArray } from './util'; describe('parseArray', () => { it('should parse array', () => { expect(parseArray('[abc, de, fg, 34]')).toEqual({ type: 'category', - categories: ['abc', 'de', 'fg', '34'], + data: ['abc', 'de', 'fg', '34'], }); }); it('should parse array awalys as string', () => { expect(parseArray('[1, 2, 3]')).toEqual({ type: 'category', - categories: ['1', '2', '3'], + data: ['1', '2', '3'], }); }); it('should parse array with quotes', () => { expect(parseArray('["abc", "de", "fg", "34"]')).toEqual({ type: 'category', - categories: ['abc', 'de', 'fg', '34'], + data: ['abc', 'de', 'fg', '34'], }); }); @@ -30,3 +30,29 @@ describe('parseArray', () => { }); }); }); + +describe('convert2dData', () => { + it('should convert 1d data into 2d data, horizontal false', () => { + expect(convert2dData(100, 300, [2, 4, 6], false)).toEqual([ + [100, 2], + [200, 4], + [300, 6], + ]); + }); + + it('should convert 1d data into 2d data, horizontal true', () => { + expect(convert2dData(100, 300, [2, 4, 6], true)).toEqual([ + [2, 100], + [4, 200], + [6, 300], + ]); + }); + + it('should convert 1d data into 2d data, horizontal false, single data', () => { + expect(convert2dData(100, 300, [2], false)).toEqual([[100, 2]]); + }); + + it('should convert 1d data into 2d data, horizontal true, single data', () => { + expect(convert2dData(100, 300, [2], true)).toEqual([[2, 100]]); + }); +}); diff --git a/src/base/util.ts b/src/base/util.ts index 57421ad..0a9e299 100644 --- a/src/base/util.ts +++ b/src/base/util.ts @@ -31,3 +31,36 @@ export function parseArray(str: string): ParseArrayResult { .map((s) => s.replace(/^"|"$/g, '')), }; } + +/** + * Convert 1d data into 2d data, linear the first dimension from xMin to xMax, + * and the second dimension is the data. + * For example, min 100, max 300 data [2, 4, 6], + * isHorizontal false: returns [[100, 2], [200, 4], [300, 6]] + * isHorizontal true: returns [[2, 100], [4, 200], [6, 300]] + * + * @param xMin + * @param xMax + * @param data + * @param isHorizontal + */ +export function convert2dData( + xMin: number, + xMax: number, + data: number[], + isHorizontal: boolean +) { + if (data.length < 1) { + return []; + } else if (data.length === 1) { + return isHorizontal ? [[data[0], xMin]] : [[xMin, data[0]]]; + } + const result = []; + const step = (xMax - xMin) / (data.length - 1); + for (let i = 0; i < data.length; i++) { + result.push( + isHorizontal ? [data[i], xMin + i * step] : [xMin + i * step, data[i]] + ); + } + return result; +} diff --git a/src/__tests__/pie.test.ts b/src/mermaid/pie/PieChart.test.ts similarity index 95% rename from src/__tests__/pie.test.ts rename to src/mermaid/pie/PieChart.test.ts index 81d6437..25a6e0c 100644 --- a/src/__tests__/pie.test.ts +++ b/src/mermaid/pie/PieChart.test.ts @@ -1,4 +1,4 @@ -import { EChartsFromMermaid } from '../index'; +import { EChartsFromMermaid } from '../../index'; describe('Pie Chart', () => { it('should parse basic pie chart correctly', () => { diff --git a/src/mermaid/xy/XYChart.test.ts b/src/mermaid/xy/XYChart.test.ts new file mode 100644 index 0000000..113e4aa --- /dev/null +++ b/src/mermaid/xy/XYChart.test.ts @@ -0,0 +1,132 @@ +import { EChartsFromMermaid } from '../../index'; + +const chartTypes = ['line', 'bar']; + +describe('XYChart', () => { + for (const chartType of chartTypes) { + describe(chartType, () => { + describe('orientation', () => { + it('should render vertical chart', () => { + const definition = ` + xychart-beta + ${chartType} [1, 4, 3] + `; + const option = EChartsFromMermaid.getOption(definition); + expect(option).toEqual({ + xAxis: { + type: 'value', + }, + yAxis: { + type: 'value', + }, + series: [ + { + type: chartType, + data: [ + [1, 1], + [2, 4], + [3, 3], + ], + }, + ], + }); + }); + it('should render horizontal chart', () => { + const definition = ` + xychart-beta horizontal + ${chartType} [1, 4, 3] + `; + const option = EChartsFromMermaid.getOption(definition); + expect(option).toEqual({ + xAxis: { type: 'value' }, + yAxis: { type: 'value' }, + series: [ + { + type: chartType, + data: [ + [1, 1], + [4, 2], + [3, 3], + ], + }, + ], + }); + }); + }); + + const axisTypes = ['x' /*, 'y'*/]; + for (const axisType of axisTypes) { + describe(axisType, () => { + const otherAxis = axisType === 'x' ? 'y' : 'x'; + it(`should render category ${axisType}Axis`, () => { + const definition = ` + xychart-beta + ${axisType}-axis [cat1, "cat2 with space", cat3] + ${chartType} [1, 4, 2] + `; + const option = EChartsFromMermaid.getOption(definition); + expect(option).toEqual({ + [axisType + 'Axis']: { + type: 'category', + data: ['cat1', 'cat2 with space', 'cat3'], + }, + [otherAxis + 'Axis']: { type: 'value' }, + series: [ + { + type: chartType, + data: [1, 4, 2], + }, + ], + }); + }); + + it(`should render value ${axisType}Axis with range`, () => { + const definition = ` + xychart-beta + ${axisType}-axis 100 --> 200 + ${chartType} [1, 4, 2] + `; + const option = EChartsFromMermaid.getOption(definition); + expect(option).toEqual({ + [axisType + 'Axis']: { type: 'value', min: 100, max: 200 }, + [otherAxis + 'Axis']: { type: 'value' }, + series: [ + { + type: chartType, + data: [ + [100, 1], + [150, 4], + [200, 2], + ], + }, + ], + }); + }); + + it(`should work with ${axisType}Axis title`, () => { + const definition = ` + xychart-beta + ${axisType}-axis "Cats" [cat1, "cat2 with space", cat3] + ${chartType} [1, 4, 2] + `; + const option = EChartsFromMermaid.getOption(definition); + expect(option).toEqual({ + [axisType + 'Axis']: { + type: 'category', + name: 'Cats', + data: ['cat1', 'cat2 with space', 'cat3'], + }, + [otherAxis + 'Axis']: { type: 'value' }, + series: [ + { + type: chartType, + data: [1, 4, 2], + }, + ], + }); + }); + }); + } + }); + } +}); diff --git a/src/mermaid/xy/XYChart.ts b/src/mermaid/xy/XYChart.ts index 7580435..367e711 100644 --- a/src/mermaid/xy/XYChart.ts +++ b/src/mermaid/xy/XYChart.ts @@ -1,9 +1,9 @@ -import { AxisDefinition, parseAxis } from '../../base/parseAxis'; -import { BaseChart } from '../../base/BaseChart'; import type { EChartsOption, LineSeriesOption, BarSeriesOption } from 'echarts'; +import { parseAxis } from '../../base/parseAxis'; +import { convert2dData } from '../../base/util'; +import { BaseChart } from '../../base/BaseChart'; const xySeriesTypes = ['line', 'bar']; -const axisTypes = ['x-axis', 'y-axis']; export class XYChart extends BaseChart { getOption(): EChartsOption { @@ -14,22 +14,18 @@ export class XYChart extends BaseChart { for (; lineId < this._lines.length; lineId++) { const line = this._lines[lineId]; - console.log('line', line); if (line.indexOf('horizontal') > 0) { isHorizontal = true; } const axisDef = parseAxis(line); - console.log('-----', isHorizontal, axisDef); if (axisDef) { const { key, ...rest } = axisDef; if ((key === 'x' && !isHorizontal) || (key === 'y' && isHorizontal)) { xAxis = rest; - console.log('========= xAxis', xAxis); } else { yAxis = rest; - console.log('========= yAxis', yAxis); } } else if (lineId > 0) { break; @@ -46,23 +42,36 @@ export class XYChart extends BaseChart { try { let data = JSON.parse(dataStr); - // If xAxis and yAxis are not defined, convert data into: - // [[1, data[0]], [2, data[1]], [3, data[2]]], ... - if (!xAxis && !yAxis) { - data = data.map((d: number, i: number) => - isHorizontal ? [d, i + 1] : [i + 1, d] - ); + const valueAxis = isHorizontal ? xAxis : yAxis; + if (valueAxis && valueAxis.type === 'category') { + throw new Error('Category axis is not supported for value axis.'); } - console.log('data', data); - // TODO: double value axes + // Case 1: xAxis: null, yAxis: any + // -> xAxis: value axis with min(1) and max(data.length), + // -> yAxis: value axis without/with min and max + // -> data: [[1, data[0]], ... [data.length, data[data.length - 1]]] + if (!xAxis) { + data = convert2dData(1, data.length, data, isHorizontal); + } + // Case 2: xAxis: value axis with min and max, yAxis: any + // -> xAxis: value axis with min and max, + // -> yAxis: value axis without/with min and max + // -> data: [[xAxis.min, data[0]], ... [xAxis.max, data[data.length - 1]]] + else if (xAxis && xAxis.max != null && xAxis.min != null) { + data = convert2dData(xAxis.min, xAxis.max, data, isHorizontal); + } + // Case 3: xAxis: category, yAxis: null + // -> xAxis: category axis + // -> yAxis: value axis + // -> data: data (not changed) series.push({ type: type as 'line' | 'bar', data, }); } catch (e) { - console.error(`Error parsing data for ${type}: ${dataStr}`); + console.error(`Error parsing data for ${type}: ${dataStr}`, e); } } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
