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

rusackas pushed a commit to branch fix/echarts-series-theme-overrides
in repository https://gitbox.apache.org/repos/asf/superset.git

commit cfc9e0a64fb82f215736ab9dc373bccad7a00e15
Author: Evan Rusackas <[email protected]>
AuthorDate: Thu Feb 12 20:44:04 2026 -0800

    feat(theme): enable generalized ECharts theme overrides for array properties
    
    Adds a custom merge function `mergeEchartsThemeOverrides` that handles
    ECharts theme overrides with special array-to-object merge behavior:
    
    1. Arrays in source values replace destination arrays entirely (backward 
compat)
    2. When source is a plain object and destination is an array, the object
       is merged into each array item (allowing default styles for all items)
    
    This enables theme authors to write intuitive overrides like:
    
    ```js
    echartsOptionsOverridesByChartType: {
      echarts_bar: {
        series: { itemStyle: { borderRadius: 4 } },  // Applied to ALL series
        yAxis: { axisLabel: { rotate: 45 } }         // Applied to ALL y-axes
      }
    }
    ```
    
    Works for any array-based ECharts option: series, xAxis, yAxis, dataZoom,
    visualMap, etc. - no need to handle each property specially.
    
    Co-Authored-By: Claude Opus 4.5 <[email protected]>
---
 .../plugin-chart-echarts/src/components/Echart.tsx |   4 +-
 .../src/utils/themeOverrides.test.ts               | 617 ++++++++++++++-------
 .../src/utils/themeOverrides.ts                    |  91 +++
 3 files changed, 498 insertions(+), 214 deletions(-)

diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx 
b/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx
index c62f5535a30..2e3d5617e96 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx
@@ -29,7 +29,6 @@ import {
 } from 'react';
 import { useSelector } from 'react-redux';
 
-import { mergeReplaceArrays } from '@superset-ui/core';
 import { styled, useTheme } from '@apache-superset/core/ui';
 import { use, init, EChartsType, registerLocale } from 'echarts/core';
 import {
@@ -66,6 +65,7 @@ import {
 import { LabelLayout } from 'echarts/features';
 import { EchartsHandler, EchartsProps, EchartsStylesProps } from '../types';
 import { DEFAULT_LOCALE } from '../constants';
+import { mergeEchartsThemeOverrides } from '../utils/themeOverrides';
 
 // Define this interface here to avoid creating a dependency back to 
superset-frontend,
 // TODO: to move the type to @superset-ui/core
@@ -244,7 +244,7 @@ function Echart(
         ? theme.echartsOptionsOverridesByChartType?.[vizType] || {}
         : {};
 
-      const themedEchartOptions = mergeReplaceArrays(
+      const themedEchartOptions = mergeEchartsThemeOverrides(
         baseTheme,
         echartOptions,
         globalOverrides,
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/utils/themeOverrides.test.ts
 
b/superset-frontend/plugins/plugin-chart-echarts/src/utils/themeOverrides.test.ts
index 931f026cb5f..5ab80512977 100644
--- 
a/superset-frontend/plugins/plugin-chart-echarts/src/utils/themeOverrides.test.ts
+++ 
b/superset-frontend/plugins/plugin-chart-echarts/src/utils/themeOverrides.test.ts
@@ -16,248 +16,441 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { mergeReplaceArrays } from '@superset-ui/core';
-
-describe('Theme Override Deep Merge Behavior', () => {
-  test('should merge nested objects correctly', () => {
-    const baseOptions = {
-      grid: {
-        left: '5%',
-        right: '5%',
-        top: '10%',
-      },
-      xAxis: {
-        type: 'category',
-        axisLabel: {
-          fontSize: 12,
-        },
-      },
-    };
+import { mergeEchartsThemeOverrides } from './themeOverrides';
 
-    const globalOverrides = {
-      grid: {
-        left: '10%',
-        bottom: '15%',
-      },
-      xAxis: {
-        axisLabel: {
-          color: '#333',
-          rotate: 45,
-        },
-      },
-    };
+// 
=============================================================================
+// Basic Deep Merge Behavior
+// 
=============================================================================
 
-    const result = mergeReplaceArrays(baseOptions, globalOverrides);
+test('merges nested objects correctly', () => {
+  const baseOptions = {
+    grid: { left: '5%', right: '5%', top: '10%' },
+    xAxis: { type: 'category', axisLabel: { fontSize: 12 } },
+  };
 
-    expect(result).toEqual({
-      grid: {
-        left: '10%', // overridden
-        right: '5%', // preserved
-        top: '10%', // preserved
-        bottom: '15%', // added
-      },
-      xAxis: {
-        type: 'category', // preserved
-        axisLabel: {
-          fontSize: 12, // preserved
-          color: '#333', // added
-          rotate: 45, // added
-        },
+  const overrides = {
+    grid: { left: '10%', bottom: '15%' },
+    xAxis: { axisLabel: { color: '#333', rotate: 45 } },
+  };
+
+  const result = mergeEchartsThemeOverrides(baseOptions, overrides);
+
+  expect(result).toEqual({
+    grid: {
+      left: '10%', // overridden
+      right: '5%', // preserved
+      top: '10%', // preserved
+      bottom: '15%', // added
+    },
+    xAxis: {
+      type: 'category', // preserved
+      axisLabel: {
+        fontSize: 12, // preserved
+        color: '#333', // added
+        rotate: 45, // added
       },
-    });
+    },
   });
+});
+
+test('handles override precedence correctly (rightmost wins)', () => {
+  const baseTheme = { textStyle: { color: '#000', fontSize: 12 } };
+  const pluginOptions = { textStyle: { fontSize: 14 }, title: { text: 'Chart' 
} };
+  const globalOverrides = { textStyle: { color: '#333' }, grid: { left: '10%' 
} };
+  const chartOverrides = { textStyle: { color: '#666', fontWeight: 'bold' } };
+
+  const result = mergeEchartsThemeOverrides(
+    baseTheme,
+    pluginOptions,
+    globalOverrides,
+    chartOverrides,
+  );
 
-  test('should replace arrays instead of merging them', () => {
-    const baseOptions = {
-      series: [
-        { name: 'Series 1', type: 'line' },
-        { name: 'Series 2', type: 'bar' },
-      ],
-    };
+  expect(result).toEqual({
+    textStyle: {
+      color: '#666', // chart override wins
+      fontSize: 14, // from plugin options
+      fontWeight: 'bold', // from chart override
+    },
+    title: { text: 'Chart' },
+    grid: { left: '10%' },
+  });
+});
 
-    const overrides = {
-      series: [{ name: 'New Series', type: 'pie' }],
-    };
+test('handles null values correctly', () => {
+  const base = { grid: { left: '5%', right: '5%' } };
+  const overrides = { grid: { left: null, bottom: '20%' } };
 
-    const result = mergeReplaceArrays(baseOptions, overrides);
+  const result = mergeEchartsThemeOverrides(base, overrides);
 
-    // Arrays are replaced entirely, not merged by index
-    expect(result.series).toEqual([{ name: 'New Series', type: 'pie' }]);
-    expect(result.series).toHaveLength(1);
+  expect(result.grid).toEqual({
+    left: null,
+    right: '5%',
+    bottom: '20%',
   });
+});
 
-  test('should handle null overrides correctly', () => {
-    const baseOptions = {
-      grid: {
-        left: '5%',
-        right: '5%',
-        top: '10%',
-      },
-      tooltip: {
-        show: true,
-        backgroundColor: '#fff',
-      },
-    };
+test('handles function values correctly', () => {
+  const original = (v: number) => `${v}%`;
+  const override = (v: number) => `$${v}`;
 
-    const overrides = {
-      grid: {
-        left: null,
-        bottom: '20%',
-      },
-      tooltip: {
-        backgroundColor: null,
-        borderColor: '#ccc',
-      },
-    };
+  const base = { yAxis: { axisLabel: { formatter: original } } };
+  const overrides = { yAxis: { axisLabel: { formatter: override } } };
 
-    const result = mergeReplaceArrays(baseOptions, overrides);
+  const result = mergeEchartsThemeOverrides(base, overrides);
 
-    expect(result).toEqual({
-      grid: {
-        left: null, // overridden with null
-        right: '5%', // preserved (undefined values are ignored by lodash 
merge)
-        top: '10%', // preserved
-        bottom: '20%', // added
-      },
-      tooltip: {
-        show: true, // preserved
-        backgroundColor: null, // overridden with null
-        borderColor: '#ccc', // added
-      },
-    });
+  expect(result.yAxis.axisLabel.formatter).toBe(override);
+  expect(result.yAxis.axisLabel.formatter(100)).toBe('$100');
+});
+
+// 
=============================================================================
+// Array Replacement (Backward Compatibility)
+// 
=============================================================================
+
+test('replaces arrays entirely when override is an array', () => {
+  const base = {
+    series: [
+      { name: 'Series 1', type: 'line' },
+      { name: 'Series 2', type: 'bar' },
+    ],
+  };
+
+  const overrides = {
+    series: [{ name: 'New Series', type: 'pie' }],
+  };
+
+  const result = mergeEchartsThemeOverrides(base, overrides);
+
+  expect(result.series).toEqual([{ name: 'New Series', type: 'pie' }]);
+  expect(result.series).toHaveLength(1);
+});
+
+test('empty array override replaces existing array', () => {
+  const base = { series: [{ name: 'Test', data: [1, 2, 3] }] };
+  const overrides = { series: [] };
+
+  const result = mergeEchartsThemeOverrides(base, overrides);
+
+  expect(result.series).toEqual([]);
+});
+
+// 
=============================================================================
+// Object-to-Array Merging (NEW FEATURE)
+// 
=============================================================================
+
+test('merges object override into each series array item', () => {
+  const chartOptions = {
+    series: [
+      { type: 'bar', name: 'Revenue', data: [1, 2, 3] },
+      { type: 'bar', name: 'Profit', data: [4, 5, 6] },
+    ],
+  };
+
+  const override = {
+    series: { itemStyle: { borderRadius: 4 } },
+  };
+
+  const result = mergeEchartsThemeOverrides(chartOptions, override);
+
+  expect(result.series).toHaveLength(2);
+  expect(result.series[0]).toEqual({
+    type: 'bar',
+    name: 'Revenue',
+    data: [1, 2, 3],
+    itemStyle: { borderRadius: 4 },
   });
+  expect(result.series[1]).toEqual({
+    type: 'bar',
+    name: 'Profit',
+    data: [4, 5, 6],
+    itemStyle: { borderRadius: 4 },
+  });
+});
 
-  test('should handle override precedence correctly', () => {
-    const baseTheme = {
-      textStyle: { color: '#000', fontSize: 12 },
-    };
-
-    const pluginOptions = {
-      textStyle: { fontSize: 14 },
-      title: { text: 'Chart Title' },
-    };
-
-    const globalOverrides = {
-      textStyle: { color: '#333' },
-      grid: { left: '10%' },
-    };
-
-    const chartOverrides = {
-      textStyle: { color: '#666', fontWeight: 'bold' },
-      legend: { orient: 'vertical' },
-    };
-
-    // Simulate the merge order in Echart.tsx
-    const result = mergeReplaceArrays(
-      baseTheme,
-      pluginOptions,
-      globalOverrides,
-      chartOverrides,
-    );
-
-    expect(result).toEqual({
-      textStyle: {
-        color: '#666', // chart override wins
-        fontSize: 14, // from plugin options
-        fontWeight: 'bold', // from chart override
-      },
-      title: { text: 'Chart Title' }, // from plugin options
-      grid: { left: '10%' }, // from global override
-      legend: { orient: 'vertical' }, // from chart override
-    });
+test('merges object override into each xAxis array item', () => {
+  const chartOptions = {
+    xAxis: [
+      { type: 'category', data: ['Mon', 'Tue'] },
+      { type: 'value', position: 'top' },
+    ],
+  };
+
+  const override = {
+    xAxis: { axisLabel: { rotate: 45 } },
+  };
+
+  const result = mergeEchartsThemeOverrides(chartOptions, override);
+
+  expect(result.xAxis).toHaveLength(2);
+  expect(result.xAxis[0]).toEqual({
+    type: 'category',
+    data: ['Mon', 'Tue'],
+    axisLabel: { rotate: 45 },
   });
+  expect(result.xAxis[1]).toEqual({
+    type: 'value',
+    position: 'top',
+    axisLabel: { rotate: 45 },
+  });
+});
 
-  test('should preserve deep nested structures', () => {
-    const baseOptions = {
-      xAxis: {
-        axisLabel: {
-          textStyle: {
-            color: '#000',
-            fontSize: 12,
-            fontFamily: 'Arial',
-          },
-        },
-      },
-    };
-
-    const overrides = {
-      xAxis: {
-        axisLabel: {
-          textStyle: {
-            color: '#333',
-            fontWeight: 'bold',
-          },
-          rotate: 45,
-        },
-        splitLine: {
-          show: true,
-        },
-      },
-    };
-
-    const result = mergeReplaceArrays(baseOptions, overrides);
-
-    expect(result).toEqual({
-      xAxis: {
-        axisLabel: {
-          textStyle: {
-            color: '#333', // overridden
-            fontSize: 12, // preserved
-            fontFamily: 'Arial', // preserved
-            fontWeight: 'bold', // added
-          },
-          rotate: 45, // added
-        },
-        splitLine: {
-          show: true, // added
-        },
-      },
-    });
+test('merges object override into each yAxis array item', () => {
+  const chartOptions = {
+    yAxis: [
+      { type: 'value', name: 'Revenue' },
+      { type: 'value', name: 'Count' },
+    ],
+  };
+
+  const override = {
+    yAxis: { axisLine: { show: true }, splitLine: { show: false } },
+  };
+
+  const result = mergeEchartsThemeOverrides(chartOptions, override);
+
+  expect(result.yAxis).toHaveLength(2);
+  expect(result.yAxis[0]).toEqual({
+    type: 'value',
+    name: 'Revenue',
+    axisLine: { show: true },
+    splitLine: { show: false },
+  });
+  expect(result.yAxis[1]).toEqual({
+    type: 'value',
+    name: 'Count',
+    axisLine: { show: true },
+    splitLine: { show: false },
   });
+});
+
+test('merges object override into dataZoom array items', () => {
+  const chartOptions = {
+    dataZoom: [
+      { type: 'inside', xAxisIndex: 0 },
+      { type: 'slider', xAxisIndex: 0 },
+    ],
+  };
+
+  const override = {
+    dataZoom: { filterMode: 'filter' },
+  };
+
+  const result = mergeEchartsThemeOverrides(chartOptions, override);
 
-  test('should handle function values correctly', () => {
-    const formatFunction = (value: any) => `${value}%`;
-    const overrideFunction = (value: any) => `$${value}`;
+  expect(result.dataZoom).toHaveLength(2);
+  expect(result.dataZoom[0]).toEqual({
+    type: 'inside',
+    xAxisIndex: 0,
+    filterMode: 'filter',
+  });
+  expect(result.dataZoom[1]).toEqual({
+    type: 'slider',
+    xAxisIndex: 0,
+    filterMode: 'filter',
+  });
+});
 
-    const baseOptions = {
-      yAxis: {
-        axisLabel: {
-          formatter: formatFunction,
-        },
+test('preserves existing properties when merging into array items', () => {
+  const chartOptions = {
+    series: [
+      {
+        type: 'bar',
+        itemStyle: { color: 'red', borderWidth: 2 },
       },
-    };
+    ],
+  };
+
+  const override = {
+    series: { itemStyle: { borderRadius: 4 } },
+  };
+
+  const result = mergeEchartsThemeOverrides(chartOptions, override);
+
+  expect(result.series[0].itemStyle).toEqual({
+    color: 'red', // preserved
+    borderWidth: 2, // preserved
+    borderRadius: 4, // added
+  });
+});
+
+test('applies multiple object overrides in order', () => {
+  const chartOptions = {
+    series: [{ type: 'bar' }],
+  };
 
-    const overrides = {
-      yAxis: {
-        axisLabel: {
-          formatter: overrideFunction,
-        },
+  const globalOverride = {
+    series: { itemStyle: { borderRadius: 2, color: 'blue' } },
+  };
+
+  const chartOverride = {
+    series: { itemStyle: { borderRadius: 8 } },
+  };
+
+  const result = mergeEchartsThemeOverrides(
+    chartOptions,
+    globalOverride,
+    chartOverride,
+  );
+
+  expect(result.series[0].itemStyle).toEqual({
+    borderRadius: 8, // chart override wins
+    color: 'blue', // global override preserved
+  });
+});
+
+test('handles deeply nested overrides in array items', () => {
+  const chartOptions = {
+    series: [
+      {
+        type: 'bar',
+        label: { show: true, position: 'top' },
       },
-    };
+    ],
+  };
 
-    const result = mergeReplaceArrays(baseOptions, overrides);
+  const override = {
+    series: {
+      label: { formatter: '{c}', fontSize: 14 },
+      itemStyle: { borderRadius: [4, 4, 0, 0] },
+    },
+  };
 
-    expect(result.yAxis.axisLabel.formatter).toBe(overrideFunction);
-    expect(result.yAxis.axisLabel.formatter('100')).toBe('$100');
+  const result = mergeEchartsThemeOverrides(chartOptions, override);
+
+  expect(result.series[0]).toEqual({
+    type: 'bar',
+    label: {
+      show: true, // preserved
+      position: 'top', // preserved
+      formatter: '{c}', // added
+      fontSize: 14, // added
+    },
+    itemStyle: { borderRadius: [4, 4, 0, 0] },
   });
+});
+
+// 
=============================================================================
+// Edge Cases
+// 
=============================================================================
 
-  test('should handle empty objects and arrays', () => {
-    const baseOptions = {
-      series: [{ name: 'Test', data: [1, 2, 3] }],
-      grid: { left: '5%' },
-    };
+test('handles single object xAxis (not array) normally', () => {
+  const chartOptions = {
+    xAxis: { type: 'category', data: ['Mon', 'Tue'] },
+  };
 
-    const emptyOverrides = {};
-    const arrayOverride = { series: [] };
-    const objectOverride = { grid: {} };
+  const override = {
+    xAxis: { axisLabel: { rotate: 45 } },
+  };
 
-    const resultEmpty = mergeReplaceArrays(baseOptions, emptyOverrides);
-    const resultArray = mergeReplaceArrays(baseOptions, arrayOverride);
-    const resultObject = mergeReplaceArrays(baseOptions, objectOverride);
+  const result = mergeEchartsThemeOverrides(chartOptions, override);
+
+  expect(result.xAxis).toEqual({
+    type: 'category',
+    data: ['Mon', 'Tue'],
+    axisLabel: { rotate: 45 },
+  });
+});
 
-    expect(resultEmpty).toEqual(baseOptions);
-    // Empty array completely replaces existing array
-    expect(resultArray.series).toEqual([]);
-    expect(resultObject.grid).toEqual({ left: '5%' });
+test('skips non-object array items when merging', () => {
+  const chartOptions = {
+    series: [
+      { type: 'bar' },
+      'invalid', // non-object item
+      null, // null item
+      { type: 'line' },
+    ],
+  };
+
+  const override = {
+    series: { itemStyle: { borderRadius: 4 } },
+  };
+
+  const result = mergeEchartsThemeOverrides(chartOptions, override);
+
+  expect(result.series).toHaveLength(4);
+  expect(result.series[0]).toEqual({
+    type: 'bar',
+    itemStyle: { borderRadius: 4 },
+  });
+  expect(result.series[1]).toBe('invalid'); // unchanged
+  expect(result.series[2]).toBe(null); // unchanged
+  expect(result.series[3]).toEqual({
+    type: 'line',
+    itemStyle: { borderRadius: 4 },
+  });
+});
+
+test('handles empty overrides gracefully', () => {
+  const chartOptions = {
+    series: [{ type: 'bar' }],
+  };
+
+  const result = mergeEchartsThemeOverrides(chartOptions, {});
+
+  expect(result).toEqual(chartOptions);
+});
+
+test('handles missing array property in base', () => {
+  const chartOptions = {
+    grid: { left: '10%' },
+  };
+
+  const override = {
+    series: { itemStyle: { borderRadius: 4 } },
+  };
+
+  const result = mergeEchartsThemeOverrides(chartOptions, override);
+
+  // series override is just added as-is since there's no array to merge into
+  expect(result).toEqual({
+    grid: { left: '10%' },
+    series: { itemStyle: { borderRadius: 4 } },
+  });
+});
+
+test('works with the full Echart.tsx merge pattern', () => {
+  const baseTheme = {
+    textStyle: { color: '#333' },
+  };
+
+  const echartOptions = {
+    series: [
+      { type: 'bar', data: [1, 2, 3] },
+      { type: 'bar', data: [4, 5, 6] },
+    ],
+    xAxis: { type: 'category' },
+  };
+
+  const globalOverrides = {
+    series: { itemStyle: { opacity: 0.8 } },
+  };
+
+  const chartOverrides = {
+    series: { itemStyle: { borderRadius: 4 } },
+    xAxis: { axisLabel: { rotate: 45 } },
+  };
+
+  const result = mergeEchartsThemeOverrides(
+    baseTheme,
+    echartOptions,
+    globalOverrides,
+    chartOverrides,
+  );
+
+  expect(result.textStyle).toEqual({ color: '#333' });
+  expect(result.xAxis).toEqual({
+    type: 'category',
+    axisLabel: { rotate: 45 },
+  });
+  expect(result.series).toHaveLength(2);
+  expect(result.series[0]).toEqual({
+    type: 'bar',
+    data: [1, 2, 3],
+    itemStyle: { opacity: 0.8, borderRadius: 4 },
+  });
+  expect(result.series[1]).toEqual({
+    type: 'bar',
+    data: [4, 5, 6],
+    itemStyle: { opacity: 0.8, borderRadius: 4 },
   });
 });
diff --git 
a/superset-frontend/plugins/plugin-chart-echarts/src/utils/themeOverrides.ts 
b/superset-frontend/plugins/plugin-chart-echarts/src/utils/themeOverrides.ts
new file mode 100644
index 00000000000..79423dd726b
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/themeOverrides.ts
@@ -0,0 +1,91 @@
+/**
+ * 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 { mergeWith, isPlainObject } from 'lodash';
+
+/**
+ * Custom merge function for ECharts theme overrides.
+ *
+ * This function extends lodash's mergeWith with special handling:
+ * 1. Arrays in source values replace destination arrays entirely (backward 
compatibility)
+ * 2. When source is a plain object and destination is an array, the object is 
merged
+ *    into each array item (allowing default styles to be applied to all items)
+ *
+ * This enables theme authors to write intuitive overrides like:
+ * ```js
+ * echartsOptionsOverridesByChartType: {
+ *   echarts_bar: {
+ *     series: { itemStyle: { borderRadius: 4 } },  // Applied to ALL series
+ *     yAxis: { axisLabel: { rotate: 45 } }         // Applied to ALL y-axes
+ *   }
+ * }
+ * ```
+ *
+ * Without this special handling, specifying `series` or `yAxis` as objects 
would
+ * fail because the chart's actual values are arrays, and standard object 
merging
+ * doesn't make sense for array-to-object merges.
+ *
+ * @param sources - Objects to merge (rightmost wins, with special array 
handling)
+ * @returns Merged object with the custom array-object merge behavior
+ *
+ * @example
+ * // Chart has multiple series:
+ * const chartOptions = {
+ *   series: [
+ *     { type: 'bar', name: 'Revenue', data: [1, 2, 3] },
+ *     { type: 'bar', name: 'Profit', data: [4, 5, 6] }
+ *   ]
+ * };
+ *
+ * // Theme override with object (not array):
+ * const override = {
+ *   series: { itemStyle: { borderRadius: 4 } }
+ * };
+ *
+ * // Result: borderRadius applied to EACH series
+ * mergeEchartsThemeOverrides(chartOptions, override);
+ * // {
+ * //   series: [
+ * //     { type: 'bar', name: 'Revenue', data: [1, 2, 3], itemStyle: { 
borderRadius: 4 } },
+ * //     { type: 'bar', name: 'Profit', data: [4, 5, 6], itemStyle: { 
borderRadius: 4 } }
+ * //   ]
+ * // }
+ */
+export function mergeEchartsThemeOverrides<T = any>(...sources: any[]): T {
+  const customizer = (objValue: any, srcValue: any): any => {
+    // If source is an array, replace entirely (backward compatibility)
+    if (Array.isArray(srcValue)) {
+      return srcValue;
+    }
+
+    // If destination is an array and source is a plain object,
+    // merge the object into each array item (apply defaults to all items)
+    if (Array.isArray(objValue) && isPlainObject(srcValue)) {
+      return objValue.map(item =>
+        isPlainObject(item)
+          ? mergeWith({}, item, srcValue, customizer)
+          : item,
+      );
+    }
+
+    // Let lodash handle other cases (deep object merge, primitives, etc.)
+    return undefined;
+  };
+
+  return mergeWith({}, ...sources, customizer);
+}

Reply via email to