This is an automated email from the ASF dual-hosted git repository.

msyavuz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 080f629ea21 fix(echarts): formula annotations not rendering with 
dataset-level columns label (#37522)
080f629ea21 is described below

commit 080f629ea219028e850f1865aa513451513d048b
Author: Jamile Celento <[email protected]>
AuthorDate: Fri Feb 13 06:37:19 2026 -0300

    fix(echarts): formula annotations not rendering with dataset-level columns 
label (#37522)
---
 .../src/MixedTimeseries/transformProps.ts          |   2 +-
 .../src/Timeseries/transformProps.ts               |   3 +-
 .../test/MixedTimeseries/transformProps.test.ts    | 273 +++++---
 .../test/Timeseries/transformProps.test.ts         | 710 ++++++++++++---------
 .../plugins/plugin-chart-echarts/test/helpers.ts   | 110 ++++
 5 files changed, 726 insertions(+), 372 deletions(-)

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<ChartDataResponseResult> & {
+    label_map?: Record<string, string[]>;
+  },
+): 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<string, string[]> };
+}
+
+/** 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 chartPropsConfig = {
-  formData,
-  width: 800,
-  height: 600,
-  queriesData,
-  theme: supersetTheme,
-};
+const queriesData: ChartDataResponseResult[] = [
+  createTestQueryData(defaultQueryRows, { label_map: defaultLabelMap }),
+  createTestQueryData(defaultQueryRows, { label_map: defaultLabelMap }),
+];
 
 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<ChartDataResponseResult> & {
+    label_map?: Record<string, string[]>;
+  },
+): 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<string, string[]> };
+}
+
 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<EchartsTimeseriesFormData>;
+  queriesData?: ChartDataResponseResult[];
+  annotationData?: Record<string, unknown>;
+  datasource?: {
+    verboseMap?: Record<string, string>;
+    columnFormats?: Record<string, string>;
+    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<EchartsTimeseriesFormData> = {
+      ...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 },
-    };
-
-    const chartProps = new ChartProps(updatedChartPropsConfig);
+      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(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 },
-    };
-
-    const chartProps = new ChartProps(updatedChartPropsConfig);
+      queriesData,
+    });
 
-    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 },
-    };
-
-    const chartProps = new ChartProps(updatedChartPropsConfig);
+      queriesData,
+    });
 
-    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<SqlaFormData>) =>
-    new ChartProps({
-      ...chartPropsConfig,
-      formData: { ...formData },
+  const getChartProps = (formDataOverrides: Partial<SqlaFormData>) =>
+    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<SqlaFormData>({
-    ...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<SqlaFormData>({
-    ...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<SqlaFormData>({
-    ...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<string, string>;
+  columnFormats?: Record<string, string>;
+  currencyFormats?: Record<string, { symbol: string; symbolPosition: string }>;
+  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<TFormData> {
+  defaultFormData: TFormData;
+  defaultVizType: string;
+  defaultQueriesData?: ChartDataResponseResult[];
+  formData?: Partial<TFormData>;
+  queriesData?: ChartDataResponseResult[];
+  datasource?: EchartsTimeseriesTestDatasource;
+  annotationData?: Record<string, unknown>;
+  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<TFormData>): TProps {
+  const {
+    defaultFormData,
+    defaultVizType,
+    defaultQueriesData = [],
+    formData: partialFormData = {},
+    queriesData: customQueriesData,
+    datasource: customDatasource,
+    annotationData,
+    width = 800,
+    height = 600,
+  } = config;
+
+  const partial = partialFormData as 
Partial<EchartsTimeseriesTestFormDataBase>;
+  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;
+}

Reply via email to