diff --git a/docs/docs/configuration/theming.mdx b/docs/docs/configuration/theming.mdx index 13ee29d39fe..e37d4eb0274 100644 --- a/docs/docs/configuration/theming.mdx +++ b/docs/docs/configuration/theming.mdx @@ -165,6 +165,206 @@ Or in the CRUD interface theme JSON: This feature works with the stock Docker image - no custom build required! +## ECharts Configuration Overrides + +:::note +Available since Superset 6.0 +::: + +Superset provides fine-grained control over ECharts visualizations through theme-level configuration overrides. This allows you to customize the appearance and behavior of all ECharts-based charts without modifying individual chart configurations. + +### Global ECharts Overrides + +Apply settings to all ECharts visualizations using `echartsOptionsOverrides`: + +```python +THEME_DEFAULT = { + "token": { + "colorPrimary": "#2893B3", + # ... other Ant Design tokens + }, + "echartsOptionsOverrides": { + "grid": { + "left": "10%", + "right": "10%", + "top": "15%", + "bottom": "15%" + }, + "tooltip": { + "backgroundColor": "rgba(0, 0, 0, 0.8)", + "borderColor": "#ccc", + "textStyle": { + "color": "#fff" + } + }, + "legend": { + "textStyle": { + "fontSize": 14, + "fontWeight": "bold" + } + } + } +} +``` + +### Chart-Specific Overrides + +Target specific chart types using `echartsOptionsOverridesByChartType`: + +```python +THEME_DEFAULT = { + "token": { + "colorPrimary": "#2893B3", + # ... other tokens + }, + "echartsOptionsOverridesByChartType": { + "echarts_pie": { + "legend": { + "orient": "vertical", + "right": 10, + "top": "center" + } + }, + "echarts_timeseries": { + "xAxis": { + "axisLabel": { + "rotate": 45, + "fontSize": 12 + } + }, + "dataZoom": [{ + "type": "slider", + "show": True, + "start": 0, + "end": 100 + }] + }, + "echarts_bubble": { + "grid": { + "left": "15%", + "bottom": "20%" + } + } + } +} +``` + +### UI Configuration + +You can also configure ECharts overrides through the theme CRUD interface: + +```json +{ + "token": { + "colorPrimary": "#2893B3" + }, + "echartsOptionsOverrides": { + "grid": { + "left": "10%", + "right": "10%" + }, + "tooltip": { + "backgroundColor": "rgba(0, 0, 0, 0.8)" + } + }, + "echartsOptionsOverridesByChartType": { + "echarts_pie": { + "legend": { + "orient": "vertical", + "right": 10 + } + } + } +} +``` + +### Override Precedence + +The system applies overrides in the following order (last wins): + +1. **Base ECharts theme** - Default Superset styling +2. **Plugin options** - Chart-specific configurations +3. **Global overrides** - `echartsOptionsOverrides` +4. **Chart-specific overrides** - `echartsOptionsOverridesByChartType[chartType]` + +This ensures chart-specific overrides take precedence over global ones. + +### Common Chart Types + +Available chart types for `echartsOptionsOverridesByChartType`: + +- `echarts_timeseries` - Time series/line charts +- `echarts_pie` - Pie and donut charts +- `echarts_bubble` - Bubble/scatter charts +- `echarts_funnel` - Funnel charts +- `echarts_gauge` - Gauge charts +- `echarts_radar` - Radar charts +- `echarts_boxplot` - Box plot charts +- `echarts_treemap` - Treemap charts +- `echarts_sunburst` - Sunburst charts +- `echarts_graph` - Network/graph charts +- `echarts_sankey` - Sankey diagrams +- `echarts_heatmap` - Heatmaps +- `echarts_mixed_timeseries` - Mixed time series + +### Best Practices + +1. **Start with global overrides** for consistent styling across all charts +2. **Use chart-specific overrides** for unique requirements per visualization type +3. **Test thoroughly** as overrides use deep merge - nested objects are combined, but arrays are completely replaced +4. **Document your overrides** to help team members understand custom styling +5. **Consider performance** - complex overrides may impact chart rendering speed + +### Example: Corporate Branding + +```python +# Complete corporate theme with ECharts customization +THEME_DEFAULT = { + "token": { + "colorPrimary": "#1B4D3E", + "fontFamily": "Corporate Sans, Arial, sans-serif" + }, + "echartsOptionsOverrides": { + "grid": { + "left": "8%", + "right": "8%", + "top": "12%", + "bottom": "12%" + }, + "textStyle": { + "fontFamily": "Corporate Sans, Arial, sans-serif" + }, + "title": { + "textStyle": { + "color": "#1B4D3E", + "fontSize": 18, + "fontWeight": "bold" + } + } + }, + "echartsOptionsOverridesByChartType": { + "echarts_timeseries": { + "xAxis": { + "axisLabel": { + "color": "#666", + "fontSize": 11 + } + } + }, + "echarts_pie": { + "legend": { + "textStyle": { + "fontSize": 12 + }, + "itemGap": 20 + } + } + } +} +``` + +This feature provides powerful theming capabilities while maintaining the flexibility of ECharts' extensive configuration options. + ## Advanced Features - **System Themes**: Manage system-wide default and dark themes via UI or configuration diff --git a/superset-frontend/packages/superset-ui-core/src/theme/types.ts b/superset-frontend/packages/superset-ui-core/src/theme/types.ts index a391e80037d..b9e2a000ea3 100644 --- a/superset-frontend/packages/superset-ui-core/src/theme/types.ts +++ b/superset-frontend/packages/superset-ui-core/src/theme/types.ts @@ -127,6 +127,15 @@ export interface SupersetSpecificTokens { // Spinner-related brandSpinnerUrl?: string; brandSpinnerSvg?: string; + + // ECharts-related + /** Global ECharts configuration overrides applied to all chart types */ + echartsOptionsOverrides?: any; + + /** Chart-specific ECharts configuration overrides keyed by viz_type */ + echartsOptionsOverridesByChartType?: { + [chartType: string]: any; + }; } /** diff --git a/superset-frontend/packages/superset-ui-core/src/utils/index.ts b/superset-frontend/packages/superset-ui-core/src/utils/index.ts index aebddc5459a..bb099c9bf82 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/index.ts @@ -33,3 +33,4 @@ export * from './random'; export * from './typedMemo'; export * from './html'; export * from './tooltip'; +export * from './merge'; diff --git a/superset-frontend/packages/superset-ui-core/src/utils/merge.test.ts b/superset-frontend/packages/superset-ui-core/src/utils/merge.test.ts new file mode 100644 index 00000000000..73a27f53648 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/utils/merge.test.ts @@ -0,0 +1,61 @@ +/** + * 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 { mergeReplaceArrays } from './merge'; + +describe('lodash utilities', () => { + describe('mergeReplaceArrays', () => { + it('should merge objects and replace arrays', () => { + const obj1 = { a: [1, 2], b: { c: 3 } }; + const obj2 = { a: [4, 5], b: { d: 6 } }; + + const result = mergeReplaceArrays(obj1, obj2); + + expect(result).toEqual({ + a: [4, 5], // array replaced + b: { c: 3, d: 6 }, // objects merged + }); + }); + + it('should handle precedence with multiple sources', () => { + const base = { x: { y: 1 }, z: [1] }; + const override1 = { x: { y: 2 }, z: [2, 3] }; + const override2 = { x: { y: 3 }, z: [4] }; + + const result = mergeReplaceArrays(base, override1, override2); + + expect(result).toEqual({ + x: { y: 3 }, // last wins + z: [4], // array replaced by last + }); + }); + + it('should handle empty and null values', () => { + const base = { a: [1], b: { x: 1 } }; + const override = { a: [], b: { x: null } }; + + const result = mergeReplaceArrays(base, override); + + expect(result).toEqual({ + a: [], // empty array replaces + b: { x: null }, // null overrides + }); + }); + }); +}); diff --git a/superset-frontend/packages/superset-ui-core/src/utils/merge.ts b/superset-frontend/packages/superset-ui-core/src/utils/merge.ts new file mode 100644 index 00000000000..bde3783f908 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/utils/merge.ts @@ -0,0 +1,52 @@ +/** + * 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 } from 'lodash'; + +/** + * Merges objects using lodash.mergeWith, but replaces arrays instead of concatenating them. + * This is useful for configuration objects where you want to completely override array values + * rather than merge them by index. + * + * @example + * const obj1 = { a: [1, 2], b: { c: 3 } }; + * const obj2 = { a: [4, 5], b: { d: 6 } }; + * mergeReplaceArrays(obj1, obj2); + * // Result: { a: [4, 5], b: { c: 3, d: 6 } } + * + * @example + * // ECharts configuration merging + * const baseConfig = { series: [{ type: 'line' }], grid: { left: '10%' } }; + * const overrides = { series: [{ type: 'bar' }], grid: { right: '10%' } }; + * mergeReplaceArrays(baseConfig, overrides); + * // Result: { series: [{ type: 'bar' }], grid: { left: '10%', right: '10%' } } + * + * @param sources - Objects to merge (rightmost wins for arrays, deep merge for objects) + * @returns Merged object with arrays replaced, not concatenated + */ +export function mergeReplaceArrays(...sources: any[]): T { + const replaceArrays = (objValue: any, srcValue: any) => { + if (Array.isArray(srcValue)) { + return srcValue; // Replace arrays entirely + } + return undefined; // Let lodash handle object merging for non-arrays + }; + + return mergeWith({}, ...sources, replaceArrays); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/EchartsBubble.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/EchartsBubble.tsx index 4aab4a2d240..adfa0acfe30 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/EchartsBubble.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/EchartsBubble.tsx @@ -20,13 +20,14 @@ import { BubbleChartTransformedProps } from './types'; import Echart from '../components/Echart'; export default function EchartsBubble(props: BubbleChartTransformedProps) { - const { height, width, echartOptions, refs } = props; + const { height, width, echartOptions, refs, formData } = props; return ( ); } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx index 4ae0ab49209..c3b2c81a12a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx @@ -21,7 +21,8 @@ import Echart from '../components/Echart'; import { allEventHandlers } from '../utils/eventHandlers'; export default function EchartsFunnel(props: FunnelChartTransformedProps) { - const { height, width, echartOptions, selectedValues, refs } = props; + const { height, width, echartOptions, selectedValues, refs, formData } = + props; const eventHandlers = allEventHandlers(props); @@ -33,6 +34,7 @@ export default function EchartsFunnel(props: FunnelChartTransformedProps) { echartOptions={echartOptions} eventHandlers={eventHandlers} selectedValues={selectedValues} + vizType={formData.vizType} /> ); } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx index 14ec2ebb7c8..3482977f83e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx @@ -21,7 +21,8 @@ import Echart from '../components/Echart'; import { allEventHandlers } from '../utils/eventHandlers'; export default function EchartsGauge(props: GaugeChartTransformedProps) { - const { height, width, echartOptions, selectedValues, refs } = props; + const { height, width, echartOptions, selectedValues, refs, formData } = + props; const eventHandlers = allEventHandlers(props); @@ -33,6 +34,7 @@ export default function EchartsGauge(props: GaugeChartTransformedProps) { echartOptions={echartOptions} eventHandlers={eventHandlers} selectedValues={selectedValues} + vizType={formData.vizType} /> ); } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx index fb82cd53bcd..3f4d4f27747 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx @@ -21,7 +21,8 @@ import Echart from '../components/Echart'; import { allEventHandlers } from '../utils/eventHandlers'; export default function EchartsPie(props: PieChartTransformedProps) { - const { height, width, echartOptions, selectedValues, refs } = props; + const { height, width, echartOptions, selectedValues, refs, formData } = + props; const eventHandlers = allEventHandlers(props); @@ -33,6 +34,7 @@ export default function EchartsPie(props: PieChartTransformedProps) { echartOptions={echartOptions} eventHandlers={eventHandlers} selectedValues={selectedValues} + vizType={formData.vizType} /> ); } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx index b8937bd42d1..e97b06000e6 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx @@ -21,7 +21,8 @@ import Echart from '../components/Echart'; import { allEventHandlers } from '../utils/eventHandlers'; export default function EchartsRadar(props: RadarChartTransformedProps) { - const { height, width, echartOptions, selectedValues, refs } = props; + const { height, width, echartOptions, selectedValues, refs, formData } = + props; const eventHandlers = allEventHandlers(props); return ( @@ -32,6 +33,7 @@ export default function EchartsRadar(props: RadarChartTransformedProps) { echartOptions={echartOptions} eventHandlers={eventHandlers} selectedValues={selectedValues} + vizType={formData.vizType} /> ); } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx index 79bec925a85..03bf68fd2aa 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx @@ -282,6 +282,7 @@ export default function EchartsTimeseries({ eventHandlers={eventHandlers} zrEventHandlers={zrEventHandlers} selectedValues={selectedValues} + vizType={formData.vizType} /> ); 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 77f03521fcf..c4211468809 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/components/Echart.tsx @@ -27,11 +27,9 @@ import { Ref, useState, } from 'react'; -import { merge } from 'lodash'; - import { useSelector } from 'react-redux'; -import { styled, useTheme } from '@superset-ui/core'; +import { styled, useTheme, mergeReplaceArrays } from '@superset-ui/core'; import { use, init, EChartsType, registerLocale } from 'echarts/core'; import { SankeyChart, @@ -131,6 +129,7 @@ function Echart( zrEventHandlers, selectedValues = {}, refs, + vizType, }: EchartsProps, ref: Ref, ) { @@ -237,11 +236,19 @@ function Echart( return echartsTheme; }; - const themedEchartOptions = merge( - {}, - getEchartsTheme(echartOptions), + const baseTheme = getEchartsTheme(echartOptions); + const globalOverrides = theme.echartsOptionsOverrides || {}; + const chartOverrides = vizType + ? theme.echartsOptionsOverridesByChartType?.[vizType] || {} + : {}; + + const themedEchartOptions = mergeReplaceArrays( + baseTheme, echartOptions, + globalOverrides, + chartOverrides, ); + chartRef.current?.setOption(themedEchartOptions, true); } }, [didMount, echartOptions, eventHandlers, zrEventHandlers, theme]); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts index c8da425f65c..817996b3182 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts @@ -55,6 +55,7 @@ export interface EchartsProps { selectedValues?: Record; forceClear?: boolean; refs: Refs; + vizType?: string; } export interface EchartsHandler { 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 new file mode 100644 index 00000000000..931f026cb5f --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/themeOverrides.test.ts @@ -0,0 +1,263 @@ +/** + * 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 { 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, + }, + }, + }; + + const globalOverrides = { + grid: { + left: '10%', + bottom: '15%', + }, + xAxis: { + axisLabel: { + color: '#333', + rotate: 45, + }, + }, + }; + + const result = mergeReplaceArrays(baseOptions, globalOverrides); + + 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('should replace arrays instead of merging them', () => { + const baseOptions = { + series: [ + { name: 'Series 1', type: 'line' }, + { name: 'Series 2', type: 'bar' }, + ], + }; + + const overrides = { + series: [{ name: 'New Series', type: 'pie' }], + }; + + const result = mergeReplaceArrays(baseOptions, overrides); + + // Arrays are replaced entirely, not merged by index + expect(result.series).toEqual([{ name: 'New Series', type: 'pie' }]); + expect(result.series).toHaveLength(1); + }); + + test('should handle null overrides correctly', () => { + const baseOptions = { + grid: { + left: '5%', + right: '5%', + top: '10%', + }, + tooltip: { + show: true, + backgroundColor: '#fff', + }, + }; + + const overrides = { + grid: { + left: null, + bottom: '20%', + }, + tooltip: { + backgroundColor: null, + borderColor: '#ccc', + }, + }; + + const result = mergeReplaceArrays(baseOptions, 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 + }, + }); + }); + + 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('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('should handle function values correctly', () => { + const formatFunction = (value: any) => `${value}%`; + const overrideFunction = (value: any) => `$${value}`; + + const baseOptions = { + yAxis: { + axisLabel: { + formatter: formatFunction, + }, + }, + }; + + const overrides = { + yAxis: { + axisLabel: { + formatter: overrideFunction, + }, + }, + }; + + const result = mergeReplaceArrays(baseOptions, overrides); + + expect(result.yAxis.axisLabel.formatter).toBe(overrideFunction); + expect(result.yAxis.axisLabel.formatter('100')).toBe('$100'); + }); + + test('should handle empty objects and arrays', () => { + const baseOptions = { + series: [{ name: 'Test', data: [1, 2, 3] }], + grid: { left: '5%' }, + }; + + const emptyOverrides = {}; + const arrayOverride = { series: [] }; + const objectOverride = { grid: {} }; + + const resultEmpty = mergeReplaceArrays(baseOptions, emptyOverrides); + const resultArray = mergeReplaceArrays(baseOptions, arrayOverride); + const resultObject = mergeReplaceArrays(baseOptions, objectOverride); + + expect(resultEmpty).toEqual(baseOptions); + // Empty array completely replaces existing array + expect(resultArray.series).toEqual([]); + expect(resultObject.grid).toEqual({ left: '5%' }); + }); +}); diff --git a/superset-frontend/scripts/build.js b/superset-frontend/scripts/build.js old mode 100644 new mode 100755