diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index 907ed4803d4..26094b3a106 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -360,7 +360,7 @@ export default function transformProps( series.push( transformFormulaAnnotation( layer, - data1, + rebasedDataA as TimeseriesDataRecord[], xAxisLabel, xAxisType, colorScale, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index 7acd63132b9..095e757d707 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -39,6 +39,7 @@ import { isTimeseriesAnnotationLayer, resolveAutoCurrency, TimeseriesChartDataResponseResult, + TimeseriesDataRecord, NumberFormats, } from '@superset-ui/core'; import { GenericDataType } from '@apache-superset/core/api/core'; @@ -463,7 +464,7 @@ export default function transformProps( series.push( transformFormulaAnnotation( layer, - data, + rebasedData as TimeseriesDataRecord[], xAxisLabel, xAxisType, colorScale, diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts index 3562a3a7668..76562999515 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts @@ -16,8 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps, VizType } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/ui'; +import { + AnnotationStyle, + AnnotationType, + DataRecord, + FormulaAnnotationLayer, + VizType, + ChartDataResponseResult, +} from '@superset-ui/core'; import { LegendOrientation, LegendType, @@ -28,6 +34,48 @@ import { EchartsMixedTimeseriesFormData, EchartsMixedTimeseriesProps, } from '../../src/MixedTimeseries/types'; +import { DEFAULT_FORM_DATA } from '../../src/MixedTimeseries/types'; +import { createEchartsTimeseriesTestChartProps } from '../helpers'; +import type { SeriesOption } from 'echarts'; + +/** + * Creates a partial ChartDataResponseResult for testing. + * Only includes the fields needed for tests, with sensible defaults for required fields. + */ +function createTestQueryData( + data: unknown[], + overrides?: Partial & { + label_map?: Record; + }, +): ChartDataResponseResult { + return { + annotation_data: null, + cache_key: null, + cache_timeout: null, + cached_dttm: null, + queried_dttm: null, + data: data as DataRecord[], + colnames: [], + coltypes: [], + error: null, + is_cached: false, + query: '', + rowcount: data.length, + sql_rowcount: data.length, + stacktrace: null, + status: 'success', + from_dttm: null, + to_dttm: null, + label_map: {}, + ...overrides, + } as ChartDataResponseResult & { label_map?: Record }; +} + +/** Defaults for createEchartsTimeseriesTestChartProps in Mixed Timeseries tests. */ +const MIXED_TIMESERIES_CHART_PROPS_DEFAULTS = { + defaultFormData: DEFAULT_FORM_DATA, + defaultVizType: 'mixed_timeseries' as const, +}; const formData: EchartsMixedTimeseriesFormData = { annotationLayers: [], @@ -85,49 +133,28 @@ const formData: EchartsMixedTimeseriesFormData = { legendSort: null, }; -const queriesData = [ - { - data: [ - { boy: 1, girl: 2, ds: 599616000000 }, - { boy: 3, girl: 4, ds: 599916000000 }, - ], - label_map: { - ds: ['ds'], - boy: ['boy'], - girl: ['girl'], - }, - }, - { - data: [ - { boy: 1, girl: 2, ds: 599616000000 }, - { boy: 3, girl: 4, ds: 599916000000 }, - ], - label_map: { - ds: ['ds'], - boy: ['boy'], - girl: ['girl'], - }, - }, +const defaultQueryRows = [ + { boy: 1, girl: 2, ds: 599616000000 }, + { boy: 3, girl: 4, ds: 599916000000 }, +]; +const defaultLabelMap = { ds: ['ds'], boy: ['boy'], girl: ['girl'] }; + +const queriesData: ChartDataResponseResult[] = [ + createTestQueryData(defaultQueryRows, { label_map: defaultLabelMap }), + createTestQueryData(defaultQueryRows, { label_map: defaultLabelMap }), ]; -const chartPropsConfig = { - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, -}; - test('should transform chart props for viz with showQueryIdentifiers=false', () => { - const chartPropsConfigWithoutIdentifiers = { - ...chartPropsConfig, - formData: { - ...formData, - showQueryIdentifiers: false, - }, - }; - const chartProps = new ChartProps(chartPropsConfigWithoutIdentifiers); - const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps); + const chartProps = createEchartsTimeseriesTestChartProps< + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps + >({ + ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, + defaultQueriesData: queriesData, + formData: { ...formData, showQueryIdentifiers: false }, + queriesData, + }); + const transformed = transformProps(chartProps); // Check that series IDs don't include query identifiers const seriesIds = (transformed.echartOptions.series as any[]).map( @@ -160,15 +187,16 @@ test('should transform chart props for viz with showQueryIdentifiers=false', () }); test('should transform chart props for viz with showQueryIdentifiers=true', () => { - const chartPropsConfigWithIdentifiers = { - ...chartPropsConfig, - formData: { - ...formData, - showQueryIdentifiers: true, - }, - }; - const chartProps = new ChartProps(chartPropsConfigWithIdentifiers); - const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps); + const chartProps = createEchartsTimeseriesTestChartProps< + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps + >({ + ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, + defaultQueriesData: queriesData, + formData: { ...formData, showQueryIdentifiers: true }, + queriesData, + }); + const transformed = transformProps(chartProps); // Check that series IDs include query identifiers const seriesIds = (transformed.echartOptions.series as any[]).map( @@ -202,22 +230,25 @@ test('should transform chart props for viz with showQueryIdentifiers=true', () = describe('legend sorting', () => { const getChartProps = (overrides = {}) => - new ChartProps({ - ...chartPropsConfig, + createEchartsTimeseriesTestChartProps< + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps + >({ + ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, + defaultQueriesData: queriesData, formData: { ...formData, ...overrides, showQueryIdentifiers: true, }, + queriesData, }); test('sort legend by data', () => { const chartProps = getChartProps({ legendSort: null, }); - const transformed = transformProps( - chartProps as EchartsMixedTimeseriesProps, - ); + const transformed = transformProps(chartProps); expect((transformed.echartOptions.legend as any).data).toEqual([ 'sum__num (Query A), girl', @@ -231,9 +262,7 @@ describe('legend sorting', () => { const chartProps = getChartProps({ legendSort: 'asc', }); - const transformed = transformProps( - chartProps as EchartsMixedTimeseriesProps, - ); + const transformed = transformProps(chartProps); expect((transformed.echartOptions.legend as any).data).toEqual([ 'sum__num (Query A), boy', @@ -247,9 +276,7 @@ describe('legend sorting', () => { const chartProps = getChartProps({ legendSort: 'desc', }); - const transformed = transformProps( - chartProps as EchartsMixedTimeseriesProps, - ); + const transformed = transformProps(chartProps); expect((transformed.echartOptions.legend as any).data).toEqual([ 'sum__num (Query B), girl', @@ -261,64 +288,148 @@ describe('legend sorting', () => { }); test('legend margin: top orientation sets grid.top correctly', () => { - const chartPropsConfigWithoutIdentifiers = { - ...chartPropsConfig, + const chartProps = createEchartsTimeseriesTestChartProps< + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps + >({ + ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, + defaultQueriesData: queriesData, formData: { ...formData, legendMargin: 250, showLegend: true, }, - }; - const chartProps = new ChartProps(chartPropsConfigWithoutIdentifiers); - const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps); + queriesData, + }); + const transformed = transformProps(chartProps); expect((transformed.echartOptions.grid as any).top).toEqual(270); }); test('legend margin: bottom orientation sets grid.bottom correctly', () => { - const chartPropsConfigWithoutIdentifiers = { - ...chartPropsConfig, + const chartProps = createEchartsTimeseriesTestChartProps< + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps + >({ + ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, + defaultQueriesData: queriesData, formData: { ...formData, legendMargin: 250, showLegend: true, legendOrientation: LegendOrientation.Bottom, }, - }; - const chartProps = new ChartProps(chartPropsConfigWithoutIdentifiers); - const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps); + queriesData, + }); + const transformed = transformProps(chartProps); expect((transformed.echartOptions.grid as any).bottom).toEqual(270); }); test('legend margin: left orientation sets grid.left correctly', () => { - const chartPropsConfigWithoutIdentifiers = { - ...chartPropsConfig, + const chartProps = createEchartsTimeseriesTestChartProps< + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps + >({ + ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, + defaultQueriesData: queriesData, formData: { ...formData, legendMargin: 250, showLegend: true, legendOrientation: LegendOrientation.Left, }, - }; - const chartProps = new ChartProps(chartPropsConfigWithoutIdentifiers); - const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps); + queriesData, + }); + const transformed = transformProps(chartProps); expect((transformed.echartOptions.grid as any).left).toEqual(270); }); test('legend margin: right orientation sets grid.right correctly', () => { - const chartPropsConfigWithoutIdentifiers = { - ...chartPropsConfig, + const chartProps = createEchartsTimeseriesTestChartProps< + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps + >({ + ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, + defaultQueriesData: queriesData, formData: { ...formData, legendMargin: 270, showLegend: true, legendOrientation: LegendOrientation.Right, }, - }; - const chartProps = new ChartProps(chartPropsConfigWithoutIdentifiers); - const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps); + queriesData, + }); + const transformed = transformProps(chartProps); expect((transformed.echartOptions.grid as any).right).toEqual(270); }); + +test('should add a formula annotation when X-axis column has dataset-level label', () => { + const formula: FormulaAnnotationLayer = { + name: 'My Formula', + annotationType: AnnotationType.Formula, + value: 'x*2', + style: AnnotationStyle.Solid, + show: true, + showLabel: true, + }; + const timeColumnName = 'ds'; + const timeColumnLabel = 'Time Label'; + const testData = [ + { + [timeColumnLabel]: 599616000000, + boy: 1, + girl: 2, + }, + { + [timeColumnLabel]: 599916000000, + boy: 3, + girl: 4, + }, + ]; + const chartProps = createEchartsTimeseriesTestChartProps< + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps + >({ + ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, + defaultQueriesData: [], + formData: { + ...formData, + x_axis: timeColumnName, + annotationLayers: [formula], + }, + queriesData: [ + createTestQueryData(testData, { + label_map: { + [timeColumnName]: [timeColumnLabel], + boy: ['boy'], + girl: ['girl'], + }, + }), + createTestQueryData(testData, { + label_map: { + [timeColumnName]: [timeColumnLabel], + boy: ['boy'], + girl: ['girl'], + }, + }), + ], + datasource: { + verboseMap: { + [timeColumnName]: timeColumnLabel, + }, + columnFormats: {}, + currencyFormats: {}, + }, + }); + const result = transformProps(chartProps); + const formulaSeries = ( + result.echartOptions.series as SeriesOption[] | undefined + )?.find((s: SeriesOption) => s.name === 'My Formula'); + expect(formulaSeries).toBeDefined(); + expect(formulaSeries?.data).toBeDefined(); + expect(Array.isArray(formulaSeries?.data)).toBe(true); + expect((formulaSeries?.data as unknown[]).length).toBeGreaterThan(0); +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts index 562f3fd5190..89303f7f1bb 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts @@ -20,23 +20,61 @@ import { AnnotationSourceType, AnnotationStyle, AnnotationType, - ChartProps, ComparisonType, + DataRecord, EventAnnotationLayer, FormulaAnnotationLayer, IntervalAnnotationLayer, SqlaFormData, TimeseriesAnnotationLayer, + ChartDataResponseResult, } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/ui'; import { EchartsTimeseriesChartProps } from '../../src/types'; +import type { SeriesOption } from 'echarts'; import transformProps from '../../src/Timeseries/transformProps'; import { EchartsTimeseriesSeriesType, OrientationType, + EchartsTimeseriesFormData, } from '../../src/Timeseries/types'; +import { StackControlsValue } from '../../src/constants'; +import { DEFAULT_FORM_DATA } from '../../src/Timeseries/constants'; +import { createEchartsTimeseriesTestChartProps } from '../helpers'; import { BASE_TIMESTAMP, createTestData } from './helpers'; +/** + * Creates a partial ChartDataResponseResult for testing. + * Only includes the fields needed for tests, with sensible defaults for required fields. + */ +function createTestQueryData( + data: unknown[], + overrides?: Partial & { + label_map?: Record; + }, +): ChartDataResponseResult { + return { + annotation_data: null, + cache_key: null, + cache_timeout: null, + cached_dttm: null, + queried_dttm: null, + data: data as DataRecord[], + colnames: [], + coltypes: [], + error: null, + is_cached: false, + query: '', + rowcount: data.length, + sql_rowcount: data.length, + stacktrace: null, + status: 'success', + from_dttm: null, + to_dttm: null, + label_map: {}, + ...overrides, + } as ChartDataResponseResult & { label_map?: Record }; +} + type YAxisFormatter = (value: number, index: number) => string; function getYAxisFormatter( @@ -51,6 +89,37 @@ function getYAxisFormatter( return yAxis.axisLabel!.formatter!; } +/** + * Creates a properly typed EchartsTimeseriesChartProps for testing. + * Uses shared createEchartsTimeseriesTestChartProps with Timeseries defaults. + */ +function createTestChartProps(config: { + formData?: Partial; + queriesData?: ChartDataResponseResult[]; + annotationData?: Record; + datasource?: { + verboseMap?: Record; + columnFormats?: Record; + currencyFormats?: Record< + string, + { symbol: string; symbolPosition: string } + >; + currencyCodeColumn?: string; + }; + width?: number; + height?: number; +}): EchartsTimeseriesChartProps { + return createEchartsTimeseriesTestChartProps< + EchartsTimeseriesFormData, + EchartsTimeseriesChartProps + >({ + defaultFormData: DEFAULT_FORM_DATA, + defaultVizType: 'my_viz', + defaultQueriesData: queriesData, + ...config, + }); +} + const formData: SqlaFormData = { colorScheme: 'bnbColors', datasource: '3__table', @@ -59,29 +128,21 @@ const formData: SqlaFormData = { groupby: ['foo', 'bar'], viz_type: 'my_viz', }; -const queriesData = [ - { - data: createTestData( +const queriesData: ChartDataResponseResult[] = [ + createTestQueryData( + createTestData( [ { 'San Francisco': 1, 'New York': 2 }, { 'San Francisco': 3, 'New York': 4 }, ], { intervalMs: 300000000 }, ), - }, + ), ]; -const chartPropsConfig = { - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, -}; - describe('EchartsTimeseries transformProps', () => { test('should transform chart props for viz', () => { - const chartProps = new ChartProps(chartPropsConfig); - expect(transformProps(chartProps as EchartsTimeseriesChartProps)).toEqual( + const chartProps = createTestChartProps({}); + expect(transformProps(chartProps)).toEqual( expect.objectContaining({ width: 800, height: 600, @@ -111,14 +172,13 @@ describe('EchartsTimeseries transformProps', () => { }); test('should transform chart props for horizontal viz', () => { - const chartProps = new ChartProps({ - ...chartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...formData, - orientation: 'horizontal', + orientation: OrientationType.Horizontal, }, }); - expect(transformProps(chartProps as EchartsTimeseriesChartProps)).toEqual( + expect(transformProps(chartProps)).toEqual( expect.objectContaining({ width: 800, height: 600, @@ -156,14 +216,13 @@ describe('EchartsTimeseries transformProps', () => { show: true, showLabel: true, }; - const chartProps = new ChartProps({ - ...chartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...formData, annotationLayers: [formula], }, }); - expect(transformProps(chartProps as EchartsTimeseriesChartProps)).toEqual( + expect(transformProps(chartProps)).toEqual( expect.objectContaining({ width: 800, height: 600, @@ -199,6 +258,137 @@ describe('EchartsTimeseries transformProps', () => { ); }); + test('should add a formula annotation when X-axis column has dataset-level label', () => { + const formula: FormulaAnnotationLayer = { + name: 'My Formula', + annotationType: AnnotationType.Formula, + value: 'x*2', + style: AnnotationStyle.Solid, + show: true, + showLabel: true, + }; + const timeColumnName = 'ds'; + const timeColumnLabel = 'Time Label'; + const testData = [ + { + [timeColumnLabel]: new Date(BASE_TIMESTAMP).toISOString(), + 'San Francisco': 1, + 'New York': 2, + }, + { + [timeColumnLabel]: new Date(BASE_TIMESTAMP + 300000000).toISOString(), + 'San Francisco': 3, + 'New York': 4, + }, + ]; + const chartProps = createTestChartProps({ + formData: { + ...formData, + x_axis: timeColumnName, + granularity_sqla: timeColumnName, + annotationLayers: [formula], + }, + queriesData: [createTestQueryData(testData)], + datasource: { + verboseMap: { + [timeColumnName]: timeColumnLabel, + }, + columnFormats: {}, + currencyFormats: {}, + }, + }); + const result = transformProps(chartProps); + const formulaSeries = ( + result.echartOptions.series as SeriesOption[] | undefined + )?.find((s: SeriesOption) => s.name === 'My Formula'); + expect(formulaSeries).toBeDefined(); + expect(formulaSeries?.data).toBeDefined(); + expect(Array.isArray(formulaSeries?.data)).toBe(true); + expect((formulaSeries?.data as unknown[]).length).toBeGreaterThan(0); + const firstDataPoint = (formulaSeries?.data as [number, number][])[0]; + expect(firstDataPoint).toBeDefined(); + expect(firstDataPoint[1]).toBe(firstDataPoint[0] * 2); + }); + + test('should add a formula annotation when X-axis column has dataset-level label and verboseMap is empty (backward compatibility)', () => { + const formula: FormulaAnnotationLayer = { + name: 'My Formula', + annotationType: AnnotationType.Formula, + value: 'x+1', + style: AnnotationStyle.Solid, + show: true, + showLabel: true, + }; + const chartProps = createTestChartProps({ + formData: { + ...formData, + annotationLayers: [formula], + }, + datasource: { + verboseMap: {}, + columnFormats: {}, + currencyFormats: {}, + }, + }); + const result = transformProps(chartProps); + const formulaSeries = ( + result.echartOptions.series as SeriesOption[] | undefined + )?.find((s: SeriesOption) => s.name === 'My Formula'); + expect(formulaSeries).toBeDefined(); + expect(formulaSeries?.data).toBeDefined(); + expect(Array.isArray(formulaSeries?.data)).toBe(true); + }); + + test('should add a formula annotation when X-axis column has dataset-level label in horizontal orientation', () => { + const formula: FormulaAnnotationLayer = { + name: 'My Formula', + annotationType: AnnotationType.Formula, + value: 'x*2', + style: AnnotationStyle.Solid, + show: true, + showLabel: true, + }; + const timeColumnName = 'ds'; + const timeColumnLabel = 'Time Label'; + const testData = [ + { + [timeColumnLabel]: new Date(BASE_TIMESTAMP).toISOString(), + 'San Francisco': 1, + 'New York': 2, + }, + { + [timeColumnLabel]: new Date(BASE_TIMESTAMP + 300000000).toISOString(), + 'San Francisco': 3, + 'New York': 4, + }, + ]; + const chartProps = createTestChartProps({ + formData: { + ...formData, + x_axis: timeColumnName, + granularity_sqla: timeColumnName, + orientation: OrientationType.Horizontal, + annotationLayers: [formula], + }, + queriesData: [createTestQueryData(testData)], + datasource: { + verboseMap: { + [timeColumnName]: timeColumnLabel, + }, + columnFormats: {}, + currencyFormats: {}, + }, + }); + const result = transformProps(chartProps); + const formulaSeries = ( + result.echartOptions.series as SeriesOption[] | undefined + )?.find((s: SeriesOption) => s.name === 'My Formula'); + expect(formulaSeries).toBeDefined(); + const firstDataPoint = (formulaSeries?.data as [number, number][])[0]; + expect(firstDataPoint).toBeDefined(); + expect(firstDataPoint[0]).toBe(firstDataPoint[1] * 2); + }); + test('should add an interval, event and timeseries annotation to viz', () => { const event: EventAnnotationLayer = { annotationType: AnnotationType.Event, @@ -270,8 +460,7 @@ describe('EchartsTimeseries transformProps', () => { ], }, }; - const chartProps = new ChartProps({ - ...chartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...formData, annotationLayers: [event, interval, timeseries], @@ -279,12 +468,12 @@ describe('EchartsTimeseries transformProps', () => { annotationData, queriesData: [ { - ...queriesData[0], + ...(queriesData[0] as ChartDataResponseResult), annotation_data: annotationData, }, ], }); - expect(transformProps(chartProps as EchartsTimeseriesChartProps)).toEqual( + expect(transformProps(chartProps)).toEqual( expect.objectContaining({ echartOptions: expect.objectContaining({ legend: expect.objectContaining({ @@ -310,9 +499,9 @@ describe('EchartsTimeseries transformProps', () => { }); test('Should add a baseline series for stream graph', () => { - const streamQueriesData = [ - { - data: createTestData( + const streamQueriesDataTyped: ChartDataResponseResult[] = [ + createTestQueryData( + createTestData( [ { 'San Francisco': 120, @@ -366,21 +555,18 @@ describe('EchartsTimeseries transformProps', () => { ], { intervalMs: 1 }, ), - }, + ), ]; - const streamFormData = { ...formData, stack: 'Stream' }; - const props = { - ...chartPropsConfig, - formData: streamFormData, - queriesData: streamQueriesData, + const streamFormData: Partial = { + ...formData, + stack: StackControlsValue.Stream, }; - - const chartProps = new ChartProps(props); + const chartProps = createTestChartProps({ + formData: streamFormData, + queriesData: streamQueriesDataTyped, + }); expect( - ( - transformProps(chartProps as EchartsTimeseriesChartProps).echartOptions - .series as any[] - )[0], + (transformProps(chartProps).echartOptions.series as any[])[0], ).toEqual({ areaStyle: { opacity: 0, @@ -437,9 +623,9 @@ describe('Does transformProps transform series correctly', () => { onlyTotal: false, percentageThreshold: 50, }; - const queriesData = [ - { - data: createTestData( + const queriesData: ChartDataResponseResult[] = [ + createTestQueryData( + createTestData( [ { 'San Francisco': 1, @@ -464,21 +650,15 @@ describe('Does transformProps transform series correctly', () => { ], { intervalMs: 300000000 }, ), - }, + ), ]; - const chartPropsConfig = { - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }; const totalStackedValues = queriesData[0].data.reduce( (totals, currentStack) => { const total = Object.keys(currentStack).reduce((stackSum, key) => { if (key === '__timestamp') return stackSum; - return stackSum + currentStack[key as keyof typeof currentStack]; + const val = currentStack[key as keyof typeof currentStack]; + return stackSum + (typeof val === 'number' ? val : 0); }, 0); totals.push(total); return totals; @@ -487,11 +667,10 @@ describe('Does transformProps transform series correctly', () => { ); test('should show labels when showValue is true', () => { - const chartProps = new ChartProps(chartPropsConfig); + const chartProps = createTestChartProps({ formData, queriesData }); - const transformedSeries = transformProps( - chartProps as EchartsTimeseriesChartProps, - ).echartOptions.series as seriesType[]; + const transformedSeries = transformProps(chartProps).echartOptions + .series as seriesType[]; transformedSeries.forEach(series => { expect(series.label.show).toBe(true); @@ -499,16 +678,13 @@ describe('Does transformProps transform series correctly', () => { }); test('should not show labels when showValue is false', () => { - const updatedChartPropsConfig = { - ...chartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...formData, showValue: false }, - }; + queriesData, + }); - const chartProps = new ChartProps(updatedChartPropsConfig); - - const transformedSeries = transformProps( - chartProps as EchartsTimeseriesChartProps, - ).echartOptions.series as seriesType[]; + const transformedSeries = transformProps(chartProps).echartOptions + .series as seriesType[]; transformedSeries.forEach(series => { expect(series.label.show).toBe(false); @@ -516,16 +692,13 @@ describe('Does transformProps transform series correctly', () => { }); test('should show only totals when onlyTotal is true', () => { - const updatedChartPropsConfig = { - ...chartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...formData, onlyTotal: true }, - }; + queriesData, + }); - const chartProps = new ChartProps(updatedChartPropsConfig); - - const transformedSeries = transformProps( - chartProps as EchartsTimeseriesChartProps, - ).echartOptions.series as seriesType[]; + const transformedSeries = transformProps(chartProps).echartOptions + .series as seriesType[]; const showValueIndexes: number[] = []; @@ -561,11 +734,10 @@ describe('Does transformProps transform series correctly', () => { }); test('should show labels on values >= percentageThreshold if onlyTotal is false', () => { - const chartProps = new ChartProps(chartPropsConfig); + const chartProps = createTestChartProps({ formData, queriesData }); - const transformedSeries = transformProps( - chartProps as EchartsTimeseriesChartProps, - ).echartOptions.series as seriesType[]; + const transformedSeries = transformProps(chartProps).echartOptions + .series as seriesType[]; const expectedThresholds = totalStackedValues.map( total => ((formData.percentageThreshold || 0) / 100) * total, @@ -587,16 +759,13 @@ describe('Does transformProps transform series correctly', () => { }); test('should not apply percentage threshold when showValue is true and stack is false', () => { - const updatedChartPropsConfig = { - ...chartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...formData, stack: false }, - }; + queriesData, + }); - const chartProps = new ChartProps(updatedChartPropsConfig); - - const transformedSeries = transformProps( - chartProps as EchartsTimeseriesChartProps, - ).echartOptions.series as seriesType[]; + const transformedSeries = transformProps(chartProps).echartOptions + .series as seriesType[]; transformedSeries.forEach((series, seriesIndex) => { expect(series.label.show).toBe(true); @@ -613,28 +782,23 @@ describe('Does transformProps transform series correctly', () => { }); test('should remove time shift labels from label_map', () => { - const updatedChartPropsConfig = { - ...chartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...formData, timeCompare: ['1 year ago'], }, queriesData: [ - { - ...queriesData[0], + createTestQueryData(queriesData[0].data as DataRecord[], { label_map: { '1 year ago, foo1, bar1': ['1 year ago', 'foo1', 'bar1'], '1 year ago, foo2, bar2': ['1 year ago', 'foo2', 'bar2'], 'foo1, bar1': ['foo1', 'bar1'], 'foo2, bar2': ['foo2', 'bar2'], }, - }, + }), ], - }; - const chartProps = new ChartProps(updatedChartPropsConfig); - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); + }); + const transformedProps = transformProps(chartProps); expect(transformedProps.labelMap).toEqual({ '1 year ago, foo1, bar1': ['foo1', 'bar1'], '1 year ago, foo2, bar2': ['foo2', 'bar2'], @@ -645,9 +809,9 @@ describe('Does transformProps transform series correctly', () => { }); describe('legend sorting', () => { - const legendSortData = [ - { - data: createTestData( + const legendSortData: ChartDataResponseResult[] = [ + createTestQueryData( + createTestData( [ { Milton: 40, @@ -676,13 +840,12 @@ describe('legend sorting', () => { ], { intervalMs: 300000000 }, ), - }, + ), ]; - const getChartProps = (formData: Partial) => - new ChartProps({ - ...chartPropsConfig, - formData: { ...formData }, + const getChartProps = (formDataOverrides: Partial) => + createTestChartProps({ + formData: { ...formData, ...formDataOverrides }, queriesData: legendSortData, }); @@ -692,9 +855,7 @@ describe('legend sorting', () => { sortSeriesType: 'min', sortSeriesAscending: true, }); - const transformed = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); + const transformed = transformProps(chartProps); expect((transformed.echartOptions.legend as any).data).toEqual([ 'San Francisco', @@ -710,9 +871,7 @@ describe('legend sorting', () => { sortSeriesType: 'min', sortSeriesAscending: true, }); - const transformed = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); + const transformed = transformProps(chartProps); expect((transformed.echartOptions.legend as any).data).toEqual([ 'Boston', @@ -728,9 +887,7 @@ describe('legend sorting', () => { sortSeriesType: 'min', sortSeriesAscending: true, }); - const transformed = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); + const transformed = transformProps(chartProps); expect((transformed.echartOptions.legend as any).data).toEqual([ 'San Francisco', @@ -749,25 +906,15 @@ const timeCompareFormData: SqlaFormData = { viz_type: 'my_viz', }; -const timeCompareChartPropsConfig = { - formData: timeCompareFormData, - width: 800, - height: 600, - theme: supersetTheme, -}; - test('should apply dashed line style to time comparison series with single metric', () => { const queriesDataWithTimeCompare = [ - { - data: [ - { sum__num: 100, '1 week ago': 80, __timestamp: 599616000000 }, - { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 }, - ], - }, + createTestQueryData([ + { sum__num: 100, '1 week ago': 80, __timestamp: 599616000000 }, + { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 }, + ]), ]; - const chartProps = new ChartProps({ - ...timeCompareChartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...timeCompareFormData, time_compare: ['1 week ago'], @@ -776,44 +923,51 @@ test('should apply dashed line style to time comparison series with single metri queriesData: queriesDataWithTimeCompare, }); - const transformed = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - const series = transformed.echartOptions.series as any[]; + const transformed = transformProps(chartProps); + const series = (transformed.echartOptions.series as SeriesOption[]) || []; - const mainSeries = series.find(s => s.name === 'sum__num'); - const comparisonSeries = series.find(s => s.name === '1 week ago'); + const mainSeries = series.find(s => s.name === 'sum__num') as + | (SeriesOption & { lineStyle?: { type?: number[] | string } }) + | undefined; + const comparisonSeries = series.find(s => s.name === '1 week ago') as + | (SeriesOption & { lineStyle?: { type?: number[] | string } }) + | undefined; expect(mainSeries).toBeDefined(); expect(comparisonSeries).toBeDefined(); // Main series should not have a dash pattern array - expect(Array.isArray(mainSeries.lineStyle?.type)).toBe(false); + expect(Array.isArray(mainSeries?.lineStyle?.type)).toBe(false); // Comparison series should have a visible dash pattern array [dash, gap] - expect(Array.isArray(comparisonSeries.lineStyle?.type)).toBe(true); - expect(comparisonSeries.lineStyle?.type[0]).toBeGreaterThanOrEqual(4); - expect(comparisonSeries.lineStyle?.type[1]).toBeGreaterThanOrEqual(3); + expect(Array.isArray(comparisonSeries?.lineStyle?.type)).toBe(true); + expect( + Array.isArray(comparisonSeries?.lineStyle?.type) + ? comparisonSeries.lineStyle.type[0] + : undefined, + ).toBeGreaterThanOrEqual(4); + expect( + Array.isArray(comparisonSeries?.lineStyle?.type) + ? comparisonSeries.lineStyle.type[1] + : undefined, + ).toBeGreaterThanOrEqual(3); }); test('should apply dashed line style to time comparison series with metric__offset pattern', () => { const queriesDataWithTimeCompare = [ - { - data: [ - { - sum__num: 100, - 'sum__num__1 week ago': 80, - __timestamp: 599616000000, - }, - { - sum__num: 150, - 'sum__num__1 week ago': 120, - __timestamp: 599916000000, - }, - ], - }, + createTestQueryData([ + { + sum__num: 100, + 'sum__num__1 week ago': 80, + __timestamp: 599616000000, + }, + { + sum__num: 150, + 'sum__num__1 week ago': 120, + __timestamp: 599916000000, + }, + ]), ]; - const chartProps = new ChartProps({ - ...timeCompareChartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...timeCompareFormData, time_compare: ['1 week ago'], @@ -822,37 +976,46 @@ test('should apply dashed line style to time comparison series with metric__offs queriesData: queriesDataWithTimeCompare, }); - const transformed = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - const series = transformed.echartOptions.series as any[]; + const transformed = transformProps(chartProps); + const series = (transformed.echartOptions.series as SeriesOption[]) || []; - const mainSeries = series.find(s => s.name === 'sum__num'); - const comparisonSeries = series.find(s => s.name === 'sum__num__1 week ago'); + const mainSeries = series.find(s => s.name === 'sum__num') as + | (SeriesOption & { lineStyle?: { type?: number[] | string } }) + | undefined; + const comparisonSeries = series.find( + s => s.name === 'sum__num__1 week ago', + ) as + | (SeriesOption & { lineStyle?: { type?: number[] | string } }) + | undefined; expect(mainSeries).toBeDefined(); expect(comparisonSeries).toBeDefined(); // Main series should not have a dash pattern array - expect(Array.isArray(mainSeries.lineStyle?.type)).toBe(false); + expect(Array.isArray(mainSeries?.lineStyle?.type)).toBe(false); // Comparison series should have a visible dash pattern array [dash, gap] - expect(Array.isArray(comparisonSeries.lineStyle?.type)).toBe(true); - expect(comparisonSeries.lineStyle?.type[0]).toBeGreaterThanOrEqual(4); - expect(comparisonSeries.lineStyle?.type[1]).toBeGreaterThanOrEqual(3); + expect(Array.isArray(comparisonSeries?.lineStyle?.type)).toBe(true); + expect( + Array.isArray(comparisonSeries?.lineStyle?.type) + ? comparisonSeries.lineStyle.type[0] + : undefined, + ).toBeGreaterThanOrEqual(4); + expect( + Array.isArray(comparisonSeries?.lineStyle?.type) + ? comparisonSeries.lineStyle.type[1] + : undefined, + ).toBeGreaterThanOrEqual(3); }); test('should apply connectNulls to time comparison series', () => { const queriesDataWithNulls = [ - { - data: [ - { sum__num: 100, '1 week ago': null, __timestamp: 599616000000 }, - { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 }, - { sum__num: 200, '1 week ago': null, __timestamp: 600216000000 }, - ], - }, + createTestQueryData([ + { sum__num: 100, '1 week ago': null, __timestamp: 599616000000 }, + { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 }, + { sum__num: 200, '1 week ago': null, __timestamp: 600216000000 }, + ]), ]; - const chartProps = new ChartProps({ - ...timeCompareChartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...timeCompareFormData, time_compare: ['1 week ago'], @@ -861,29 +1024,26 @@ test('should apply connectNulls to time comparison series', () => { queriesData: queriesDataWithNulls, }); - const transformed = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - const series = transformed.echartOptions.series as any[]; + const transformed = transformProps(chartProps); + const series = (transformed.echartOptions.series as SeriesOption[]) || []; - const comparisonSeries = series.find(s => s.name === '1 week ago'); + const comparisonSeries = series.find(s => s.name === '1 week ago') as + | (SeriesOption & { connectNulls?: boolean }) + | undefined; expect(comparisonSeries).toBeDefined(); - expect(comparisonSeries.connectNulls).toBe(true); + expect(comparisonSeries?.connectNulls).toBe(true); }); test('should not apply dashed line style for non-Values comparison types', () => { const queriesDataWithTimeCompare = [ - { - data: [ - { sum__num: 100, '1 week ago': 80, __timestamp: 599616000000 }, - { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 }, - ], - }, + createTestQueryData([ + { sum__num: 100, '1 week ago': 80, __timestamp: 599616000000 }, + { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 }, + ]), ]; - const chartProps = new ChartProps({ - ...timeCompareChartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...timeCompareFormData, time_compare: ['1 week ago'], @@ -892,22 +1052,24 @@ test('should not apply dashed line style for non-Values comparison types', () => queriesData: queriesDataWithTimeCompare, }); - const transformed = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - const series = transformed.echartOptions.series as any[]; + const transformed = transformProps(chartProps); + const series = (transformed.echartOptions.series as SeriesOption[]) || []; - const comparisonSeries = series.find(s => s.name === '1 week ago'); + const comparisonSeries = series.find(s => s.name === '1 week ago') as + | (SeriesOption & { + lineStyle?: { type?: number[] | string }; + connectNulls?: boolean; + }) + | undefined; expect(comparisonSeries).toBeDefined(); // Non-Values comparison types don't get dashed styling (isDerivedSeries returns false) - expect(Array.isArray(comparisonSeries.lineStyle?.type)).toBe(false); - expect(comparisonSeries.connectNulls).toBeFalsy(); + expect(Array.isArray(comparisonSeries?.lineStyle?.type)).toBe(false); + expect(comparisonSeries?.connectNulls).toBeFalsy(); }); test('EchartsTimeseries AUTO mode should detect single currency and format with $ for USD', () => { - const chartProps = new ChartProps({ - ...chartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...formData, metrics: ['sum__num'], @@ -920,8 +1082,8 @@ test('EchartsTimeseries AUTO mode should detect single currency and format with verboseMap: {}, }, queriesData: [ - { - data: [ + createTestQueryData( + [ { 'San Francisco': 1000, __timestamp: 599616000000, @@ -933,19 +1095,19 @@ test('EchartsTimeseries AUTO mode should detect single currency and format with currency_code: 'USD', }, ], - }, + { detected_currency: 'USD' }, + ), ], }); - const transformed = transformProps(chartProps as EchartsTimeseriesChartProps); + const transformed = transformProps(chartProps); const formatter = getYAxisFormatter(transformed); expect(formatter(1000, 0)).toContain('$'); }); test('EchartsTimeseries AUTO mode should use neutral formatting for mixed currencies', () => { - const chartProps = new ChartProps({ - ...chartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...formData, metrics: ['sum__num'], @@ -958,24 +1120,22 @@ test('EchartsTimeseries AUTO mode should use neutral formatting for mixed curren verboseMap: {}, }, queriesData: [ - { - data: [ - { - 'San Francisco': 1000, - __timestamp: 599616000000, - currency_code: 'USD', - }, - { - 'San Francisco': 2000, - __timestamp: 599916000000, - currency_code: 'EUR', - }, - ], - }, + createTestQueryData([ + { + 'San Francisco': 1000, + __timestamp: 599616000000, + currency_code: 'USD', + }, + { + 'San Francisco': 2000, + __timestamp: 599916000000, + currency_code: 'EUR', + }, + ]), ], }); - const transformed = transformProps(chartProps as EchartsTimeseriesChartProps); + const transformed = transformProps(chartProps); // With mixed currencies, Y-axis should use neutral formatting const formatter = getYAxisFormatter(transformed); @@ -985,8 +1145,7 @@ test('EchartsTimeseries AUTO mode should use neutral formatting for mixed curren }); test('EchartsTimeseries should preserve static currency format with £ for GBP', () => { - const chartProps = new ChartProps({ - ...chartPropsConfig, + const chartProps = createTestChartProps({ formData: { ...formData, metrics: ['sum__num'], @@ -999,24 +1158,22 @@ test('EchartsTimeseries should preserve static currency format with £ for GBP', verboseMap: {}, }, queriesData: [ - { - data: [ - { - 'San Francisco': 1000, - __timestamp: 599616000000, - currency_code: 'USD', - }, - { - 'San Francisco': 2000, - __timestamp: 599916000000, - currency_code: 'EUR', - }, - ], - }, + createTestQueryData([ + { + 'San Francisco': 1000, + __timestamp: 599616000000, + currency_code: 'USD', + }, + { + 'San Francisco': 2000, + __timestamp: 599916000000, + currency_code: 'EUR', + }, + ]), ], }); - const transformed = transformProps(chartProps as EchartsTimeseriesChartProps); + const transformed = transformProps(chartProps); // Static mode should always show £ const formatter = getYAxisFormatter(transformed); @@ -1037,26 +1194,21 @@ const baseFormDataHorizontalBar: SqlaFormData = { }; test('should set yAxis max to actual data max for horizontal bar charts', () => { - const queriesData = [ - { - data: createTestData( + const queriesData: ChartDataResponseResult[] = [ + createTestQueryData( + createTestData( [{ 'Series A': 15000 }, { 'Series A': 20000 }, { 'Series A': 18000 }], { intervalMs: 300000000 }, ), - }, + ), ]; - const chartProps = new ChartProps({ + const chartProps = createTestChartProps({ formData: baseFormDataHorizontalBar, - width: 800, - height: 600, queriesData, - theme: supersetTheme, }); - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); + const transformedProps = transformProps(chartProps); // In horizontal orientation, axes are swapped, so yAxis becomes xAxis const xAxisRaw = transformedProps.echartOptions.xAxis as any; @@ -1064,26 +1216,21 @@ test('should set yAxis max to actual data max for horizontal bar charts', () => }); test('should set yAxis min and max for diverging horizontal bar charts', () => { - const queriesData = [ - { - data: createTestData( + const queriesData: ChartDataResponseResult[] = [ + createTestQueryData( + createTestData( [{ 'Series A': -21000 }, { 'Series A': 20000 }, { 'Series A': 18000 }], { intervalMs: 300000000 }, ), - }, + ), ]; - const chartProps = new ChartProps({ + const chartProps = createTestChartProps({ formData: baseFormDataHorizontalBar, - width: 800, - height: 600, queriesData, - theme: supersetTheme, }); - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); + const transformedProps = transformProps(chartProps); // In horizontal orientation, axes are swapped, so yAxis becomes xAxis const xAxisRaw = transformedProps.echartOptions.xAxis as any; @@ -1092,29 +1239,24 @@ test('should set yAxis min and max for diverging horizontal bar charts', () => { }); test('should not override explicit yAxisBounds for horizontal bar charts', () => { - const queriesData = [ - { - data: createTestData( + const queriesData: ChartDataResponseResult[] = [ + createTestQueryData( + createTestData( [{ 'Series A': 15000 }, { 'Series A': 20000 }, { 'Series A': 18000 }], { intervalMs: 300000000 }, ), - }, + ), ]; - const chartProps = new ChartProps({ + const chartProps = createTestChartProps({ formData: { ...baseFormDataHorizontalBar, yAxisBounds: [0, 25000], // Explicit bounds }, - width: 800, - height: 600, queriesData, - theme: supersetTheme, }); - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); + const transformedProps = transformProps(chartProps); // In horizontal orientation, axes are swapped, so yAxis becomes xAxis const xAxisRaw = transformedProps.echartOptions.xAxis as any; @@ -1123,29 +1265,24 @@ test('should not override explicit yAxisBounds for horizontal bar charts', () => }); test('should not apply axis bounds calculation when truncateYAxis is false for horizontal bar charts', () => { - const queriesData = [ - { - data: createTestData( + const queriesData: ChartDataResponseResult[] = [ + createTestQueryData( + createTestData( [{ 'Series A': 15000 }, { 'Series A': 20000 }, { 'Series A': 18000 }], { intervalMs: 300000000 }, ), - }, + ), ]; - const chartProps = new ChartProps({ + const chartProps = createTestChartProps({ formData: { ...baseFormDataHorizontalBar, truncateYAxis: false, }, - width: 800, - height: 600, queriesData, - theme: supersetTheme, }); - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); + const transformedProps = transformProps(chartProps); // In horizontal orientation, axes are swapped, so yAxis becomes xAxis const xAxis = transformedProps.echartOptions.xAxis as any; @@ -1154,29 +1291,24 @@ test('should not apply axis bounds calculation when truncateYAxis is false for h }); test('should not apply axis bounds calculation when seriesType is not Bar for horizontal charts', () => { - const queriesData = [ - { - data: createTestData( + const queriesData: ChartDataResponseResult[] = [ + createTestQueryData( + createTestData( [{ 'Series A': 15000 }, { 'Series A': 20000 }, { 'Series A': 18000 }], { intervalMs: 300000000 }, ), - }, + ), ]; - const chartProps = new ChartProps({ + const chartProps = createTestChartProps({ formData: { ...baseFormDataHorizontalBar, seriesType: EchartsTimeseriesSeriesType.Line, }, - width: 800, - height: 600, queriesData, - theme: supersetTheme, }); - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); + const transformedProps = transformProps(chartProps); // In horizontal orientation, axes are swapped, so yAxis becomes xAxis const xAxisRaw = transformedProps.echartOptions.xAxis as any; diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/helpers.ts b/superset-frontend/plugins/plugin-chart-echarts/test/helpers.ts new file mode 100644 index 00000000000..2fe9dc8ae03 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/helpers.ts @@ -0,0 +1,110 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { ChartProps, ChartDataResponseResult } from '@superset-ui/core'; +import { supersetTheme } from '@apache-superset/core/ui'; + +/** + * Datasource shape used by Echarts Timeseries and Mixed Timeseries chart props. + */ +export interface EchartsTimeseriesTestDatasource { + verboseMap?: Record; + columnFormats?: Record; + currencyFormats?: Record; + currencyCodeColumn?: string; +} + +const DEFAULT_DATASOURCE: EchartsTimeseriesTestDatasource = { + verboseMap: {}, + columnFormats: {}, + currencyFormats: {}, +}; + +/** + * Form data shape that at minimum has datasource and viz_type (used for merging). + */ +export interface EchartsTimeseriesTestFormDataBase { + datasource?: string; + viz_type?: string; + [key: string]: unknown; +} + +/** + * Config for creating Echarts Timeseries-style chart props in tests. + * Shared by Timeseries and Mixed Timeseries transformProps tests. + */ +export interface CreateEchartsTimeseriesTestChartPropsConfig { + defaultFormData: TFormData; + defaultVizType: string; + defaultQueriesData?: ChartDataResponseResult[]; + formData?: Partial; + queriesData?: ChartDataResponseResult[]; + datasource?: EchartsTimeseriesTestDatasource; + annotationData?: Record; + width?: number; + height?: number; +} + +/** + * Creates chart props for Echarts Timeseries-style plugins in tests. + * Merges partial formData with defaultFormData and builds a ChartProps-like object. + * Use this to avoid duplicating createTestChartProps in Timeseries and Mixed Timeseries tests. + * + * @param config - defaultFormData, defaultVizType, defaultQueriesData, and optional overrides + * @returns Chart props object typed as TProps (e.g. EchartsTimeseriesChartProps) + */ +export function createEchartsTimeseriesTestChartProps< + TFormData extends EchartsTimeseriesTestFormDataBase, + TProps, +>(config: CreateEchartsTimeseriesTestChartPropsConfig): TProps { + const { + defaultFormData, + defaultVizType, + defaultQueriesData = [], + formData: partialFormData = {}, + queriesData: customQueriesData, + datasource: customDatasource, + annotationData, + width = 800, + height = 600, + } = config; + + const partial = partialFormData as Partial; + const fullFormData = { + ...defaultFormData, + ...partialFormData, + datasource: partial.datasource ?? '3__table', + viz_type: partial.viz_type ?? defaultVizType, + } as TFormData; + + const chartProps = new ChartProps({ + formData: fullFormData, + width, + height, + queriesData: customQueriesData ?? defaultQueriesData, + theme: supersetTheme, + datasource: customDatasource ?? { ...DEFAULT_DATASOURCE }, + ...(annotationData !== undefined && { annotationData }), + }); + + return { + ...chartProps, + formData: fullFormData, + queriesData: customQueriesData ?? defaultQueriesData, + } as TProps; +}