You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2025/08/19 16:44:24 UTC
(superset) 02/04: feat: Complete migration of ALL ECharts control panels to React architecture 🎉
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch move-controls
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 184f800ec10e26bfe890e2e8478e3f8fa2ae007a
Author: Evan Rusackas <ev...@rusackas.com>
AuthorDate: Mon Aug 18 21:25:23 2025 -0700
feat: Complete migration of ALL ECharts control panels to React architecture 🎉
Migrated 25 chart control panels from legacy config-based to modern React-based:
Core Charts:
- Pie, Funnel, Sankey, Treemap (already done)
- BoxPlot, Bubble, Gantt, Gauge, Graph
- Heatmap, Histogram, Radar, Sunburst, Tree, Waterfall
BigNumber Variants (3):
- BigNumberTotal
- BigNumberPeriodOverPeriod
- BigNumberWithTrendline
Timeseries Variants (7):
- Area, Step, Line, Bar, Scatter, SmoothLine
- MixedTimeseries (most complex with dual Y-axes)
Key improvements across all migrations:
- Direct React components instead of config objects
- Full TypeScript support with proper types
- Tab-based organization (Data/Customize/Options)
- Proper safety checks and validation
- Conditional rendering for dependent controls
- Single column handling where needed
- Modern control components (DndColumnSelect, etc.)
All charts compile successfully with webpack dev server.
This completes the control panel modernization effort!
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <no...@anthropic.com>
---
...BigNumberPeriodOverPeriodControlPanelSimple.tsx | 497 +++++++++
.../BigNumber/BigNumberPeriodOverPeriod/index.ts | 2 +-
.../BigNumberTotalControlPanelSimple.tsx | 442 ++++++++
.../src/BigNumber/BigNumberTotal/index.ts | 2 +-
.../BigNumberWithTrendlineControlPanelSimple.tsx | 783 ++++++++++++++
.../src/BigNumber/BigNumberWithTrendline/index.ts | 2 +-
.../BoxPlotControlPanelSimple.tsx} | 298 ++++--
.../plugin-chart-echarts/src/BoxPlot/index.ts | 2 +-
.../src/Bubble/BubbleControlPanelSimple.tsx | 629 +++++++++++
.../plugin-chart-echarts/src/Bubble/index.ts | 2 +-
.../src/Gantt/GanttControlPanelSimple.tsx | 596 +++++++++++
.../plugin-chart-echarts/src/Gantt/index.ts | 2 +-
.../src/Gauge/GaugeControlPanelSimple.tsx | 665 ++++++++++++
.../plugin-chart-echarts/src/Gauge/index.ts | 2 +-
.../src/Graph/GraphControlPanelSimple.tsx | 681 ++++++++++++
.../plugin-chart-echarts/src/Graph/index.ts | 2 +-
.../src/Heatmap/HeatmapControlPanelSimple.tsx | 754 +++++++++++++
.../plugin-chart-echarts/src/Heatmap/index.ts | 2 +-
.../src/Histogram/HistogramControlPanelSimple.tsx | 493 +++++++++
.../plugin-chart-echarts/src/Histogram/index.ts | 2 +-
.../MixedTimeseriesControlPanelSimple.tsx | 1111 ++++++++++++++++++++
.../src/MixedTimeseries/index.ts | 2 +-
.../RadarControlPanelSimple.tsx} | 336 ++++--
.../plugin-chart-echarts/src/Radar/index.ts | 2 +-
.../src/Sankey/SankeyControlPanelSimple.tsx | 10 +-
.../src/Sunburst/SunburstControlPanelSimple.tsx | 538 ++++++++++
.../plugin-chart-echarts/src/Sunburst/index.ts | 2 +-
.../src/Timeseries/Area/AreaControlPanelSimple.tsx | 877 +++++++++++++++
.../src/Timeseries/Area/index.ts | 2 +-
.../Regular/Bar/BarControlPanelSimple.tsx | 1075 +++++++++++++++++++
.../src/Timeseries/Regular/Bar/index.ts | 2 +-
.../Regular/Line/LineControlPanelSimple.tsx | 831 +++++++++++++++
.../src/Timeseries/Regular/Line/index.ts | 2 +-
.../Regular/Scatter/ScatterControlPanelSimple.tsx | 770 ++++++++++++++
.../src/Timeseries/Regular/Scatter/index.ts | 2 +-
.../SmoothLine/SmoothLineControlPanelSimple.tsx | 770 ++++++++++++++
.../src/Timeseries/Regular/SmoothLine/index.ts | 2 +-
.../src/Timeseries/Step/StepControlPanelSimple.tsx | 827 +++++++++++++++
.../src/Timeseries/Step/index.ts | 2 +-
.../src/Tree/TreeControlPanelSimple.tsx | 548 ++++++++++
.../plugins/plugin-chart-echarts/src/Tree/index.ts | 2 +-
.../src/Waterfall/WaterfallControlPanelSimple.tsx | 574 ++++++++++
.../plugin-chart-echarts/src/Waterfall/index.ts | 2 +-
43 files changed, 13946 insertions(+), 201 deletions(-)
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/BigNumberPeriodOverPeriodControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/BigNumberPeriodOverPeriodControlPanelSimple.tsx
new file mode 100644
index 0000000000..f136597777
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/BigNumberPeriodOverPeriodControlPanelSimple.tsx
@@ -0,0 +1,497 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { t, GenericDataType } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import { DndMetricSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../../src/explore/components/controls/CheckboxControl';
+import SelectControl from '../../../../../src/explore/components/controls/SelectControl';
+import CurrencyControl from '../../../../../src/explore/components/controls/CurrencyControl';
+import ControlHeader from '../../../../../src/explore/components/ControlHeader';
+import Control from '../../../../../src/explore/components/Control';
+import ColumnConfigControl from '../../../../../src/explore/components/controls/ColumnConfigControl';
+import { ColorSchemeEnum } from './types';
+
+interface BigNumberPeriodOverPeriodControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for BigNumber Period-over-Period charts.
+ */
+export const BigNumberPeriodOverPeriodControlPanel: FC<BigNumberPeriodOverPeriodControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* Metric */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metric')}
+ description={t('Metric to display')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.metric}
+ onChange={handleChange('metric')}
+ datasource={safeDataSource}
+ name="metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+
+ {/* Row Limit */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limit the number of rows that are returned')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="100"
+ controlId="row_limit"
+ />
+ </div>
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Number format */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Number format')}
+ value={formValues.y_axis_format || ''}
+ onChange={handleChange('y_axis_format')}
+ placeholder="SMART_NUMBER"
+ controlId="y_axis_format"
+ renderTrigger
+ />
+ </div>
+
+ {/* Percent Difference format */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Percent Difference format')}
+ value={formValues.percentDifferenceFormat || ''}
+ onChange={handleChange('percentDifferenceFormat')}
+ placeholder="SMART_NUMBER"
+ controlId="percentDifferenceFormat"
+ renderTrigger
+ />
+ </div>
+
+ {/* Currency format */}
+ <div style={{ marginBottom: 16 }}>
+ <CurrencyControl
+ value={formValues.currency_format}
+ onChange={handleChange('currency_format')}
+ />
+ </div>
+
+ {/* Header Font Size */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Big Number Font Size')}
+ value={formValues.header_font_size || 0.2}
+ onChange={handleChange('header_font_size')}
+ choices={[
+ [0.2, t('Tiny')],
+ [0.3, t('Small')],
+ [0.4, t('Normal')],
+ [0.5, t('Large')],
+ [0.6, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Subtitle */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Subtitle')}
+ description={t('Description text that shows up below your Big Number')}
+ value={formValues.subtitle || ''}
+ onChange={handleChange('subtitle')}
+ controlId="subtitle"
+ renderTrigger
+ />
+ </div>
+
+ {/* Subtitle Font Size */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Subtitle Font Size')}
+ value={formValues.subtitle_font_size || 0.15}
+ onChange={handleChange('subtitle_font_size')}
+ choices={[
+ [0.125, t('Tiny')],
+ [0.15, t('Small')],
+ [0.2, t('Normal')],
+ [0.3, t('Large')],
+ [0.4, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Show Metric Name */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Metric Name')}
+ description={t('Whether to display the metric name')}
+ value={formValues.show_metric_name ?? false}
+ onChange={handleChange('show_metric_name')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Metric Name Font Size - CONDITIONAL */}
+ {formValues.show_metric_name && (
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Metric Name Font Size')}
+ value={formValues.metric_name_font_size || 0.15}
+ onChange={handleChange('metric_name_font_size')}
+ choices={[
+ [0.125, t('Tiny')],
+ [0.15, t('Small')],
+ [0.2, t('Normal')],
+ [0.3, t('Large')],
+ [0.4, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ {/* Comparison font size */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Comparison font size')}
+ value={formValues.subheader_font_size || 0.125}
+ onChange={handleChange('subheader_font_size')}
+ choices={[
+ [0.125, t('Tiny')],
+ [0.15, t('Small')],
+ [0.2, t('Normal')],
+ [0.3, t('Large')],
+ [0.4, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Add color for positive/negative change */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Add color for positive/negative change')}
+ description={t('Add color for positive/negative change')}
+ value={formValues.comparison_color_enabled ?? false}
+ onChange={handleChange('comparison_color_enabled')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Color scheme for comparison - CONDITIONAL */}
+ {formValues.comparison_color_enabled && (
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('color scheme for comparison')}
+ description={t(
+ 'Adds color to the chart symbols based on the positive or ' +
+ 'negative change from the comparison value.',
+ )}
+ value={formValues.comparison_color_scheme || ColorSchemeEnum.Green}
+ onChange={handleChange('comparison_color_scheme')}
+ choices={[
+ [ColorSchemeEnum.Green, 'Green for increase, red for decrease'],
+ [ColorSchemeEnum.Red, 'Red for increase, green for decrease'],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ {/* Column Configuration */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Customize columns')}
+ description={t('Further customize how to display each column')}
+ hovered
+ />
+ <ColumnConfigControl
+ value={formValues.column_config || {}}
+ onChange={handleChange('column_config')}
+ configFormLayout={{
+ [GenericDataType.Numeric]: [
+ {
+ tab: t('General'),
+ children: [
+ ['customColumnName'],
+ ['displayTypeIcon'],
+ ['visible'],
+ ],
+ },
+ ],
+ }}
+ columnsPropsObject={{
+ colnames: ['Previous value', 'Delta', 'Percent change'],
+ coltypes: [
+ GenericDataType.Numeric,
+ GenericDataType.Numeric,
+ GenericDataType.Numeric,
+ ],
+ }}
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart/Viz Type Picker */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Big Number with Period-over-Period')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t('Displays a metric value with comparison to previous period')}
+ </div>
+ </div>
+
+ {/* Tabs for Data and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(BigNumberPeriodOverPeriodControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[BigNumberPeriodOverPeriodControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ metric: {
+ default: null,
+ label: t('Metric'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ row_limit: {
+ default: 100,
+ label: t('Row limit'),
+ },
+ y_axis_format: {
+ default: '',
+ label: t('Number format'),
+ renderTrigger: true,
+ },
+ percentDifferenceFormat: {
+ default: '',
+ label: t('Percent Difference format'),
+ renderTrigger: true,
+ },
+ currency_format: {
+ default: undefined,
+ label: t('Currency format'),
+ renderTrigger: true,
+ },
+ header_font_size: {
+ default: 0.2,
+ label: t('Big Number Font Size'),
+ renderTrigger: true,
+ },
+ subtitle: {
+ default: '',
+ label: t('Subtitle'),
+ renderTrigger: true,
+ },
+ subtitle_font_size: {
+ default: 0.15,
+ label: t('Subtitle Font Size'),
+ renderTrigger: true,
+ },
+ show_metric_name: {
+ default: false,
+ label: t('Show Metric Name'),
+ renderTrigger: true,
+ },
+ metric_name_font_size: {
+ default: 0.15,
+ label: t('Metric Name Font Size'),
+ renderTrigger: true,
+ },
+ subheader_font_size: {
+ default: 0.125,
+ label: t('Comparison font size'),
+ renderTrigger: true,
+ },
+ comparison_color_enabled: {
+ default: false,
+ label: t('Add color for positive/negative change'),
+ renderTrigger: true,
+ },
+ comparison_color_scheme: {
+ default: ColorSchemeEnum.Green,
+ label: t('color scheme for comparison'),
+ renderTrigger: true,
+ },
+ column_config: {
+ default: {},
+ label: t('Customize columns'),
+ renderTrigger: true,
+ },
+ },
+ formDataOverrides: (formData: any) => ({
+ ...formData,
+ metric: formData.metric,
+ }),
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/index.ts
index 53ab44c2aa..3f84766995 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/index.ts
@@ -18,7 +18,7 @@
*/
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import buildQuery from './buildQuery';
-import controlPanel from './controlPanel';
+import controlPanel from './BigNumberPeriodOverPeriodControlPanelSimple';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/BigNumberTotalControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/BigNumberTotalControlPanelSimple.tsx
new file mode 100644
index 0000000000..3d9903dd74
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/BigNumberTotalControlPanelSimple.tsx
@@ -0,0 +1,442 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { GenericDataType, SMART_DATE_ID, t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ D3_FORMAT_DOCS,
+ D3_TIME_FORMAT_OPTIONS,
+ Dataset,
+} from '@superset-ui/chart-controls';
+import { DndMetricSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../../src/explore/components/controls/CheckboxControl';
+import SelectControl from '../../../../../src/explore/components/controls/SelectControl';
+import CurrencyControl from '../../../../../src/explore/components/controls/CurrencyControl';
+import ControlHeader from '../../../../../src/explore/components/ControlHeader';
+import Control from '../../../../../src/explore/components/Control';
+import ConditionalFormattingControl from '../../../../../src/explore/components/controls/ConditionalFormattingControl';
+
+interface BigNumberTotalControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for BigNumber Total charts.
+ */
+export const BigNumberTotalControlPanel: FC<BigNumberTotalControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* Metric */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metric')}
+ description={t('Metric to display')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.metric}
+ onChange={handleChange('metric')}
+ datasource={safeDataSource}
+ name="metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+
+ {/* Time Column */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Time Column')}
+ description={t('Select the time column for temporal filtering')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.granularity || null}
+ onChange={handleChange('granularity')}
+ choices={safeColumns
+ .filter((col: any) => col.is_dttm)
+ .map((col: any) => [col.column_name, col.verbose_name || col.column_name])}
+ clearable
+ placeholder={t('Select time column')}
+ renderTrigger
+ />
+ </div>
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Header Font Size */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Big Number Font Size')}
+ value={formValues.header_font_size || 0.4}
+ onChange={handleChange('header_font_size')}
+ choices={[
+ [0.2, t('Tiny')],
+ [0.3, t('Small')],
+ [0.4, t('Normal')],
+ [0.5, t('Large')],
+ [0.6, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Subtitle */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Subtitle')}
+ description={t('Description text that shows up below your Big Number')}
+ value={formValues.subtitle || ''}
+ onChange={handleChange('subtitle')}
+ controlId="subtitle"
+ renderTrigger
+ />
+ </div>
+
+ {/* Subtitle Font Size */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Subtitle Font Size')}
+ value={formValues.subtitle_font_size || 0.15}
+ onChange={handleChange('subtitle_font_size')}
+ choices={[
+ [0.125, t('Tiny')],
+ [0.15, t('Small')],
+ [0.2, t('Normal')],
+ [0.3, t('Large')],
+ [0.4, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Show Metric Name */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Metric Name')}
+ description={t('Whether to display the metric name')}
+ value={formValues.show_metric_name ?? false}
+ onChange={handleChange('show_metric_name')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Metric Name Font Size - CONDITIONAL */}
+ {formValues.show_metric_name && (
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Metric Name Font Size')}
+ value={formValues.metric_name_font_size || 0.15}
+ onChange={handleChange('metric_name_font_size')}
+ choices={[
+ [0.125, t('Tiny')],
+ [0.15, t('Small')],
+ [0.2, t('Normal')],
+ [0.3, t('Large')],
+ [0.4, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+ </div>
+
+ {/* Formatting Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Formatting')}</h4>
+
+ {/* Number format */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Number format')}
+ value={formValues.y_axis_format || ''}
+ onChange={handleChange('y_axis_format')}
+ placeholder="SMART_NUMBER"
+ controlId="y_axis_format"
+ renderTrigger
+ />
+ </div>
+
+ {/* Currency format */}
+ <div style={{ marginBottom: 16 }}>
+ <CurrencyControl
+ value={formValues.currency_format}
+ onChange={handleChange('currency_format')}
+ />
+ </div>
+
+ {/* Date format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Date format')}
+ description={D3_FORMAT_DOCS}
+ value={formValues.time_format || SMART_DATE_ID}
+ onChange={handleChange('time_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Force Date Format */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Force date format')}
+ description={t(
+ 'Use date formatting even when metric value is not a timestamp',
+ )}
+ value={formValues.force_timestamp_formatting ?? false}
+ onChange={handleChange('force_timestamp_formatting')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Conditional Formatting */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Conditional Formatting')}
+ description={t('Apply conditional color formatting to metric')}
+ hovered
+ />
+ <ConditionalFormattingControl
+ value={formValues.conditional_formatting || []}
+ onChange={handleChange('conditional_formatting')}
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart/Viz Type Picker */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Big Number')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t('Displays a single metric value in a large font')}
+ </div>
+ </div>
+
+ {/* Tabs for Data and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(BigNumberTotalControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[BigNumberTotalControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ metric: {
+ default: null,
+ label: t('Metric'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ granularity: {
+ default: null,
+ label: t('Time Column'),
+ },
+ header_font_size: {
+ default: 0.4,
+ label: t('Big Number Font Size'),
+ renderTrigger: true,
+ },
+ subtitle: {
+ default: '',
+ label: t('Subtitle'),
+ renderTrigger: true,
+ },
+ subtitle_font_size: {
+ default: 0.15,
+ label: t('Subtitle Font Size'),
+ renderTrigger: true,
+ },
+ show_metric_name: {
+ default: false,
+ label: t('Show Metric Name'),
+ renderTrigger: true,
+ },
+ metric_name_font_size: {
+ default: 0.15,
+ label: t('Metric Name Font Size'),
+ renderTrigger: true,
+ },
+ y_axis_format: {
+ default: '',
+ label: t('Number format'),
+ renderTrigger: true,
+ },
+ currency_format: {
+ default: undefined,
+ label: t('Currency format'),
+ renderTrigger: true,
+ },
+ time_format: {
+ default: SMART_DATE_ID,
+ label: t('Date format'),
+ renderTrigger: true,
+ },
+ force_timestamp_formatting: {
+ default: false,
+ label: t('Force date format'),
+ renderTrigger: true,
+ },
+ conditional_formatting: {
+ default: [],
+ label: t('Conditional Formatting'),
+ renderTrigger: true,
+ },
+ },
+ formDataOverrides: (formData: any) => ({
+ ...formData,
+ metric: formData.metric,
+ }),
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts
index d77ea1e4cd..ff057179e1 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import { t, Behavior } from '@superset-ui/core';
-import controlPanel from './controlPanel';
+import controlPanel from './BigNumberTotalControlPanelSimple';
import transformProps from './transformProps';
import buildQuery from './buildQuery';
import example1 from './images/BigNumber.jpg';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/BigNumberWithTrendlineControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/BigNumberWithTrendlineControlPanelSimple.tsx
new file mode 100644
index 0000000000..2590b485eb
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/BigNumberWithTrendlineControlPanelSimple.tsx
@@ -0,0 +1,783 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { SMART_DATE_ID, t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ D3_FORMAT_DOCS,
+ D3_TIME_FORMAT_OPTIONS,
+} from '@superset-ui/chart-controls';
+import { DndColumnSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndMetricSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../../src/explore/components/controls/CheckboxControl';
+import SelectControl from '../../../../../src/explore/components/controls/SelectControl';
+import CurrencyControl from '../../../../../src/explore/components/controls/CurrencyControl';
+import ColorPickerControl from '../../../../../src/explore/components/controls/ColorPickerControl';
+import ControlHeader from '../../../../../src/explore/components/ControlHeader';
+
+interface BigNumberWithTrendlineControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for BigNumber with Trendline charts.
+ */
+export const BigNumberWithTrendlineControlPanel: FC<BigNumberWithTrendlineControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* Temporal X-Axis */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Temporal X-Axis')}
+ description={t('Column used for temporal grouping')}
+ hovered
+ />
+ <DndColumnSelect
+ value={formValues.x_axis ? [formValues.x_axis] : []}
+ onChange={(val: any) => {
+ const singleValue = Array.isArray(val) ? val[0] : val;
+ handleChange('x_axis')(singleValue);
+ }}
+ options={safeColumns.filter((col: any) => col.is_dttm)}
+ name="x_axis"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Add temporal column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ </div>
+
+ {/* Time Grain */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Time Grain')}
+ description={t('The time granularity for the visualization')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.time_grain_sqla || null}
+ onChange={handleChange('time_grain_sqla')}
+ choices={[
+ [null, t('Auto')],
+ ['PT1S', t('Second')],
+ ['PT1M', t('Minute')],
+ ['PT5M', t('5 Minutes')],
+ ['PT10M', t('10 Minutes')],
+ ['PT15M', t('15 Minutes')],
+ ['PT30M', t('30 Minutes')],
+ ['PT1H', t('Hour')],
+ ['P1D', t('Day')],
+ ['P1W', t('Week')],
+ ['P1M', t('Month')],
+ ['P3M', t('Quarter')],
+ ['P1Y', t('Year')],
+ ]}
+ clearable
+ renderTrigger
+ />
+ </div>
+
+ {/* Aggregation */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Aggregation')}
+ description={t('Aggregation function to apply')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.time_range_endpoints || null}
+ onChange={handleChange('time_range_endpoints')}
+ choices={[
+ [null, t('Auto')],
+ ['inclusive', t('Inclusive of both endpoints')],
+ ['exclusive', t('Exclusive of both endpoints')],
+ ]}
+ clearable
+ renderTrigger
+ />
+ </div>
+
+ {/* Metric */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metric')}
+ description={t('Metric to display')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.metric}
+ onChange={handleChange('metric')}
+ datasource={safeDataSource}
+ name="metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+ </div>
+ );
+
+ // Options tab content (labeled as "Options" in original)
+ const optionsTabContent = (
+ <div>
+ {/* Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Comparison Options')}</h4>
+
+ {/* Comparison Period Lag */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Comparison Period Lag')}
+ description={t(
+ 'Based on granularity, number of time periods to compare against',
+ )}
+ value={formValues.compare_lag || ''}
+ onChange={handleChange('compare_lag')}
+ isInt
+ controlId="compare_lag"
+ />
+ </div>
+
+ {/* Comparison suffix */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Comparison suffix')}
+ description={t('Suffix to apply after the percentage display')}
+ value={formValues.compare_suffix || ''}
+ onChange={handleChange('compare_suffix')}
+ controlId="compare_suffix"
+ />
+ </div>
+
+ {/* Show Timestamp */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Timestamp')}
+ description={t('Whether to display the timestamp')}
+ value={formValues.show_timestamp ?? false}
+ onChange={handleChange('show_timestamp')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Show Trend Line */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Trend Line')}
+ description={t('Whether to display the trend line')}
+ value={formValues.show_trend_line ?? true}
+ onChange={handleChange('show_trend_line')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Start y-axis at 0 */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Start y-axis at 0')}
+ description={t(
+ 'Start y-axis at zero. Uncheck to start y-axis at minimum value in the data.',
+ )}
+ value={formValues.start_y_axis_at_zero ?? true}
+ onChange={handleChange('start_y_axis_at_zero')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Fix to selected Time Range - CONDITIONAL */}
+ {formValues.time_range && formValues.time_range !== 'No filter' && (
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Fix to selected Time Range')}
+ description={t(
+ 'Fix the trend line to the full time range specified in case filtered results do not include the start or end dates',
+ )}
+ value={formValues.time_range_fixed ?? false}
+ onChange={handleChange('time_range_fixed')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Color Picker */}
+ <div style={{ marginBottom: 16 }}>
+ <ColorPickerControl
+ value={formValues.color_picker}
+ onChange={handleChange('color_picker')}
+ />
+ </div>
+
+ {/* Header Font Size */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Big Number Font Size')}
+ value={formValues.header_font_size || 0.4}
+ onChange={handleChange('header_font_size')}
+ choices={[
+ [0.2, t('Tiny')],
+ [0.3, t('Small')],
+ [0.4, t('Normal')],
+ [0.5, t('Large')],
+ [0.6, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Subheader Font Size */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Subheader Font Size')}
+ value={formValues.subheader_font_size || 0.15}
+ onChange={handleChange('subheader_font_size')}
+ choices={[
+ [0.125, t('Tiny')],
+ [0.15, t('Small')],
+ [0.2, t('Normal')],
+ [0.3, t('Large')],
+ [0.4, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Subtitle */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Subtitle')}
+ description={t('Description text that shows up below your Big Number')}
+ value={formValues.subtitle || ''}
+ onChange={handleChange('subtitle')}
+ controlId="subtitle"
+ renderTrigger
+ />
+ </div>
+
+ {/* Subtitle Font Size */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Subtitle Font Size')}
+ value={formValues.subtitle_font_size || 0.15}
+ onChange={handleChange('subtitle_font_size')}
+ choices={[
+ [0.125, t('Tiny')],
+ [0.15, t('Small')],
+ [0.2, t('Normal')],
+ [0.3, t('Large')],
+ [0.4, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Show Metric Name */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Metric Name')}
+ description={t('Whether to display the metric name')}
+ value={formValues.show_metric_name ?? false}
+ onChange={handleChange('show_metric_name')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Metric Name Font Size - CONDITIONAL */}
+ {formValues.show_metric_name && (
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Metric Name Font Size')}
+ value={formValues.metric_name_font_size || 0.15}
+ onChange={handleChange('metric_name_font_size')}
+ choices={[
+ [0.125, t('Tiny')],
+ [0.15, t('Small')],
+ [0.2, t('Normal')],
+ [0.3, t('Large')],
+ [0.4, t('Huge')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+ </div>
+
+ {/* Formatting Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Formatting')}</h4>
+
+ {/* Number format */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Number format')}
+ value={formValues.y_axis_format || ''}
+ onChange={handleChange('y_axis_format')}
+ placeholder="SMART_NUMBER"
+ controlId="y_axis_format"
+ renderTrigger
+ />
+ </div>
+
+ {/* Currency format */}
+ <div style={{ marginBottom: 16 }}>
+ <CurrencyControl
+ value={formValues.currency_format}
+ onChange={handleChange('currency_format')}
+ />
+ </div>
+
+ {/* Date format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Date format')}
+ description={D3_FORMAT_DOCS}
+ value={formValues.time_format || SMART_DATE_ID}
+ onChange={handleChange('time_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Force Date Format */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Force date format')}
+ description={t(
+ 'Use date formatting even when metric value is not a timestamp',
+ )}
+ value={formValues.force_timestamp_formatting ?? false}
+ onChange={handleChange('force_timestamp_formatting')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Advanced Analytics Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Advanced Analytics')}</h4>
+
+ {/* Rolling Window subsection */}
+ <div style={{ marginBottom: 16 }}>
+ <h5>{t('Rolling Window')}</h5>
+
+ {/* Rolling Function */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Rolling Function')}
+ description={t(
+ 'Defines a rolling window function to apply, works along ' +
+ 'with the [Periods] text box',
+ )}
+ value={formValues.rolling_type || 'None'}
+ onChange={handleChange('rolling_type')}
+ choices={[
+ ['None', t('None')],
+ ['mean', t('mean')],
+ ['sum', t('sum')],
+ ['std', t('std')],
+ ['cumsum', t('cumsum')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Periods */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Periods')}
+ description={t(
+ 'Defines the size of the rolling window function, ' +
+ 'relative to the time granularity selected',
+ )}
+ value={formValues.rolling_periods || ''}
+ onChange={handleChange('rolling_periods')}
+ isInt
+ controlId="rolling_periods"
+ />
+ </div>
+
+ {/* Min Periods */}
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Min Periods')}
+ description={t(
+ 'The minimum number of rolling periods required to show ' +
+ 'a value. For instance if you do a cumulative sum on 7 days ' +
+ 'you may want your "Min Period" to be 7, so that all data points ' +
+ 'shown are the total of 7 periods. This will hide the "ramp up" ' +
+ 'taking place over the first 7 periods',
+ )}
+ value={formValues.min_periods || ''}
+ onChange={handleChange('min_periods')}
+ isInt
+ controlId="min_periods"
+ />
+ </div>
+ </div>
+
+ {/* Resample subsection */}
+ <div style={{ marginBottom: 16 }}>
+ <h5>{t('Resample')}</h5>
+
+ {/* Rule */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Rule')}
+ description={t('Pandas resample rule')}
+ value={formValues.resample_rule || null}
+ onChange={handleChange('resample_rule')}
+ choices={[
+ ['1T', t('1 minutely frequency')],
+ ['1H', t('1 hourly frequency')],
+ ['1D', t('1 calendar day frequency')],
+ ['7D', t('7 calendar day frequency')],
+ ['1MS', t('1 month start frequency')],
+ ['1M', t('1 month end frequency')],
+ ['1AS', t('1 year start frequency')],
+ ['1A', t('1 year end frequency')],
+ ]}
+ freeForm
+ clearable
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Fill method */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Fill method')}
+ description={t('Pandas resample method')}
+ value={formValues.resample_method || null}
+ onChange={handleChange('resample_method')}
+ choices={[
+ ['asfreq', t('Null imputation')],
+ ['zerofill', t('Zero imputation')],
+ ['linear', t('Linear interpolation')],
+ ['ffill', t('Forward values')],
+ ['bfill', t('Backward values')],
+ ['median', t('Median values')],
+ ['mean', t('Mean values')],
+ ['sum', t('Sum values')],
+ ]}
+ freeForm
+ clearable
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'options',
+ label: t('Options'),
+ children: optionsTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart/Viz Type Picker */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Big Number with Trendline')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t('Displays a metric value with a time-series trend chart')}
+ </div>
+ </div>
+
+ {/* Tabs for Data, Options, and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(BigNumberWithTrendlineControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[BigNumberWithTrendlineControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ x_axis: {
+ default: null,
+ label: t('Temporal X-Axis'),
+ },
+ time_grain_sqla: {
+ default: null,
+ label: t('Time Grain'),
+ },
+ metric: {
+ default: null,
+ label: t('Metric'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ compare_lag: {
+ default: '',
+ label: t('Comparison Period Lag'),
+ },
+ compare_suffix: {
+ default: '',
+ label: t('Comparison suffix'),
+ },
+ show_timestamp: {
+ default: false,
+ label: t('Show Timestamp'),
+ renderTrigger: true,
+ },
+ show_trend_line: {
+ default: true,
+ label: t('Show Trend Line'),
+ renderTrigger: true,
+ },
+ start_y_axis_at_zero: {
+ default: true,
+ label: t('Start y-axis at 0'),
+ renderTrigger: true,
+ },
+ time_range_fixed: {
+ default: false,
+ label: t('Fix to selected Time Range'),
+ renderTrigger: true,
+ },
+ color_picker: {
+ default: undefined,
+ label: t('Color Picker'),
+ renderTrigger: true,
+ },
+ header_font_size: {
+ default: 0.4,
+ label: t('Big Number Font Size'),
+ renderTrigger: true,
+ },
+ subheader_font_size: {
+ default: 0.15,
+ label: t('Subheader Font Size'),
+ renderTrigger: true,
+ },
+ subtitle: {
+ default: '',
+ label: t('Subtitle'),
+ renderTrigger: true,
+ },
+ subtitle_font_size: {
+ default: 0.15,
+ label: t('Subtitle Font Size'),
+ renderTrigger: true,
+ },
+ show_metric_name: {
+ default: false,
+ label: t('Show Metric Name'),
+ renderTrigger: true,
+ },
+ metric_name_font_size: {
+ default: 0.15,
+ label: t('Metric Name Font Size'),
+ renderTrigger: true,
+ },
+ y_axis_format: {
+ default: '',
+ label: t('Number format'),
+ renderTrigger: true,
+ },
+ currency_format: {
+ default: undefined,
+ label: t('Currency format'),
+ renderTrigger: true,
+ },
+ time_format: {
+ default: SMART_DATE_ID,
+ label: t('Date format'),
+ renderTrigger: true,
+ },
+ force_timestamp_formatting: {
+ default: false,
+ label: t('Force date format'),
+ renderTrigger: true,
+ },
+ rolling_type: {
+ default: 'None',
+ label: t('Rolling Function'),
+ renderTrigger: true,
+ },
+ rolling_periods: {
+ default: '',
+ label: t('Periods'),
+ },
+ min_periods: {
+ default: '',
+ label: t('Min Periods'),
+ },
+ resample_rule: {
+ default: null,
+ label: t('Rule'),
+ renderTrigger: true,
+ },
+ resample_method: {
+ default: null,
+ label: t('Fill method'),
+ renderTrigger: true,
+ },
+ },
+ formDataOverrides: (formData: any) => ({
+ ...formData,
+ metric: formData.metric,
+ }),
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts
index 4379c88e63..31cf60eacb 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import { t, Behavior } from '@superset-ui/core';
-import controlPanel from './controlPanel';
+import controlPanel from './BigNumberWithTrendlineControlPanelSimple';
import transformProps from './transformProps';
import buildQuery from './buildQuery';
import example from './images/Big_Number_Trendline.jpg';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/SankeyControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/BoxPlotControlPanelSimple.tsx
similarity index 50%
copy from superset-frontend/plugins/plugin-chart-echarts/src/Sankey/SankeyControlPanelSimple.tsx
copy to superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/BoxPlotControlPanelSimple.tsx
index 1372757963..256326165e 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/SankeyControlPanelSimple.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/BoxPlotControlPanelSimple.tsx
@@ -17,22 +17,24 @@
* under the License.
*/
import { FC, useState } from 'react';
-import { t, validateNonEmpty } from '@superset-ui/core';
+import { t } from '@superset-ui/core';
import { Tabs } from 'antd';
import {
ColorSchemeControl,
+ D3_FORMAT_OPTIONS,
+ D3_TIME_FORMAT_OPTIONS,
+ D3_FORMAT_DOCS,
+ D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
} from '@superset-ui/chart-controls';
-
-// Direct component imports
import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
import TextControl from '../../../../src/explore/components/controls/TextControl';
-import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
+import SelectControl from '../../../../src/explore/components/controls/SelectControl';
import ControlHeader from '../../../../src/explore/components/ControlHeader';
import Control from '../../../../src/explore/components/Control';
-interface SankeyControlPanelProps {
+interface BoxPlotControlPanelProps {
onChange?: (field: string, value: any) => void;
value?: Record<string, any>;
datasource?: any;
@@ -42,9 +44,9 @@ interface SankeyControlPanelProps {
}
/**
- * A modern React component-based control panel for Sankey charts.
+ * A modern React component-based control panel for Box Plot charts.
*/
-export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
+export const BoxPlotControlPanel: FC<BoxPlotControlPanelProps> = ({
onChange,
value,
datasource,
@@ -86,30 +88,37 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
// Get form values
const formValues = form_data || value || {};
+ // Helper to check if time_grain_sqla should be visible
+ const shouldShowTimeGrain = () => {
+ return (formValues.columns || []).some((col: any) => {
+ if (typeof col === 'string') {
+ const column = safeColumns.find(c => c.column_name === col);
+ return column?.is_dttm;
+ }
+ return col?.is_dttm || col?.sqlExpression?.includes('date') || col?.sqlExpression?.includes('time');
+ });
+ };
+
// Data tab content
const dataTabContent = (
<div>
- {/* Source Column */}
+ {/* Distribute across (Columns) */}
<div style={{ marginBottom: 16 }}>
<ControlHeader
- label={t('Source')}
- description={t('The column to be used as the source of the edge.')}
+ label={t('Distribute across')}
+ description={t('Columns to calculate distribution across.')}
hovered
/>
{safeColumns.length > 0 ? (
<DndColumnSelect
- value={formValues.source ? [formValues.source] : []}
- onChange={(val: any) => {
- // Source is single select, so take first value
- const sourceValue = Array.isArray(val) && val.length > 0 ? val[0] : null;
- handleChange('source')(sourceValue);
- }}
+ value={formValues.columns || []}
+ onChange={handleChange('columns')}
options={safeColumns}
- name="source"
+ name="columns"
label=""
- multi={false}
+ multi
canDelete
- ghostButtonText={t('Select source column')}
+ ghostButtonText={t('Add column')}
type="DndColumnSelect"
actions={actions}
/>
@@ -120,52 +129,80 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
)}
</div>
- {/* Target Column */}
+ {/* Time Grain - Only show if temporal columns are selected */}
+ {shouldShowTimeGrain() && (
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Time Grain')}
+ description={t('Time granularity for temporal grouping')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.time_grain_sqla || null}
+ onChange={handleChange('time_grain_sqla')}
+ choices={[
+ ['PT1S', t('Second')],
+ ['PT1M', t('Minute')],
+ ['PT5M', t('5 minute')],
+ ['PT10M', t('10 minute')],
+ ['PT15M', t('15 minute')],
+ ['PT30M', t('30 minute')],
+ ['PT1H', t('Hour')],
+ ['P1D', t('Day')],
+ ['P1W', t('Week')],
+ ['P1M', t('Month')],
+ ['P3M', t('Quarter')],
+ ['P1Y', t('Year')],
+ ]}
+ clearable
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ {/* Dimensions (Group by) */}
<div style={{ marginBottom: 16 }}>
<ControlHeader
- label={t('Target')}
- description={t('The column to be used as the target of the edge.')}
+ label={t('Dimensions')}
+ description={t('Categories to group by on the x-axis.')}
hovered
/>
{safeColumns.length > 0 ? (
<DndColumnSelect
- value={formValues.target ? [formValues.target] : []}
- onChange={(val: any) => {
- // Target is single select, so take first value
- const targetValue = Array.isArray(val) && val.length > 0 ? val[0] : null;
- handleChange('target')(targetValue);
- }}
+ value={formValues.groupby || []}
+ onChange={handleChange('groupby')}
options={safeColumns}
- name="target"
+ name="groupby"
label=""
- multi={false}
+ multi
canDelete
- ghostButtonText={t('Select target column')}
+ ghostButtonText={t('Add dimension')}
type="DndColumnSelect"
actions={actions}
/>
) : (
<div style={{ padding: '10px' }}>
- {t('No columns available. Please select a dataset first.')}
+ {t('No columns available for grouping.')}
</div>
)}
</div>
- {/* Metric */}
+ {/* Metrics */}
<div style={{ marginBottom: 16 }}>
<ControlHeader
- label={t('Metric')}
- description={t('Metric to display')}
+ label={t('Metrics')}
+ description={t('Metrics to display')}
hovered
/>
{safeDataSource && safeDataSource.columns ? (
<DndMetricSelect
- value={formValues.metric}
- onChange={handleChange('metric')}
+ value={formValues.metrics || []}
+ onChange={handleChange('metrics')}
datasource={safeDataSource}
- name="metric"
+ name="metrics"
label=""
- multi={false}
+ multi={true}
savedMetrics={safeMetrics}
/>
) : (
@@ -189,7 +226,7 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
formData={formValues}
name="adhoc_filters"
savedMetrics={safeMetrics}
- selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ selectedMetrics={formValues.metrics || []}
type="DndFilterSelect"
actions={actions}
/>
@@ -200,8 +237,22 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
)}
</div>
- {/* Row limit and Sort by metric */}
+ {/* Row and Series Limits */}
<div style={{ display: 'flex', gap: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Series limit')}
+ description={t('Limit number of series that are displayed')}
+ hovered
+ />
+ <TextControl
+ value={formValues.series_limit}
+ onChange={handleChange('series_limit')}
+ isInt
+ placeholder="25"
+ controlId="series_limit"
+ />
+ </div>
<div style={{ flex: 1 }}>
<ControlHeader
label={t('Row limit')}
@@ -212,21 +263,53 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
value={formValues.row_limit}
onChange={handleChange('row_limit')}
isInt
- placeholder="100"
+ placeholder="1000"
controlId="row_limit"
/>
</div>
- <div style={{ flex: 1 }}>
- <CheckboxControl
- label={t('Sort by metric')}
- description={t(
- 'Whether to sort results by the selected metric in descending order',
- )}
- value={formValues.sort_by_metric ?? true}
- onChange={handleChange('sort_by_metric')}
- hovered
+ </div>
+
+ {/* Series limit metric */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Series limit metric')}
+ description={t('Metric used to order the series in the limit')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.series_limit_metric}
+ onChange={handleChange('series_limit_metric')}
+ datasource={safeDataSource}
+ name="series_limit_metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
/>
- </div>
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Whisker Options */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Whisker/outlier options')}
+ description={t('Determines how whiskers and outliers are calculated.')}
+ value={formValues.whiskerOptions || 'Tukey'}
+ onChange={handleChange('whiskerOptions')}
+ choices={[
+ ['Tukey', t('Tukey')],
+ ['Min/max (no outliers)', t('Min/max (no outliers)')],
+ ['2/98 percentiles', t('2/98 percentiles')],
+ ['5/95 percentiles', t('5/95 percentiles')],
+ ['9/91 percentiles', t('9/91 percentiles')],
+ ['10/90 percentiles', t('10/90 percentiles')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
</div>
</div>
);
@@ -259,6 +342,54 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
);
})()}
</div>
+
+ {/* X Tick Layout */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('X Tick Layout')}
+ description={t('The way the ticks are laid out on the X-axis')}
+ value={formValues.x_ticks_layout || 'auto'}
+ onChange={handleChange('x_ticks_layout')}
+ choices={[
+ ['auto', t('auto')],
+ ['flat', t('flat')],
+ ['45°', '45°'],
+ ['90°', '90°'],
+ ['staggered', t('staggered')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Number Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Number format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ value={formValues.number_format || 'SMART_NUMBER'}
+ onChange={handleChange('number_format')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Date Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Date format')}
+ description={D3_FORMAT_DOCS}
+ value={formValues.date_format || 'smart_date'}
+ onChange={handleChange('date_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
</div>
</div>
);
@@ -282,10 +413,10 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
{/* Chart/Viz Type Picker */}
<div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
<div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
- {t('Sankey Chart')}
+ {t('Box Plot')}
</div>
<div style={{ fontSize: '12px', opacity: 0.65 }}>
- {t('Visualize flow data as a Sankey diagram')}
+ {t('Compare distributions across multiple groups with quartiles, median, and outliers')}
</div>
</div>
@@ -301,7 +432,7 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
};
// Mark this component as a modern panel
-(SankeyControlPanel as any).isModernPanel = true;
+(BoxPlotControlPanel as any).isModernPanel = true;
// Create a config that wraps our React component
const config = {
@@ -309,41 +440,68 @@ const config = {
{
label: null,
expanded: true,
- controlSetRows: [[SankeyControlPanel as any]],
+ controlSetRows: [[BoxPlotControlPanel as any]],
},
],
controlOverrides: {
- source: {
- default: null,
- label: t('Source'),
- validators: [validateNonEmpty],
+ columns: {
+ default: [],
+ label: t('Distribute across'),
+ multi: true,
},
- target: {
+ time_grain_sqla: {
default: null,
- label: t('Target'),
- validators: [validateNonEmpty],
+ label: t('Time Grain'),
},
- metric: {
- default: null,
- label: t('Metric'),
+ groupby: {
+ default: [],
+ label: t('Dimensions'),
+ },
+ metrics: {
+ default: [],
+ label: t('Metrics'),
},
adhoc_filters: {
default: [],
label: t('Filters'),
},
+ series_limit: {
+ default: 25,
+ label: t('Series limit'),
+ },
row_limit: {
- default: 100,
+ default: 1000,
label: t('Row limit'),
},
- sort_by_metric: {
- default: true,
- label: t('Sort by metric'),
+ series_limit_metric: {
+ default: null,
+ label: t('Series limit metric'),
+ },
+ whiskerOptions: {
+ default: 'Tukey',
+ label: t('Whisker/outlier options'),
+ renderTrigger: true,
},
color_scheme: {
default: 'supersetColors',
label: t('Color scheme'),
renderTrigger: true,
},
+ x_ticks_layout: {
+ default: 'auto',
+ label: t('X Tick Layout'),
+ renderTrigger: true,
+ },
+ number_format: {
+ default: 'SMART_NUMBER',
+ label: t('Number format'),
+ renderTrigger: true,
+ },
+ date_format: {
+ default: 'smart_date',
+ label: t('Date format'),
+ renderTrigger: true,
+ },
},
};
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts
index a379832f9e..511c7a56f5 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts
@@ -18,7 +18,7 @@
*/
import { Behavior, t } from '@superset-ui/core';
import buildQuery from './buildQuery';
-import controlPanel from './controlPanel';
+import controlPanel from './BoxPlotControlPanelSimple';
import transformProps from './transformProps';
import example from './images/BoxPlot.jpg';
import thumbnail from './images/thumbnail.png';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/BubbleControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/BubbleControlPanelSimple.tsx
new file mode 100644
index 0000000000..db63dd5a0c
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/BubbleControlPanelSimple.tsx
@@ -0,0 +1,629 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+ D3_FORMAT_OPTIONS,
+ D3_TIME_FORMAT_OPTIONS,
+ D3_FORMAT_DOCS,
+ D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
+ formatSelectOptions,
+ sections,
+} from '@superset-ui/chart-controls';
+import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
+import SliderControl from '../../../../src/explore/components/controls/SliderControl';
+import SelectControl from '../../../../src/explore/components/controls/SelectControl';
+import ControlHeader from '../../../../src/explore/components/ControlHeader';
+import Control from '../../../../src/explore/components/Control';
+import { defaultXAxis } from '../defaults';
+
+interface BubbleControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for Bubble charts.
+ */
+export const BubbleControlPanel: FC<BubbleControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Helper for single column selection (series, entity)
+ const handleSingleColumnChange = (field: string) => (val: any) => {
+ const singleValue = Array.isArray(val) ? val[0] : val;
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, singleValue);
+ } else if (onChange) {
+ onChange(field, singleValue);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* Series */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Series')}
+ description={t('Columns to group by')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.series ? [formValues.series] : []}
+ onChange={handleSingleColumnChange('series')}
+ options={safeColumns}
+ name="series"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Add series')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available. Please select a dataset first.')}
+ </div>
+ )}
+ </div>
+
+ {/* Entity */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Entity')}
+ description={t('Entity ID column')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.entity ? [formValues.entity] : []}
+ onChange={handleSingleColumnChange('entity')}
+ options={safeColumns}
+ name="entity"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Add entity')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available.')}
+ </div>
+ )}
+ </div>
+
+ {/* X Axis */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('X Axis')}
+ description={t('Metric for X axis')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.x}
+ onChange={handleChange('x')}
+ datasource={safeDataSource}
+ name="x"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Y Axis */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Y Axis')}
+ description={t('Metric for Y axis')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.y}
+ onChange={handleChange('y')}
+ datasource={safeDataSource}
+ name="y"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Size */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Bubble Size')}
+ description={t('Metric for bubble size')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.size}
+ onChange={handleChange('size')}
+ datasource={safeDataSource}
+ name="size"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={[formValues.x, formValues.y, formValues.size].filter(Boolean)}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+
+ {/* Row limit and Order by */}
+ <div style={{ display: 'flex', gap: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limit the number of rows that are returned')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="100"
+ controlId="row_limit"
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Order descending')}
+ description={t('Sort results by order by descending')}
+ value={formValues.order_desc ?? true}
+ onChange={handleChange('order_desc')}
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Color Scheme */}
+ <div style={{ marginBottom: 16 }}>
+ {(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="color_scheme"
+ value={formValues.color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ {/* Show Legend checkbox */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Legend')}
+ description={t('Whether to display a legend for the chart')}
+ value={formValues.show_legend ?? true}
+ onChange={handleChange('show_legend')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Max Bubble Size */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Max Bubble Size')}
+ description={t('Maximum size of bubbles')}
+ value={formValues.max_bubble_size || '25'}
+ onChange={handleChange('max_bubble_size')}
+ choices={formatSelectOptions([
+ '5',
+ '10',
+ '15',
+ '25',
+ '50',
+ '75',
+ '100',
+ ])}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Bubble Opacity */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Bubble Opacity')}
+ description={t('Opacity of bubbles, 0 means completely transparent, 1 means opaque')}
+ renderTrigger
+ hovered
+ />
+ <SliderControl
+ value={formValues.opacity || 0.6}
+ onChange={handleChange('opacity')}
+ {...{ min: 0, max: 1, step: 0.1 }}
+ />
+ </div>
+
+ {/* Tooltip Size Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Bubble size number format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ value={formValues.tooltipSizeFormat || 'SMART_NUMBER'}
+ onChange={handleChange('tooltipSizeFormat')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* X Axis Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('X Axis')}</h4>
+
+ {/* X Axis Title */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('X Axis Title')}
+ description={t('Title for the X axis')}
+ hovered
+ />
+ <TextControl
+ value={formValues.x_axis_label || ''}
+ onChange={handleChange('x_axis_label')}
+ placeholder={t('X Axis Title')}
+ controlId="x_axis_label"
+ />
+ </div>
+
+ {/* X Axis Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('X Axis Format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ value={formValues.xAxisFormat || 'SMART_NUMBER'}
+ onChange={handleChange('xAxisFormat')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Logarithmic X-axis */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Logarithmic x-axis')}
+ description={t('Use logarithmic scale for X axis')}
+ value={formValues.logXAxis ?? false}
+ onChange={handleChange('logXAxis')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Y Axis Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Y Axis')}</h4>
+
+ {/* Y Axis Title */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Y Axis Title')}
+ description={t('Title for the Y axis')}
+ hovered
+ />
+ <TextControl
+ value={formValues.y_axis_label || ''}
+ onChange={handleChange('y_axis_label')}
+ placeholder={t('Y Axis Title')}
+ controlId="y_axis_label"
+ />
+ </div>
+
+ {/* Y Axis Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Y Axis Format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ value={formValues.yAxisFormat || 'SMART_NUMBER'}
+ onChange={handleChange('yAxisFormat')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Logarithmic Y-axis */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Logarithmic y-axis')}
+ description={t('Use logarithmic scale for Y axis')}
+ value={formValues.logYAxis ?? false}
+ onChange={handleChange('logYAxis')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Truncate Y Axis */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Truncate Y Axis')}
+ description={t('Truncate Y Axis. Can be overridden by specifying a min or max bound.')}
+ value={formValues.truncateYAxis ?? false}
+ onChange={handleChange('truncateYAxis')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart/Viz Type Picker */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Bubble Chart')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t('Visualizes a metric across three dimensions of data in a single chart (X axis, Y axis, and bubble size). Bubbles from the same group can be showcased using bubble color.')}
+ </div>
+ </div>
+
+ {/* Tabs for Data and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(BubbleControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[BubbleControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ series: {
+ default: null,
+ label: t('Series'),
+ },
+ entity: {
+ default: null,
+ label: t('Entity'),
+ },
+ x: {
+ default: null,
+ label: t('X Axis'),
+ },
+ y: {
+ default: null,
+ label: t('Y Axis'),
+ },
+ size: {
+ default: null,
+ label: t('Bubble Size'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ row_limit: {
+ default: 100,
+ label: t('Row limit'),
+ },
+ order_desc: {
+ default: true,
+ label: t('Order descending'),
+ },
+ color_scheme: {
+ default: 'supersetColors',
+ label: t('Color scheme'),
+ renderTrigger: true,
+ },
+ show_legend: {
+ default: true,
+ label: t('Show legend'),
+ renderTrigger: true,
+ },
+ max_bubble_size: {
+ default: '25',
+ label: t('Max Bubble Size'),
+ renderTrigger: true,
+ },
+ opacity: {
+ default: 0.6,
+ label: t('Bubble Opacity'),
+ renderTrigger: true,
+ },
+ tooltipSizeFormat: {
+ default: 'SMART_NUMBER',
+ label: t('Bubble size number format'),
+ renderTrigger: true,
+ },
+ x_axis_label: {
+ default: '',
+ label: t('X Axis Title'),
+ renderTrigger: true,
+ },
+ y_axis_label: {
+ default: '',
+ label: t('Y Axis Title'),
+ renderTrigger: true,
+ },
+ xAxisFormat: {
+ default: 'SMART_NUMBER',
+ label: t('X Axis Format'),
+ renderTrigger: true,
+ },
+ yAxisFormat: {
+ default: 'SMART_NUMBER',
+ label: t('Y Axis Format'),
+ renderTrigger: true,
+ },
+ logXAxis: {
+ default: false,
+ label: t('Logarithmic x-axis'),
+ renderTrigger: true,
+ },
+ logYAxis: {
+ default: false,
+ label: t('Logarithmic y-axis'),
+ renderTrigger: true,
+ },
+ truncateYAxis: {
+ default: false,
+ label: t('Truncate Y Axis'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.ts
index e8880dac1a..3a2fea3a0d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.ts
@@ -20,7 +20,7 @@ import { ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
import thumbnail from './images/thumbnail.png';
import transformProps from './transformProps';
import buildQuery from './buildQuery';
-import controlPanel from './controlPanel';
+import controlPanel from './BubbleControlPanelSimple';
import example1 from './images/example1.png';
import example2 from './images/example2.png';
import { EchartsBubbleChartProps, EchartsBubbleFormData } from './types';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/GanttControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/GanttControlPanelSimple.tsx
new file mode 100644
index 0000000000..ba645822c9
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/GanttControlPanelSimple.tsx
@@ -0,0 +1,596 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+ D3_TIME_FORMAT_OPTIONS,
+ D3_TIME_FORMAT_DOCS,
+} from '@superset-ui/chart-controls';
+import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
+import SelectControl from '../../../../src/explore/components/controls/SelectControl';
+import ControlHeader from '../../../../src/explore/components/ControlHeader';
+import Control from '../../../../src/explore/components/Control';
+import { ColumnSelectControl } from '../controls';
+
+interface GanttControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for Gantt charts.
+ */
+export const GanttControlPanel: FC<GanttControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Helper for single column selection (not array)
+ const handleSingleColumnChange = (field: string) => (val: any) => {
+ const singleValue = Array.isArray(val) ? val[0] : val;
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, singleValue);
+ } else if (onChange) {
+ onChange(field, singleValue);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* Start Time */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Start Time')}
+ description={t('Column representing the start time of tasks')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.start_time ? [formValues.start_time] : []}
+ onChange={handleSingleColumnChange('start_time')}
+ options={safeColumns.filter(col => col.is_dttm)}
+ name="start_time"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Select start time column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No temporal columns available. Please select a dataset first.')}
+ </div>
+ )}
+ </div>
+
+ {/* End Time */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('End Time')}
+ description={t('Column representing the end time of tasks')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.end_time ? [formValues.end_time] : []}
+ onChange={handleSingleColumnChange('end_time')}
+ options={safeColumns.filter(col => col.is_dttm)}
+ name="end_time"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Select end time column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No temporal columns available.')}
+ </div>
+ )}
+ </div>
+
+ {/* Y-axis */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Y-axis')}
+ description={t('Dimension to use on y-axis.')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.y_axis ? [formValues.y_axis] : []}
+ onChange={handleSingleColumnChange('y_axis')}
+ options={safeColumns}
+ name="y_axis"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Select Y-axis column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for Y-axis.')}
+ </div>
+ )}
+ </div>
+
+ {/* Series */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Series')}
+ description={t('Columns to group by')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.series || []}
+ onChange={handleChange('series')}
+ options={safeColumns}
+ name="series"
+ label=""
+ multi
+ canDelete
+ ghostButtonText={t('Add dimension')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for series.')}
+ </div>
+ )}
+ </div>
+
+ {/* Subcategories - Conditional */}
+ {formValues.series && formValues.series.length > 0 && (
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Subcategories')}
+ description={t(
+ 'Divides each category into subcategories based on the values in ' +
+ 'the dimension. It can be used to exclude intersections.',
+ )}
+ value={formValues.subcategories ?? false}
+ onChange={handleChange('subcategories')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ {/* Tooltip Metrics */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Tooltip Metrics')}
+ description={t('Metrics to display in tooltip')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.tooltip_metrics || []}
+ onChange={handleChange('tooltip_metrics')}
+ datasource={safeDataSource}
+ name="tooltip_metrics"
+ label=""
+ multi
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Tooltip Columns */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Tooltip Columns')}
+ description={t('Additional columns to display in tooltip')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.tooltip_columns || []}
+ onChange={handleChange('tooltip_columns')}
+ options={safeColumns}
+ name="tooltip_columns"
+ label=""
+ multi
+ canDelete
+ ghostButtonText={t('Add column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No columns available.')}</div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.tooltip_metrics || []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+
+ {/* Row limit */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limit the number of rows that are returned')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="10000"
+ controlId="row_limit"
+ />
+ </div>
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Color Scheme */}
+ <div style={{ marginBottom: 16 }}>
+ {(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="color_scheme"
+ value={formValues.color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ {/* Show Legend */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Legend')}
+ description={t('Whether to display a legend for the chart')}
+ value={formValues.show_legend ?? true}
+ onChange={handleChange('show_legend')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Zoomable */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Zoomable')}
+ description={t('Whether to enable zoom and pan in the chart')}
+ value={formValues.zoomable ?? true}
+ onChange={handleChange('zoomable')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Show extra controls */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Extra Controls')}
+ description={t('Whether to show extra controls')}
+ value={formValues.show_extra_controls ?? false}
+ onChange={handleChange('show_extra_controls')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* X Axis Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('X Axis')}</h4>
+
+ {/* X Axis Time Bounds */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Bounds')}
+ description={t(
+ 'Bounds for the X-axis. Selected time merges with ' +
+ 'min/max date of the data. When left empty, bounds ' +
+ 'dynamically defined based on the min/max of the data.',
+ )}
+ renderTrigger
+ hovered
+ />
+ <TextControl
+ value={formValues.x_axis_time_bounds || ''}
+ onChange={handleChange('x_axis_time_bounds')}
+ placeholder="Time range"
+ controlId="x_axis_time_bounds"
+ />
+ </div>
+
+ {/* X Axis Time Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Time format')}
+ description={D3_TIME_FORMAT_DOCS}
+ value={formValues.x_axis_time_format || 'smart_date'}
+ onChange={handleChange('x_axis_time_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Tooltip Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Tooltip')}</h4>
+
+ {/* Tooltip Time Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Tooltip time format')}
+ description={D3_TIME_FORMAT_DOCS}
+ value={formValues.tooltip_time_format || 'smart_date'}
+ onChange={handleChange('tooltip_time_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Tooltip Values Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Tooltip values format')}
+ description={t('Format for tooltip values')}
+ value={formValues.tooltip_values_format || 'SMART_NUMBER'}
+ onChange={handleChange('tooltip_values_format')}
+ choices={[
+ ['SMART_NUMBER', 'SMART_NUMBER'],
+ ['.1f', '.1f'],
+ ['.2f', '.2f'],
+ ['.3f', '.3f'],
+ ]}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart/Viz Type Picker */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Gantt Chart')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t('Visualize project schedules and task timelines')}
+ </div>
+ </div>
+
+ {/* Tabs for Data and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(GanttControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[GanttControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ start_time: {
+ default: null,
+ label: t('Start Time'),
+ },
+ end_time: {
+ default: null,
+ label: t('End Time'),
+ },
+ y_axis: {
+ default: null,
+ label: t('Y-axis'),
+ },
+ series: {
+ default: [],
+ label: t('Series'),
+ },
+ subcategories: {
+ default: false,
+ label: t('Subcategories'),
+ renderTrigger: true,
+ },
+ tooltip_metrics: {
+ default: [],
+ label: t('Tooltip Metrics'),
+ },
+ tooltip_columns: {
+ default: [],
+ label: t('Tooltip Columns'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ row_limit: {
+ default: 10000,
+ label: t('Row limit'),
+ },
+ color_scheme: {
+ default: 'supersetColors',
+ label: t('Color scheme'),
+ renderTrigger: true,
+ },
+ show_legend: {
+ default: true,
+ label: t('Show legend'),
+ renderTrigger: true,
+ },
+ zoomable: {
+ default: true,
+ label: t('Zoomable'),
+ renderTrigger: true,
+ },
+ show_extra_controls: {
+ default: false,
+ label: t('Extra Controls'),
+ renderTrigger: true,
+ },
+ x_axis_time_bounds: {
+ default: '',
+ label: t('Bounds'),
+ renderTrigger: true,
+ },
+ x_axis_time_format: {
+ default: 'smart_date',
+ label: t('Time format'),
+ renderTrigger: true,
+ },
+ tooltip_time_format: {
+ default: 'smart_date',
+ label: t('Tooltip time format'),
+ renderTrigger: true,
+ },
+ tooltip_values_format: {
+ default: 'SMART_NUMBER',
+ label: t('Tooltip values format'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/index.ts
index a474ceb35c..5ce14fffab 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/index.ts
@@ -18,7 +18,7 @@
*/
import { Behavior, t } from '@superset-ui/core';
import transformProps from './transformProps';
-import controlPanel from './controlPanel';
+import controlPanel from './GanttControlPanelSimple';
import buildQuery from './buildQuery';
import { EchartsChartPlugin } from '../types';
import thumbnail from './images/thumbnail.png';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/GaugeControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/GaugeControlPanelSimple.tsx
new file mode 100644
index 0000000000..baaad2d21c
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/GaugeControlPanelSimple.tsx
@@ -0,0 +1,665 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+ D3_FORMAT_OPTIONS,
+ D3_FORMAT_DOCS,
+ D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
+} from '@superset-ui/chart-controls';
+import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
+import SliderControl from '../../../../src/explore/components/controls/SliderControl';
+import SelectControl from '../../../../src/explore/components/controls/SelectControl';
+import CurrencyControl from '../../../../src/explore/components/controls/CurrencyControl';
+import ControlHeader from '../../../../src/explore/components/ControlHeader';
+import Control from '../../../../src/explore/components/Control';
+import { DEFAULT_FORM_DATA } from './types';
+
+interface GaugeControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for Gauge charts.
+ */
+export const GaugeControlPanel: FC<GaugeControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* Group by */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Columns to group by')}
+ description={t('Columns to group by')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.groupby || []}
+ onChange={handleChange('groupby')}
+ options={safeColumns}
+ name="groupby"
+ label=""
+ multi
+ canDelete
+ ghostButtonText={t('Add dimension')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available. Please select a dataset first.')}
+ </div>
+ )}
+ </div>
+
+ {/* Metric */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metric')}
+ description={t('Metric to display')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.metric}
+ onChange={handleChange('metric')}
+ datasource={safeDataSource}
+ name="metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+
+ {/* Row limit and Sort by metric */}
+ <div style={{ display: 'flex', gap: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limit the number of rows that are returned')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.row_limit || DEFAULT_FORM_DATA.rowLimit}
+ onChange={handleChange('row_limit')}
+ choices={[...Array(10).keys()].map(n => [n + 1, n + 1])}
+ clearable={false}
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Sort by metric')}
+ description={t('Sort by metric in descending order')}
+ hovered
+ />
+ <CheckboxControl
+ label=""
+ description=""
+ value={formValues.sort_by_metric ?? true}
+ onChange={handleChange('sort_by_metric')}
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* General Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('General')}</h4>
+
+ {/* Min and Max Values */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Min')}
+ description={t('Minimum value on the gauge axis')}
+ hovered
+ />
+ <TextControl
+ value={formValues.min_val ?? DEFAULT_FORM_DATA.minVal}
+ onChange={handleChange('min_val')}
+ isInt
+ placeholder="0"
+ controlId="min_val"
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Max')}
+ description={t('Maximum value on the gauge axis')}
+ hovered
+ />
+ <TextControl
+ value={formValues.max_val ?? DEFAULT_FORM_DATA.maxVal}
+ onChange={handleChange('max_val')}
+ isInt
+ placeholder="100"
+ controlId="max_val"
+ />
+ </div>
+ </div>
+
+ {/* Start and End Angles */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Start angle')}
+ description={t('Angle at which to start progress axis')}
+ hovered
+ />
+ <TextControl
+ value={formValues.start_angle ?? DEFAULT_FORM_DATA.startAngle}
+ onChange={handleChange('start_angle')}
+ placeholder="225"
+ controlId="start_angle"
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('End angle')}
+ description={t('Angle at which to end progress axis')}
+ hovered
+ />
+ <TextControl
+ value={formValues.end_angle ?? DEFAULT_FORM_DATA.endAngle}
+ onChange={handleChange('end_angle')}
+ placeholder="-45"
+ controlId="end_angle"
+ />
+ </div>
+ </div>
+
+ {/* Color Scheme */}
+ <div style={{ marginBottom: 16 }}>
+ {(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="color_scheme"
+ value={formValues.color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ {/* Font Size */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Font size')}
+ description={t('Font size for axis labels, detail value and other text elements')}
+ renderTrigger
+ hovered
+ />
+ <SliderControl
+ value={formValues.font_size ?? DEFAULT_FORM_DATA.fontSize}
+ onChange={handleChange('font_size')}
+ {...{ min: 10, max: 20 }}
+ />
+ </div>
+
+ {/* Number Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Number format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ value={formValues.number_format || DEFAULT_FORM_DATA.numberFormat}
+ onChange={handleChange('number_format')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Currency Format */}
+ <div style={{ marginBottom: 16 }}>
+ <CurrencyControl
+ value={formValues.currency_format}
+ onChange={handleChange('currency_format')}
+ />
+ </div>
+
+ {/* Value Format */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Value format')}
+ description={t('Additional text to add before or after the value, e.g. unit')}
+ hovered
+ />
+ <TextControl
+ value={formValues.value_formatter ?? DEFAULT_FORM_DATA.valueFormatter}
+ onChange={handleChange('value_formatter')}
+ placeholder="{value}"
+ controlId="value_formatter"
+ />
+ </div>
+
+ {/* Show Pointer and Animation */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Show pointer')}
+ description={t('Whether to show the pointer')}
+ value={formValues.show_pointer ?? DEFAULT_FORM_DATA.showPointer}
+ onChange={handleChange('show_pointer')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Animation')}
+ description={t('Whether to animate the progress and the value or just display them')}
+ value={formValues.animation ?? DEFAULT_FORM_DATA.animation}
+ onChange={handleChange('animation')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* Axis Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Axis')}</h4>
+
+ {/* Axis Options */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Show axis line ticks')}
+ description={t('Whether to show minor ticks on the axis')}
+ value={formValues.show_axis_tick ?? DEFAULT_FORM_DATA.showAxisTick}
+ onChange={handleChange('show_axis_tick')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Show split lines')}
+ description={t('Whether to show the split lines on the axis')}
+ value={formValues.show_split_line ?? DEFAULT_FORM_DATA.showSplitLine}
+ onChange={handleChange('show_split_line')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Split Number */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Split number')}
+ description={t('Number of split segments on the axis')}
+ renderTrigger
+ hovered
+ />
+ <SliderControl
+ value={formValues.split_number ?? DEFAULT_FORM_DATA.splitNumber}
+ onChange={handleChange('split_number')}
+ {...{ min: 3, max: 30 }}
+ />
+ </div>
+ </div>
+
+ {/* Progress Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Progress')}</h4>
+
+ {/* Progress Options */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Show progress')}
+ description={t('Whether to show the progress of gauge chart')}
+ value={formValues.show_progress ?? DEFAULT_FORM_DATA.showProgress}
+ onChange={handleChange('show_progress')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Overlap')}
+ description={t('Whether the progress bar overlaps when there are multiple groups of data')}
+ value={formValues.overlap ?? DEFAULT_FORM_DATA.overlap}
+ onChange={handleChange('overlap')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Round Cap */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Round cap')}
+ description={t('Style the ends of the progress bar with a round cap')}
+ value={formValues.round_cap ?? DEFAULT_FORM_DATA.roundCap}
+ onChange={handleChange('round_cap')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Intervals Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Intervals')}</h4>
+
+ {/* Interval Bounds */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Interval bounds')}
+ description={t('Comma-separated interval bounds, e.g. 2,4,5 for intervals 0-2, 2-4 and 4-5. Last number should match the value provided for MAX.')}
+ hovered
+ />
+ <TextControl
+ value={formValues.intervals ?? DEFAULT_FORM_DATA.intervals}
+ onChange={handleChange('intervals')}
+ placeholder="2,4,5"
+ controlId="intervals"
+ />
+ </div>
+
+ {/* Interval Colors */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Interval colors')}
+ description={t('Comma-separated color picks for the intervals, e.g. 1,2,4. Integers denote colors from the chosen color scheme and are 1-indexed. Length must be matching that of interval bounds.')}
+ hovered
+ />
+ <TextControl
+ value={formValues.interval_color_indices ?? DEFAULT_FORM_DATA.intervalColorIndices}
+ onChange={handleChange('interval_color_indices')}
+ placeholder="1,2,4"
+ controlId="interval_color_indices"
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart/Viz Type Picker */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Gauge Chart')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t('Visualize metrics as gauge charts showing progress towards a goal')}
+ </div>
+ </div>
+
+ {/* Tabs for Data and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(GaugeControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[GaugeControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ groupby: {
+ default: [],
+ label: t('Group by'),
+ },
+ metric: {
+ default: null,
+ label: t('Metric'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ row_limit: {
+ default: DEFAULT_FORM_DATA.rowLimit,
+ label: t('Row limit'),
+ },
+ sort_by_metric: {
+ default: true,
+ label: t('Sort by metric'),
+ },
+ min_val: {
+ default: DEFAULT_FORM_DATA.minVal,
+ label: t('Min'),
+ renderTrigger: true,
+ },
+ max_val: {
+ default: DEFAULT_FORM_DATA.maxVal,
+ label: t('Max'),
+ renderTrigger: true,
+ },
+ start_angle: {
+ default: DEFAULT_FORM_DATA.startAngle,
+ label: t('Start angle'),
+ renderTrigger: true,
+ },
+ end_angle: {
+ default: DEFAULT_FORM_DATA.endAngle,
+ label: t('End angle'),
+ renderTrigger: true,
+ },
+ color_scheme: {
+ default: 'supersetColors',
+ label: t('Color scheme'),
+ renderTrigger: true,
+ },
+ font_size: {
+ default: DEFAULT_FORM_DATA.fontSize,
+ label: t('Font size'),
+ renderTrigger: true,
+ },
+ number_format: {
+ default: DEFAULT_FORM_DATA.numberFormat,
+ label: t('Number format'),
+ renderTrigger: true,
+ },
+ currency_format: {
+ default: undefined,
+ label: t('Currency format'),
+ renderTrigger: true,
+ },
+ value_formatter: {
+ default: DEFAULT_FORM_DATA.valueFormatter,
+ label: t('Value format'),
+ renderTrigger: true,
+ },
+ show_pointer: {
+ default: DEFAULT_FORM_DATA.showPointer,
+ label: t('Show pointer'),
+ renderTrigger: true,
+ },
+ animation: {
+ default: DEFAULT_FORM_DATA.animation,
+ label: t('Animation'),
+ renderTrigger: true,
+ },
+ show_axis_tick: {
+ default: DEFAULT_FORM_DATA.showAxisTick,
+ label: t('Show axis line ticks'),
+ renderTrigger: true,
+ },
+ show_split_line: {
+ default: DEFAULT_FORM_DATA.showSplitLine,
+ label: t('Show split lines'),
+ renderTrigger: true,
+ },
+ split_number: {
+ default: DEFAULT_FORM_DATA.splitNumber,
+ label: t('Split number'),
+ renderTrigger: true,
+ },
+ show_progress: {
+ default: DEFAULT_FORM_DATA.showProgress,
+ label: t('Show progress'),
+ renderTrigger: true,
+ },
+ overlap: {
+ default: DEFAULT_FORM_DATA.overlap,
+ label: t('Overlap'),
+ renderTrigger: true,
+ },
+ round_cap: {
+ default: DEFAULT_FORM_DATA.roundCap,
+ label: t('Round cap'),
+ renderTrigger: true,
+ },
+ intervals: {
+ default: DEFAULT_FORM_DATA.intervals,
+ label: t('Interval bounds'),
+ renderTrigger: true,
+ },
+ interval_color_indices: {
+ default: DEFAULT_FORM_DATA.intervalColorIndices,
+ label: t('Interval colors'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts
index a3359826b3..6b1dd0aa52 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import { t, Behavior } from '@superset-ui/core';
-import controlPanel from './controlPanel';
+import controlPanel from './GaugeControlPanelSimple';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import example1 from './images/example1.jpg';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/GraphControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/GraphControlPanelSimple.tsx
new file mode 100644
index 0000000000..cbcaa64634
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/GraphControlPanelSimple.tsx
@@ -0,0 +1,681 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+} from '@superset-ui/chart-controls';
+import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../src/explore/components/controls/TextControl';
+import SelectControl from '../../../../src/explore/components/controls/SelectControl';
+import RadioButtonControl from '../../../../packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl';
+import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
+import SliderControl from '../../../../src/explore/components/controls/SliderControl';
+import ControlHeader from '../../../../src/explore/components/ControlHeader';
+import Control from '../../../../src/explore/components/Control';
+import { DEFAULT_FORM_DATA } from './types';
+
+interface GraphControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for Graph charts.
+ */
+export const GraphControlPanel: FC<GraphControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Helper for single column selection (source/target expect single values)
+ const handleSingleColumnChange = (field: string) => (val: any) => {
+ const singleValue = Array.isArray(val) ? val[0] : val;
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, singleValue);
+ } else if (onChange) {
+ onChange(field, singleValue);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Check if force layout is selected
+ const isForceLayout = formValues.layout === 'force' || (!formValues.layout && DEFAULT_FORM_DATA.layout === 'force');
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* Source */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Source')}
+ description={t('Name of the source nodes')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.source ? [formValues.source] : []}
+ onChange={handleSingleColumnChange('source')}
+ options={safeColumns}
+ name="source"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Select source column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available. Please select a dataset first.')}
+ </div>
+ )}
+ </div>
+
+ {/* Target */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Target')}
+ description={t('Name of the target nodes')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.target ? [formValues.target] : []}
+ onChange={handleSingleColumnChange('target')}
+ options={safeColumns}
+ name="target"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Select target column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for target.')}
+ </div>
+ )}
+ </div>
+
+ {/* Metric */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metric')}
+ description={t('Metric to display')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.metric}
+ onChange={handleChange('metric')}
+ datasource={safeDataSource}
+ name="metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Source Category */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Source category')}
+ description={t('The category of source nodes used to assign colors. If a node is associated with more than one category, only the first will be used.')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.source_category ? [formValues.source_category] : []}
+ onChange={handleSingleColumnChange('source_category')}
+ options={safeColumns}
+ name="source_category"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Select source category')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for source category.')}
+ </div>
+ )}
+ </div>
+
+ {/* Target Category */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Target category')}
+ description={t('Category of target nodes')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.target_category ? [formValues.target_category] : []}
+ onChange={handleSingleColumnChange('target_category')}
+ options={safeColumns}
+ name="target_category"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Select target category')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for target category.')}
+ </div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+
+ {/* Row limit */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limit the number of rows that are returned')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="1000"
+ controlId="row_limit"
+ />
+ </div>
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Color Scheme */}
+ <div style={{ marginBottom: 16 }}>
+ {(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="color_scheme"
+ value={formValues.color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ {/* Show Legend */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Legend')}
+ description={t('Whether to display a legend for the chart')}
+ value={formValues.show_legend ?? true}
+ onChange={handleChange('show_legend')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Layout Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Layout')}</h4>
+
+ {/* Graph Layout */}
+ <div style={{ marginBottom: 16 }}>
+ <RadioButtonControl
+ label={t('Graph layout')}
+ description={t('Layout type of graph')}
+ value={formValues.layout || DEFAULT_FORM_DATA.layout}
+ onChange={handleChange('layout')}
+ options={[
+ ['force', t('Force')],
+ ['circular', t('Circular')],
+ ]}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Edge Symbols */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Edge symbols')}
+ description={t('Symbol of two ends of edge line')}
+ value={formValues.edgeSymbol || DEFAULT_FORM_DATA.edgeSymbol}
+ onChange={handleChange('edgeSymbol')}
+ choices={[
+ ['none,none', t('None -> None')],
+ ['none,arrow', t('None -> Arrow')],
+ ['circle,arrow', t('Circle -> Arrow')],
+ ['circle,circle', t('Circle -> Circle')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Enable node dragging - Only for force layout */}
+ {isForceLayout && (
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Enable node dragging')}
+ description={t('Whether to enable node dragging in force layout mode.')}
+ value={formValues.draggable ?? DEFAULT_FORM_DATA.draggable}
+ onChange={handleChange('draggable')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ {/* Enable graph roaming */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Enable graph roaming')}
+ description={t('Whether to enable changing graph position and scaling.')}
+ value={formValues.roam ?? DEFAULT_FORM_DATA.roam}
+ onChange={handleChange('roam')}
+ choices={[
+ [false, t('Disabled')],
+ ['scale', t('Scale only')],
+ ['move', t('Move only')],
+ [true, t('Scale and Move')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Node select mode */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Node select mode')}
+ description={t('Allow node selections')}
+ value={formValues.selectedMode ?? DEFAULT_FORM_DATA.selectedMode}
+ onChange={handleChange('selectedMode')}
+ choices={[
+ [false, t('Disabled')],
+ ['single', t('Single')],
+ ['multiple', t('Multiple')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Label threshold */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Label threshold')}
+ description={t('Minimum value for label to be displayed on graph.')}
+ hovered
+ />
+ <TextControl
+ value={formValues.showSymbolThreshold ?? DEFAULT_FORM_DATA.showSymbolThreshold}
+ onChange={handleChange('showSymbolThreshold')}
+ isInt
+ placeholder="0"
+ controlId="showSymbolThreshold"
+ />
+ </div>
+
+ {/* Node and Edge Sizes */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Node size')}
+ description={t('Median node size, the largest node will be 4 times larger than the smallest')}
+ hovered
+ />
+ <TextControl
+ value={formValues.baseNodeSize ?? DEFAULT_FORM_DATA.baseNodeSize}
+ onChange={handleChange('baseNodeSize')}
+ isFloat
+ placeholder="20"
+ controlId="baseNodeSize"
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Edge width')}
+ description={t('Median edge width, the thickest edge will be 4 times thicker than the thinnest.')}
+ hovered
+ />
+ <TextControl
+ value={formValues.baseEdgeWidth ?? DEFAULT_FORM_DATA.baseEdgeWidth}
+ onChange={handleChange('baseEdgeWidth')}
+ isFloat
+ placeholder="3"
+ controlId="baseEdgeWidth"
+ />
+ </div>
+ </div>
+
+ {/* Force Layout Options - Only show when force layout is selected */}
+ {isForceLayout && (
+ <>
+ {/* Edge length */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Edge length')}
+ description={t('Edge length between nodes')}
+ renderTrigger
+ hovered
+ />
+ <SliderControl
+ value={formValues.edgeLength ?? DEFAULT_FORM_DATA.edgeLength}
+ onChange={handleChange('edgeLength')}
+ {...{ min: 100, max: 1000, step: 50 }}
+ />
+ </div>
+
+ {/* Gravity */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Gravity')}
+ description={t('Strength to pull the graph toward center')}
+ renderTrigger
+ hovered
+ />
+ <SliderControl
+ value={formValues.gravity ?? DEFAULT_FORM_DATA.gravity}
+ onChange={handleChange('gravity')}
+ {...{ min: 0.1, max: 1, step: 0.1 }}
+ />
+ </div>
+
+ {/* Repulsion */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Repulsion')}
+ description={t('Repulsion strength between nodes')}
+ renderTrigger
+ hovered
+ />
+ <SliderControl
+ value={formValues.repulsion ?? DEFAULT_FORM_DATA.repulsion}
+ onChange={handleChange('repulsion')}
+ {...{ min: 100, max: 3000, step: 50 }}
+ />
+ </div>
+
+ {/* Friction */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Friction')}
+ description={t('Friction between nodes')}
+ renderTrigger
+ hovered
+ />
+ <SliderControl
+ value={formValues.friction ?? DEFAULT_FORM_DATA.friction}
+ onChange={handleChange('friction')}
+ {...{ min: 0.1, max: 1, step: 0.1 }}
+ />
+ </div>
+ </>
+ )}
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart/Viz Type Picker */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Graph Chart')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t('Visualize connections between entities in a graph structure')}
+ </div>
+ </div>
+
+ {/* Tabs for Data and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(GraphControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[GraphControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ source: {
+ default: '',
+ label: t('Source'),
+ },
+ target: {
+ default: '',
+ label: t('Target'),
+ },
+ metric: {
+ default: null,
+ label: t('Metric'),
+ },
+ source_category: {
+ default: null,
+ label: t('Source category'),
+ },
+ target_category: {
+ default: null,
+ label: t('Target category'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ row_limit: {
+ default: 1000,
+ label: t('Row limit'),
+ },
+ color_scheme: {
+ default: 'supersetColors',
+ label: t('Color scheme'),
+ renderTrigger: true,
+ },
+ show_legend: {
+ default: true,
+ label: t('Show legend'),
+ renderTrigger: true,
+ },
+ layout: {
+ default: DEFAULT_FORM_DATA.layout,
+ label: t('Graph layout'),
+ renderTrigger: true,
+ },
+ edgeSymbol: {
+ default: DEFAULT_FORM_DATA.edgeSymbol,
+ label: t('Edge symbols'),
+ renderTrigger: true,
+ },
+ draggable: {
+ default: DEFAULT_FORM_DATA.draggable,
+ label: t('Enable node dragging'),
+ renderTrigger: true,
+ },
+ roam: {
+ default: DEFAULT_FORM_DATA.roam,
+ label: t('Enable graph roaming'),
+ renderTrigger: true,
+ },
+ selectedMode: {
+ default: DEFAULT_FORM_DATA.selectedMode,
+ label: t('Node select mode'),
+ renderTrigger: true,
+ },
+ showSymbolThreshold: {
+ default: DEFAULT_FORM_DATA.showSymbolThreshold,
+ label: t('Label threshold'),
+ renderTrigger: true,
+ },
+ baseNodeSize: {
+ default: DEFAULT_FORM_DATA.baseNodeSize,
+ label: t('Node size'),
+ renderTrigger: true,
+ },
+ baseEdgeWidth: {
+ default: DEFAULT_FORM_DATA.baseEdgeWidth,
+ label: t('Edge width'),
+ renderTrigger: true,
+ },
+ edgeLength: {
+ default: DEFAULT_FORM_DATA.edgeLength,
+ label: t('Edge length'),
+ renderTrigger: true,
+ },
+ gravity: {
+ default: DEFAULT_FORM_DATA.gravity,
+ label: t('Gravity'),
+ renderTrigger: true,
+ },
+ repulsion: {
+ default: DEFAULT_FORM_DATA.repulsion,
+ label: t('Repulsion'),
+ renderTrigger: true,
+ },
+ friction: {
+ default: DEFAULT_FORM_DATA.friction,
+ label: t('Friction'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts
index 347e61d1ef..230301121c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import { Behavior, t } from '@superset-ui/core';
-import controlPanel from './controlPanel';
+import controlPanel from './GraphControlPanelSimple';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import example from './images/example.jpg';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/HeatmapControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/HeatmapControlPanelSimple.tsx
new file mode 100644
index 0000000000..28a98fe035
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/HeatmapControlPanelSimple.tsx
@@ -0,0 +1,754 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ LinearColorSchemeControl,
+ formatSelectOptionsForRange,
+ D3_TIME_FORMAT_OPTIONS,
+} from '@superset-ui/chart-controls';
+import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../src/explore/components/controls/TextControl';
+import SelectControl from '../../../../src/explore/components/controls/SelectControl';
+import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
+import SliderControl from '../../../../src/explore/components/controls/SliderControl';
+import ColorPickerControl from '../../../../src/explore/components/controls/ColorPickerControl';
+import CurrencyControl from '../../../../src/explore/components/controls/CurrencyControl';
+import BoundsControl from '../../../../src/explore/components/controls/BoundsControl';
+import ControlHeader from '../../../../src/explore/components/ControlHeader';
+import Control from '../../../../src/explore/components/Control';
+
+interface HeatmapControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for Heatmap charts.
+ */
+export const HeatmapControlPanel: FC<HeatmapControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Helper for single column selection
+ const handleSingleColumnChange = (field: string) => (val: any) => {
+ const singleValue = Array.isArray(val) ? val[0] : val;
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, singleValue);
+ } else if (onChange) {
+ onChange(field, singleValue);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Sort axis choices
+ const sortAxisChoices = [
+ ['alpha_asc', t('Axis ascending')],
+ ['alpha_desc', t('Axis descending')],
+ ['value_asc', t('Metric ascending')],
+ ['value_desc', t('Metric descending')],
+ ];
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* X-Axis */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('X-Axis')}
+ description={t('Dimension to use on x-axis.')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.x_axis ? [formValues.x_axis] : []}
+ onChange={handleSingleColumnChange('x_axis')}
+ options={safeColumns}
+ name="x_axis"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Select X-axis column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available. Please select a dataset first.')}
+ </div>
+ )}
+ </div>
+
+ {/* Time Grain */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Time Grain')}
+ description={t('Time granularity for temporal grouping')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.time_grain_sqla || null}
+ onChange={handleChange('time_grain_sqla')}
+ choices={[
+ ['PT1S', t('Second')],
+ ['PT1M', t('Minute')],
+ ['PT5M', t('5 minute')],
+ ['PT10M', t('10 minute')],
+ ['PT15M', t('15 minute')],
+ ['PT30M', t('30 minute')],
+ ['PT1H', t('Hour')],
+ ['P1D', t('Day')],
+ ['P1W', t('Week')],
+ ['P1M', t('Month')],
+ ['P3M', t('Quarter')],
+ ['P1Y', t('Year')],
+ ]}
+ clearable
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Y-Axis */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Y-Axis')}
+ description={t('Dimension to use on y-axis.')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.groupby ? [formValues.groupby] : []}
+ onChange={handleSingleColumnChange('groupby')}
+ options={safeColumns}
+ name="groupby"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Select Y-axis column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for Y-axis.')}
+ </div>
+ )}
+ </div>
+
+ {/* Metric */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metric')}
+ description={t('Metric to display')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.metric}
+ onChange={handleChange('metric')}
+ datasource={safeDataSource}
+ name="metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+
+ {/* Row limit */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limit the number of rows that are returned')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="1000"
+ controlId="row_limit"
+ />
+ </div>
+
+ {/* Sort Axes */}
+ <div style={{ display: 'flex', gap: 16 }}>
+ <div style={{ flex: 1 }}>
+ <SelectControl
+ label={t('Sort X Axis')}
+ value={formValues.sort_x_axis || null}
+ onChange={handleChange('sort_x_axis')}
+ choices={sortAxisChoices}
+ clearable
+ renderTrigger={false}
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <SelectControl
+ label={t('Sort Y Axis')}
+ value={formValues.sort_y_axis || null}
+ onChange={handleChange('sort_y_axis')}
+ choices={sortAxisChoices}
+ clearable
+ renderTrigger={false}
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Normalize Across */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Normalize Across')}
+ description={
+ <div>
+ <div>
+ {t(
+ 'Color will be shaded based the normalized (0% to 100%) value of a given cell against the other cells in the selected range: ',
+ )}
+ </div>
+ <ul>
+ <li>{t('x: values are normalized within each column')}</li>
+ <li>{t('y: values are normalized within each row')}</li>
+ <li>
+ {t('heatmap: values are normalized across the entire heatmap')}
+ </li>
+ </ul>
+ </div>
+ }
+ value={formValues.normalize_across || 'heatmap'}
+ onChange={handleChange('normalize_across')}
+ choices={[
+ ['heatmap', t('heatmap')],
+ ['x', t('x')],
+ ['y', t('y')],
+ ]}
+ clearable={false}
+ renderTrigger={false}
+ hovered
+ />
+ </div>
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Legend Type */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Legend Type')}
+ value={formValues.legend_type || 'continuous'}
+ onChange={handleChange('legend_type')}
+ choices={[
+ ['continuous', t('Continuous')],
+ ['piecewise', t('Piecewise')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Linear Color Scheme */}
+ <div style={{ marginBottom: 16 }}>
+ {(() => {
+ const linearColorSchemeControl = LinearColorSchemeControl();
+ const { hidden, ...cleanConfig } = linearColorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="linear_color_scheme"
+ value={formValues.linear_color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('linear_color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ {/* Border Color and Width */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Border color')}
+ description={t('The color of the elements border')}
+ hovered
+ />
+ <ColorPickerControl
+ value={formValues.border_color || { r: 0, g: 0, b: 0, a: 1 }}
+ onChange={handleChange('border_color')}
+ renderTrigger
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Border width')}
+ description={t('The width of the elements border')}
+ renderTrigger
+ hovered
+ />
+ <SliderControl
+ value={formValues.border_width || 0}
+ onChange={handleChange('border_width')}
+ {...{ min: 0, max: 2, step: 0.1 }}
+ />
+ </div>
+ </div>
+
+ {/* Scale Intervals */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <SelectControl
+ label={t('XScale Interval')}
+ description={t('Number of steps to take between ticks when displaying the X scale')}
+ value={formValues.xscale_interval ?? -1}
+ onChange={handleChange('xscale_interval')}
+ choices={[[-1, t('Auto')]].concat(formatSelectOptionsForRange(1, 50))}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <SelectControl
+ label={t('YScale Interval')}
+ description={t('Number of steps to take between ticks when displaying the Y scale')}
+ value={formValues.yscale_interval ?? -1}
+ onChange={handleChange('yscale_interval')}
+ choices={[[-1, t('Auto')]].concat(formatSelectOptionsForRange(1, 50))}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Margins */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <SelectControl
+ label={t('Left Margin')}
+ description={t('Left margin, in pixels, allowing for more room for axis labels')}
+ value={formValues.left_margin || 'auto'}
+ onChange={handleChange('left_margin')}
+ choices={[
+ ['auto', t('Auto')],
+ [50, '50'],
+ [75, '75'],
+ [100, '100'],
+ [125, '125'],
+ [150, '150'],
+ [200, '200'],
+ ]}
+ freeForm
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <SelectControl
+ label={t('Bottom Margin')}
+ description={t('Bottom margin, in pixels, allowing for more room for axis labels')}
+ value={formValues.bottom_margin || 'auto'}
+ onChange={handleChange('bottom_margin')}
+ choices={[
+ ['auto', t('Auto')],
+ [50, '50'],
+ [75, '75'],
+ [100, '100'],
+ [125, '125'],
+ [150, '150'],
+ [200, '200'],
+ ]}
+ freeForm
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Value bounds */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Value bounds')}
+ description={t('Hard value bounds applied for color coding.')}
+ renderTrigger
+ hovered
+ />
+ <BoundsControl
+ value={formValues.value_bounds || [null, null]}
+ onChange={handleChange('value_bounds')}
+ />
+ </div>
+
+ {/* Format Controls */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Value Format')}
+ value={formValues.y_axis_format || null}
+ onChange={handleChange('y_axis_format')}
+ choices={[
+ ['.1f', '.1f (12345.432 => 12345.4)'],
+ ['.2f', '.2f (12345.432 => 12345.43)'],
+ ['.3f', '.3f (12345.432 => 12345.432)'],
+ ['.1%', '.1% (0.432 => 43.2%)'],
+ ['.2%', '.2% (0.432 => 43.20%)'],
+ ['.3%', '.3% (0.432 => 43.200%)'],
+ [',.1f', ',.1f (12345.432 => 12,345.4)'],
+ [',.2f', ',.2f (12345.432 => 12,345.43)'],
+ [',.3f', ',.3f (12345.432 => 12,345.432)'],
+ ['.1s', '.1s (12345.432 => 10k)'],
+ ['.2s', '.2s (12345.432 => 12k)'],
+ ['.3s', '.3s (12345.432 => 12.3k)'],
+ ]}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Date Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('X-axis time format')}
+ value={formValues.x_axis_time_format || null}
+ onChange={handleChange('x_axis_time_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Currency Format */}
+ <div style={{ marginBottom: 16 }}>
+ <CurrencyControl
+ value={formValues.currency_format}
+ onChange={handleChange('currency_format')}
+ />
+ </div>
+
+ {/* Display Options */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Legend')}
+ description={t('Whether to display the legend (toggles)')}
+ value={formValues.show_legend ?? true}
+ onChange={handleChange('show_legend')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Show percentage')}
+ description={t('Whether to include the percentage in the tooltip')}
+ value={formValues.show_percentage ?? true}
+ onChange={handleChange('show_percentage')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Show Values')}
+ description={t('Whether to display the numerical values within the cells')}
+ value={formValues.show_values ?? false}
+ onChange={handleChange('show_values')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Normalized')}
+ description={t('Whether to apply a normal distribution based on rank on the color scale')}
+ value={formValues.normalized ?? false}
+ onChange={handleChange('normalized')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart/Viz Type Picker */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Heatmap')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t('Visualize correlations and patterns across two dimensions with color intensity')}
+ </div>
+ </div>
+
+ {/* Tabs for Data and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(HeatmapControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[HeatmapControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ x_axis: {
+ default: null,
+ label: t('X-Axis'),
+ },
+ time_grain_sqla: {
+ default: null,
+ label: t('Time Grain'),
+ },
+ groupby: {
+ default: null,
+ label: t('Y-Axis'),
+ multi: false,
+ },
+ metric: {
+ default: null,
+ label: t('Metric'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ row_limit: {
+ default: 1000,
+ label: t('Row limit'),
+ },
+ sort_x_axis: {
+ default: null,
+ label: t('Sort X Axis'),
+ },
+ sort_y_axis: {
+ default: null,
+ label: t('Sort Y Axis'),
+ },
+ normalize_across: {
+ default: 'heatmap',
+ label: t('Normalize Across'),
+ },
+ legend_type: {
+ default: 'continuous',
+ label: t('Legend Type'),
+ renderTrigger: true,
+ },
+ linear_color_scheme: {
+ default: 'blue_white_yellow',
+ label: t('Linear Color Scheme'),
+ renderTrigger: true,
+ },
+ border_color: {
+ default: { r: 0, g: 0, b: 0, a: 1 },
+ label: t('Border color'),
+ renderTrigger: true,
+ },
+ border_width: {
+ default: 0,
+ label: t('Border width'),
+ renderTrigger: true,
+ },
+ xscale_interval: {
+ default: -1,
+ label: t('XScale Interval'),
+ renderTrigger: true,
+ },
+ yscale_interval: {
+ default: -1,
+ label: t('YScale Interval'),
+ renderTrigger: true,
+ },
+ left_margin: {
+ default: 'auto',
+ label: t('Left Margin'),
+ renderTrigger: true,
+ },
+ bottom_margin: {
+ default: 'auto',
+ label: t('Bottom Margin'),
+ renderTrigger: true,
+ },
+ value_bounds: {
+ default: [null, null],
+ label: t('Value bounds'),
+ renderTrigger: true,
+ },
+ y_axis_format: {
+ default: null,
+ label: t('Value Format'),
+ renderTrigger: true,
+ },
+ x_axis_time_format: {
+ default: null,
+ label: t('X-axis time format'),
+ renderTrigger: true,
+ },
+ currency_format: {
+ default: null,
+ label: t('Currency format'),
+ renderTrigger: true,
+ },
+ show_legend: {
+ default: true,
+ label: t('Legend'),
+ renderTrigger: true,
+ },
+ show_percentage: {
+ default: true,
+ label: t('Show percentage'),
+ renderTrigger: true,
+ },
+ show_values: {
+ default: false,
+ label: t('Show Values'),
+ renderTrigger: true,
+ },
+ normalized: {
+ default: false,
+ label: t('Normalized'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/index.ts
index 23ae090f3e..dc2ef7f2b0 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/index.ts
@@ -23,7 +23,7 @@ import example1 from './images/example1.png';
import example2 from './images/example2.png';
import example3 from './images/example3.png';
import thumbnail from './images/thumbnail.png';
-import controlPanel from './controlPanel';
+import controlPanel from './HeatmapControlPanelSimple';
const metadata = new ChartMetadata({
category: t('Correlation'),
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/HistogramControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/HistogramControlPanelSimple.tsx
new file mode 100644
index 0000000000..5e7727acfc
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/HistogramControlPanelSimple.tsx
@@ -0,0 +1,493 @@
+/**
+ * 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 { FC, useState } from 'react';
+import {
+ t,
+ GenericDataType,
+ validateInteger,
+ validateNonEmpty,
+} from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+ formatSelectOptionsForRange,
+ columnsByType,
+ D3_FORMAT_OPTIONS,
+ D3_FORMAT_DOCS,
+ D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
+} from '@superset-ui/chart-controls';
+import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
+import SelectControl from '../../../../src/explore/components/controls/SelectControl';
+import ControlHeader from '../../../../src/explore/components/ControlHeader';
+import Control from '../../../../src/explore/components/Control';
+
+interface HistogramControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for Histogram charts.
+ */
+export const HistogramControlPanel: FC<HistogramControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Get numeric columns for the histogram column selection
+ const numericColumns = columnsByType(datasource, GenericDataType.Numeric);
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* Column (Primary metric column) */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Column')}
+ description={t('Numeric column used to calculate the histogram.')}
+ hovered
+ />
+ {numericColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.column ? [formValues.column] : []}
+ onChange={(val: any) => {
+ const singleValue = Array.isArray(val) ? val[0] : val;
+ handleChange('column')(singleValue);
+ }}
+ options={numericColumns}
+ name="column"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Select a numeric column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t(
+ 'No numeric columns available. Please select a dataset with numeric data.',
+ )}
+ </div>
+ )}
+ </div>
+
+ {/* Group by */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Group by')}
+ description={t('Columns to group by')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.groupby || []}
+ onChange={handleChange('groupby')}
+ options={safeColumns}
+ name="groupby"
+ label=""
+ multi
+ canDelete
+ ghostButtonText={t('Add dimension')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available. Please select a dataset first.')}
+ </div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={[]}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+
+ {/* Row limit and Bins */}
+ <div style={{ display: 'flex', gap: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limit the number of rows that are returned')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="100"
+ controlId="row_limit"
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Bins')}
+ description={t('The number of bins for the histogram')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.bins || 5}
+ onChange={handleChange('bins')}
+ choices={formatSelectOptionsForRange(5, 20, 5)}
+ freeForm
+ clearable={false}
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Normalize and Cumulative */}
+ <div style={{ display: 'flex', gap: 16, marginTop: 16 }}>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Normalize')}
+ description={t(`
+ The normalize option transforms the histogram values into proportions or
+ probabilities by dividing each bin's count by the total count of data points.
+ This normalization process ensures that the resulting values sum up to 1,
+ enabling a relative comparison of the data's distribution and providing a
+ clearer understanding of the proportion of data points within each bin.`)}
+ value={formValues.normalize ?? false}
+ onChange={handleChange('normalize')}
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Cumulative')}
+ description={t(`
+ The cumulative option allows you to see how your data accumulates over different
+ values. When enabled, the histogram bars represent the running total of frequencies
+ up to each bin. This helps you understand how likely it is to encounter values
+ below a certain point. Keep in mind that enabling cumulative doesn't change your
+ original data, it just changes the way the histogram is displayed.`)}
+ value={formValues.cumulative ?? false}
+ onChange={handleChange('cumulative')}
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Color Scheme */}
+ <div style={{ marginBottom: 16 }}>
+ {(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="color_scheme"
+ value={formValues.color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ {/* Show Value */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Value')}
+ description={t('Show series values on the chart')}
+ value={formValues.show_value ?? false}
+ onChange={handleChange('show_value')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Show Legend */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show legend')}
+ description={t('Whether to display a legend for the chart')}
+ value={formValues.show_legend ?? true}
+ onChange={handleChange('show_legend')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Axis Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Axis')}</h4>
+
+ {/* X Axis Title and Format */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader label={t('X Axis Title')} hovered />
+ <TextControl
+ value={formValues.x_axis_title || ''}
+ onChange={handleChange('x_axis_title')}
+ placeholder={t('X Axis Title')}
+ controlId="x_axis_title"
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('X Axis Format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ hovered
+ />
+ <SelectControl
+ value={formValues.x_axis_format || 'SMART_NUMBER'}
+ onChange={handleChange('x_axis_format')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Y Axis Title and Format */}
+ <div style={{ display: 'flex', gap: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader label={t('Y Axis Title')} hovered />
+ <TextControl
+ value={formValues.y_axis_title || ''}
+ onChange={handleChange('y_axis_title')}
+ placeholder={t('Y Axis Title')}
+ controlId="y_axis_title"
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Y Axis Format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ hovered
+ />
+ <SelectControl
+ value={formValues.y_axis_format || 'SMART_NUMBER'}
+ onChange={handleChange('y_axis_format')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart Description */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Histogram')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t(
+ 'Display the distribution of a dataset by representing the frequency or count of values within different ranges or bins',
+ )}
+ </div>
+ </div>
+
+ {/* Tabs for Data and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(HistogramControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[HistogramControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ column: {
+ default: null,
+ label: t('Column'),
+ validators: [validateNonEmpty],
+ },
+ groupby: {
+ default: [],
+ label: t('Group by'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ row_limit: {
+ default: 100,
+ label: t('Row limit'),
+ },
+ bins: {
+ default: 5,
+ label: t('Bins'),
+ validators: [validateInteger],
+ },
+ normalize: {
+ default: false,
+ label: t('Normalize'),
+ },
+ cumulative: {
+ default: false,
+ label: t('Cumulative'),
+ },
+ color_scheme: {
+ default: 'supersetColors',
+ label: t('Color scheme'),
+ renderTrigger: true,
+ },
+ show_value: {
+ default: false,
+ label: t('Show Value'),
+ renderTrigger: true,
+ },
+ show_legend: {
+ default: true,
+ label: t('Show legend'),
+ renderTrigger: true,
+ },
+ x_axis_title: {
+ default: '',
+ label: t('X Axis Title'),
+ renderTrigger: true,
+ },
+ x_axis_format: {
+ default: 'SMART_NUMBER',
+ label: t('X Axis Format'),
+ renderTrigger: true,
+ },
+ y_axis_title: {
+ default: '',
+ label: t('Y Axis Title'),
+ renderTrigger: true,
+ },
+ y_axis_format: {
+ default: 'SMART_NUMBER',
+ label: t('Y Axis Format'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/index.ts
index 15e6e84ddd..f2b4dd2a12 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/index.ts
@@ -19,7 +19,7 @@
*/
import { ChartMetadata, ChartPlugin, t } from '@superset-ui/core';
import buildQuery from './buildQuery';
-import controlPanel from './controlPanel';
+import controlPanel from './HistogramControlPanelSimple';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import example1 from './images/example1.png';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/MixedTimeseriesControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/MixedTimeseriesControlPanelSimple.tsx
new file mode 100644
index 0000000000..c9e0bdc460
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/MixedTimeseriesControlPanelSimple.tsx
@@ -0,0 +1,1111 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+ D3_FORMAT_OPTIONS,
+ D3_TIME_FORMAT_OPTIONS,
+ D3_FORMAT_DOCS,
+ D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
+ SORT_SERIES_CHOICES,
+ DEFAULT_SORT_SERIES_DATA,
+ CURRENCY_FORMAT_OPTIONS,
+ CurrencyFormatControl,
+} from '@superset-ui/chart-controls';
+import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
+import SliderControl from '../../../../src/explore/components/controls/SliderControl';
+import SelectControl from '../../../../src/explore/components/controls/SelectControl';
+import CurrencyControl from '../../../../src/explore/components/controls/CurrencyControl';
+import ControlHeader from '../../../../src/explore/components/ControlHeader';
+import Control from '../../../../src/explore/components/Control';
+import BoundsControl from '../../../../src/explore/components/controls/BoundsControl';
+import { EchartsTimeseriesSeriesType } from '../Timeseries/types';
+
+interface MixedTimeseriesControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for Mixed Timeseries charts.
+ * Supports dual Y-axes with independent formatting and chart types.
+ */
+export const MixedTimeseriesControlPanel: FC<MixedTimeseriesControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Series type choices
+ const seriesTypeChoices = [
+ [EchartsTimeseriesSeriesType.Line, t('Line')],
+ [EchartsTimeseriesSeriesType.Scatter, t('Scatter')],
+ [EchartsTimeseriesSeriesType.Smooth, t('Smooth Line')],
+ [EchartsTimeseriesSeriesType.Bar, t('Bar')],
+ [EchartsTimeseriesSeriesType.Start, t('Step - start')],
+ [EchartsTimeseriesSeriesType.Middle, t('Step - middle')],
+ [EchartsTimeseriesSeriesType.End, t('Step - end')],
+ ];
+
+ // Render query section for Query A or B
+ const renderQuerySection = (label: string, suffix: string) => (
+ <div style={{ marginBottom: 24 }}>
+ <h4>{label}</h4>
+
+ {/* Metrics */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metrics')}
+ description={t('Metrics to display')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues[`metrics${suffix}`] || []}
+ onChange={handleChange(`metrics${suffix}`)}
+ datasource={safeDataSource}
+ name={`metrics${suffix}`}
+ label=""
+ multi
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Group by */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Group by')}
+ description={t('Columns to group by')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues[`groupby${suffix}`] || []}
+ onChange={handleChange(`groupby${suffix}`)}
+ options={safeColumns}
+ name={`groupby${suffix}`}
+ label=""
+ multi
+ canDelete
+ ghostButtonText={t('Add dimension')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available. Please select a dataset first.')}
+ </div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues[`adhoc_filters${suffix}`] || []}
+ onChange={handleChange(`adhoc_filters${suffix}`)}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name={`adhoc_filters${suffix}`}
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues[`metrics${suffix}`] || []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+
+ {/* Row limits and sort controls */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Series limit')}
+ description={t('Limits the number of time series that get displayed')}
+ hovered
+ />
+ <TextControl
+ value={formValues[`limit${suffix}`]}
+ onChange={handleChange(`limit${suffix}`)}
+ isInt
+ placeholder="0"
+ controlId={`limit${suffix}`}
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Sort by metric')}
+ description={t('Metric to sort the results by')}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues[`timeseries_limit_metric${suffix}`]}
+ onChange={handleChange(`timeseries_limit_metric${suffix}`)}
+ datasource={safeDataSource}
+ name={`timeseries_limit_metric${suffix}`}
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+ </div>
+
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Sort Descending')}
+ description={t('Whether to sort descending or ascending')}
+ value={formValues[`order_desc${suffix}`] ?? false}
+ onChange={handleChange(`order_desc${suffix}`)}
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limit the number of rows that are returned')}
+ hovered
+ />
+ <TextControl
+ value={formValues[`row_limit${suffix}`]}
+ onChange={handleChange(`row_limit${suffix}`)}
+ isInt
+ placeholder="10000"
+ controlId={`row_limit${suffix}`}
+ />
+ </div>
+ </div>
+
+ {/* Truncate metric */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Truncate Metric')}
+ description={t('Whether to truncate metric')}
+ value={formValues[`truncate_metric${suffix}`] ?? true}
+ onChange={handleChange(`truncate_metric${suffix}`)}
+ hovered
+ />
+ </div>
+ </div>
+ );
+
+ // Render customization section for Query A or B
+ const renderCustomizeSection = (label: string, suffix: string) => (
+ <div style={{ marginBottom: 24 }}>
+ <h4>{label}</h4>
+
+ {/* Series Type */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Series type')}
+ description={t('Series chart type (line, bar etc)')}
+ value={formValues[`seriesType${suffix}`] || EchartsTimeseriesSeriesType.Line}
+ onChange={handleChange(`seriesType${suffix}`)}
+ choices={seriesTypeChoices}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Y-Axis Assignment */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Y Axis')}
+ description={t('Primary or secondary y-axis')}
+ value={formValues[`yAxisIndex${suffix}`] ?? 0}
+ onChange={handleChange(`yAxisIndex${suffix}`)}
+ choices={[
+ [0, t('Primary')],
+ [1, t('Secondary')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Chart Style Options */}
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Stack series')}
+ description={t('Stack series on top of each other')}
+ value={formValues[`stack${suffix}`] ?? false}
+ onChange={handleChange(`stack${suffix}`)}
+ renderTrigger
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Area chart')}
+ description={t('Draw area under curves. Only applicable for line types.')}
+ value={formValues[`area${suffix}`] ?? false}
+ onChange={handleChange(`area${suffix}`)}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ <div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Show Values')}
+ description={t('Whether to display the numerical values within the cells')}
+ value={formValues[`show_value${suffix}`] ?? false}
+ onChange={handleChange(`show_value${suffix}`)}
+ renderTrigger
+ hovered
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Marker')}
+ description={t('Draw a marker on data points. Only applicable for line types.')}
+ value={formValues[`markerEnabled${suffix}`] ?? false}
+ onChange={handleChange(`markerEnabled${suffix}`)}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Only Total checkbox - conditional on show_value and stack */}
+ {formValues[`show_value${suffix}`] && formValues[`stack${suffix}`] && (
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Only Total')}
+ description={t('Only show the total value on the stacked chart, and not show on the selected category')}
+ value={formValues[`only_total${suffix}`] ?? true}
+ onChange={handleChange(`only_total${suffix}`)}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ {/* Sliders for opacity and marker size */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Opacity')}
+ description={t('Opacity of area chart.')}
+ renderTrigger
+ hovered
+ />
+ <SliderControl
+ value={formValues[`opacity${suffix}`] ?? 0.7}
+ onChange={handleChange(`opacity${suffix}`)}
+ {...{ min: 0, max: 1, step: 0.1 }}
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Marker size')}
+ description={t('Size of marker. Also applies to forecast observations.')}
+ renderTrigger
+ hovered
+ />
+ <SliderControl
+ value={formValues[`markerSize${suffix}`] ?? 6}
+ onChange={handleChange(`markerSize${suffix}`)}
+ {...{ min: 0, max: 100, step: 1 }}
+ />
+ </div>
+
+ {/* Series Order */}
+ <div style={{ marginBottom: 16 }}>
+ <h5>{t('Series Order')}</h5>
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Sort Series By')}
+ description={t('Based on what should series be ordered on the chart and legend')}
+ value={formValues[`sort_series_type${suffix}`] || DEFAULT_SORT_SERIES_DATA.sort_series_type}
+ onChange={handleChange(`sort_series_type${suffix}`)}
+ choices={SORT_SERIES_CHOICES}
+ renderTrigger
+ hovered
+ />
+ </div>
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Sort Series Ascending')}
+ description={t('Sort series in ascending order')}
+ value={formValues[`sort_series_ascending${suffix}`] ?? DEFAULT_SORT_SERIES_DATA.sort_series_ascending}
+ onChange={handleChange(`sort_series_ascending${suffix}`)}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* Shared query fields */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Shared query fields')}</h4>
+
+ {/* X-Axis */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('X-Axis')}
+ description={t('The column to be used as the x-axis')}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.x_axis ? [formValues.x_axis] : []}
+ onChange={(val: any) => {
+ const singleValue = Array.isArray(val) ? val[0] : val;
+ handleChange('x_axis')(singleValue);
+ }}
+ options={safeColumns}
+ name="x_axis"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Add dimension')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available. Please select a dataset first.')}
+ </div>
+ )}
+ </div>
+
+ {/* Time Grain */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Time Grain')}
+ description={t('The time granularity for the visualization')}
+ value={formValues.time_grain_sqla}
+ onChange={handleChange('time_grain_sqla')}
+ choices={[
+ [null, t('None')],
+ ['PT1S', t('Second')],
+ ['PT1M', t('Minute')],
+ ['PT5M', t('5 Minutes')],
+ ['PT10M', t('10 Minutes')],
+ ['PT15M', t('15 Minutes')],
+ ['PT30M', t('30 Minutes')],
+ ['PT1H', t('Hour')],
+ ['P1D', t('Day')],
+ ['P1W', t('Week')],
+ ['P1M', t('Month')],
+ ['P3M', t('Quarter')],
+ ['P1Y', t('Year')],
+ ]}
+ clearable
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Query A and Query B */}
+ {renderQuerySection(t('Query A'), '')}
+ {renderQuerySection(t('Query B'), '_b')}
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Color Scheme */}
+ <div style={{ marginBottom: 16 }}>
+ {(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="color_scheme"
+ value={formValues.color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ {/* Zoomable */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Data Zoom')}
+ description={t('Enable data zoom controls')}
+ value={formValues.zoomable ?? true}
+ onChange={handleChange('zoomable')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Minor Ticks */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Minor ticks')}
+ description={t('Show minor ticks on axes')}
+ value={formValues.minorTicks ?? false}
+ onChange={handleChange('minorTicks')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Query A and B Customizations */}
+ {renderCustomizeSection(t('Query A'), '')}
+ {renderCustomizeSection(t('Query B'), 'B')}
+
+ {/* X-Axis Controls */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('X Axis')}</h4>
+
+ {/* X-Axis Time Format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Time format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ value={formValues.x_axis_time_format || 'smart_date'}
+ onChange={handleChange('x_axis_time_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* X-Axis Label Rotation */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Rotate x axis label')}
+ description={t('Input field supports custom rotation')}
+ renderTrigger
+ hovered
+ />
+ <TextControl
+ value={formValues.xAxisLabelRotation ?? 0}
+ onChange={handleChange('xAxisLabelRotation')}
+ isInt
+ placeholder="0"
+ controlId="xAxisLabelRotation"
+ />
+ </div>
+ </div>
+
+ {/* Tooltip */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Tooltip')}</h4>
+
+ {/* Show query identifiers */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show query identifiers')}
+ description={t('Add Query A and Query B identifiers to tooltips to help differentiate series')}
+ value={formValues.show_query_identifiers ?? false}
+ onChange={handleChange('show_query_identifiers')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Rich tooltip */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Rich Tooltip')}
+ description={t('The rich tooltip shows a table of all series values')}
+ value={formValues.rich_tooltip ?? true}
+ onChange={handleChange('rich_tooltip')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Y-Axis Controls */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Y Axis')}</h4>
+
+ {/* Primary Y-Axis */}
+ <div style={{ marginBottom: 16 }}>
+ <h5>{t('Primary Y-Axis')}</h5>
+
+ {/* Primary Y-axis format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Primary y-axis format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ value={formValues.y_axis_format || 'SMART_NUMBER'}
+ onChange={handleChange('y_axis_format')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Currency format */}
+ <div style={{ marginBottom: 16 }}>
+ <CurrencyControl
+ value={formValues.currency_format}
+ onChange={handleChange('currency_format')}
+ />
+ </div>
+
+ {/* Logarithmic y-axis */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Logarithmic y-axis')}
+ description={t('Logarithmic scale on primary y-axis')}
+ value={formValues.logAxis ?? false}
+ onChange={handleChange('logAxis')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Primary y-axis bounds */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Primary y-axis Bounds')}
+ description={t('Bounds for the primary Y-axis. When left empty, the bounds are dynamically defined based on the min/max of the data.')}
+ hovered
+ />
+ <BoundsControl
+ value={formValues.y_axis_bounds}
+ onChange={handleChange('y_axis_bounds')}
+ renderTrigger
+ />
+ </div>
+ </div>
+
+ {/* Secondary Y-Axis */}
+ <div style={{ marginBottom: 16 }}>
+ <h5>{t('Secondary Y-Axis')}</h5>
+
+ {/* Secondary Y-axis format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Secondary y-axis format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ value={formValues.y_axis_format_secondary}
+ onChange={handleChange('y_axis_format_secondary')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Secondary currency format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Secondary currency format')}
+ description={t('The currency symbol')}
+ value={formValues.currency_format_secondary}
+ onChange={handleChange('currency_format_secondary')}
+ choices={CURRENCY_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Secondary Y-axis title */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Secondary y-axis title')}
+ description={t('Logarithmic y-axis')}
+ hovered
+ />
+ <TextControl
+ value={formValues.yAxisTitleSecondary || ''}
+ onChange={handleChange('yAxisTitleSecondary')}
+ placeholder=""
+ controlId="yAxisTitleSecondary"
+ />
+ </div>
+
+ {/* Logarithmic secondary y-axis */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Logarithmic y-axis')}
+ description={t('Logarithmic scale on secondary y-axis')}
+ value={formValues.logAxisSecondary ?? false}
+ onChange={handleChange('logAxisSecondary')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Secondary y-axis bounds */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Secondary y-axis Bounds')}
+ description={t('Bounds for the secondary Y-axis. Only works when Independent Y-axis bounds are enabled.')}
+ hovered
+ />
+ <BoundsControl
+ value={formValues.y_axis_bounds_secondary}
+ onChange={handleChange('y_axis_bounds_secondary')}
+ renderTrigger
+ />
+ </div>
+ </div>
+
+ {/* Additional Y-Axis Controls */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Minor Split Line')}
+ description={t('Draw split lines for minor y-axis ticks')}
+ value={formValues.minorSplitLine ?? false}
+ onChange={handleChange('minorSplitLine')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Truncate Y Axis')}
+ description={t('Truncate Y Axis. Can be overridden by specifying a min or max bound.')}
+ value={formValues.truncateYAxis ?? true}
+ onChange={handleChange('truncateYAxis')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart Type Picker */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Mixed Chart')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t('Visualize two different series using the same x-axis. Note that both series can be visualized with a different chart type (e.g. 1 using bars and 1 using a line).')}
+ </div>
+ </div>
+
+ {/* Tabs for Data and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(MixedTimeseriesControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[MixedTimeseriesControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ // Shared controls
+ x_axis: {
+ default: null,
+ label: t('X-Axis'),
+ },
+ time_grain_sqla: {
+ default: null,
+ label: t('Time Grain'),
+ },
+ color_scheme: {
+ default: 'supersetColors',
+ label: t('Color scheme'),
+ renderTrigger: true,
+ },
+ zoomable: {
+ default: true,
+ label: t('Data Zoom'),
+ renderTrigger: true,
+ },
+ minorTicks: {
+ default: false,
+ label: t('Minor ticks'),
+ renderTrigger: true,
+ },
+
+ // Query A controls
+ metrics: {
+ default: [],
+ label: t('Metrics'),
+ },
+ groupby: {
+ default: [],
+ label: t('Group by'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ limit: {
+ default: 0,
+ label: t('Series limit'),
+ },
+ timeseries_limit_metric: {
+ default: null,
+ label: t('Sort by metric'),
+ },
+ order_desc: {
+ default: false,
+ label: t('Sort Descending'),
+ },
+ row_limit: {
+ default: 10000,
+ label: t('Row limit'),
+ },
+ truncate_metric: {
+ default: true,
+ label: t('Truncate Metric'),
+ },
+ seriesType: {
+ default: EchartsTimeseriesSeriesType.Line,
+ label: t('Series type'),
+ renderTrigger: true,
+ },
+ yAxisIndex: {
+ default: 0,
+ label: t('Y Axis'),
+ renderTrigger: true,
+ },
+ stack: {
+ default: false,
+ label: t('Stack series'),
+ renderTrigger: true,
+ },
+ area: {
+ default: false,
+ label: t('Area chart'),
+ renderTrigger: true,
+ },
+ show_value: {
+ default: false,
+ label: t('Show Values'),
+ renderTrigger: true,
+ },
+ markerEnabled: {
+ default: false,
+ label: t('Marker'),
+ renderTrigger: true,
+ },
+ only_total: {
+ default: true,
+ label: t('Only Total'),
+ renderTrigger: true,
+ },
+ opacity: {
+ default: 0.7,
+ label: t('Opacity'),
+ renderTrigger: true,
+ },
+ markerSize: {
+ default: 6,
+ label: t('Marker size'),
+ renderTrigger: true,
+ },
+ sort_series_type: {
+ default: DEFAULT_SORT_SERIES_DATA.sort_series_type,
+ label: t('Sort Series By'),
+ renderTrigger: true,
+ },
+ sort_series_ascending: {
+ default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending,
+ label: t('Sort Series Ascending'),
+ renderTrigger: true,
+ },
+
+ // Query B controls
+ metrics_b: {
+ default: [],
+ label: t('Metrics'),
+ },
+ groupby_b: {
+ default: [],
+ label: t('Group by'),
+ },
+ adhoc_filters_b: {
+ default: [],
+ label: t('Filters'),
+ },
+ limit_b: {
+ default: 0,
+ label: t('Series limit'),
+ },
+ timeseries_limit_metric_b: {
+ default: null,
+ label: t('Sort by metric'),
+ },
+ order_desc_b: {
+ default: false,
+ label: t('Sort Descending'),
+ },
+ row_limit_b: {
+ default: 10000,
+ label: t('Row limit'),
+ },
+ truncate_metric_b: {
+ default: true,
+ label: t('Truncate Metric'),
+ },
+ seriesTypeB: {
+ default: EchartsTimeseriesSeriesType.Line,
+ label: t('Series type'),
+ renderTrigger: true,
+ },
+ yAxisIndexB: {
+ default: 0,
+ label: t('Y Axis'),
+ renderTrigger: true,
+ },
+ stackB: {
+ default: false,
+ label: t('Stack series'),
+ renderTrigger: true,
+ },
+ areaB: {
+ default: false,
+ label: t('Area chart'),
+ renderTrigger: true,
+ },
+ show_valueB: {
+ default: false,
+ label: t('Show Values'),
+ renderTrigger: true,
+ },
+ markerEnabledB: {
+ default: false,
+ label: t('Marker'),
+ renderTrigger: true,
+ },
+ only_totalB: {
+ default: true,
+ label: t('Only Total'),
+ renderTrigger: true,
+ },
+ opacityB: {
+ default: 0.7,
+ label: t('Opacity'),
+ renderTrigger: true,
+ },
+ markerSizeB: {
+ default: 6,
+ label: t('Marker size'),
+ renderTrigger: true,
+ },
+ sort_series_typeB: {
+ default: DEFAULT_SORT_SERIES_DATA.sort_series_type,
+ label: t('Sort Series By'),
+ renderTrigger: true,
+ },
+ sort_series_ascendingB: {
+ default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending,
+ label: t('Sort Series Ascending'),
+ renderTrigger: true,
+ },
+
+ // X-Axis controls
+ x_axis_time_format: {
+ default: 'smart_date',
+ label: t('Time format'),
+ renderTrigger: true,
+ },
+ xAxisLabelRotation: {
+ default: 0,
+ label: t('Rotate x axis label'),
+ renderTrigger: true,
+ },
+
+ // Tooltip controls
+ show_query_identifiers: {
+ default: false,
+ label: t('Show query identifiers'),
+ renderTrigger: true,
+ },
+ rich_tooltip: {
+ default: true,
+ label: t('Rich Tooltip'),
+ renderTrigger: true,
+ },
+
+ // Y-Axis controls
+ y_axis_format: {
+ default: 'SMART_NUMBER',
+ label: t('Primary y-axis format'),
+ renderTrigger: true,
+ },
+ currency_format: {
+ default: undefined,
+ label: t('Currency format'),
+ renderTrigger: true,
+ },
+ logAxis: {
+ default: false,
+ label: t('Logarithmic y-axis'),
+ renderTrigger: true,
+ },
+ y_axis_bounds: {
+ default: [null, null],
+ label: t('Primary y-axis Bounds'),
+ renderTrigger: true,
+ },
+ y_axis_format_secondary: {
+ default: undefined,
+ label: t('Secondary y-axis format'),
+ renderTrigger: true,
+ },
+ currency_format_secondary: {
+ default: undefined,
+ label: t('Secondary currency format'),
+ renderTrigger: true,
+ },
+ yAxisTitleSecondary: {
+ default: '',
+ label: t('Secondary y-axis title'),
+ renderTrigger: true,
+ },
+ logAxisSecondary: {
+ default: false,
+ label: t('Logarithmic y-axis'),
+ renderTrigger: true,
+ },
+ y_axis_bounds_secondary: {
+ default: [null, null],
+ label: t('Secondary y-axis Bounds'),
+ renderTrigger: true,
+ },
+ minorSplitLine: {
+ default: false,
+ label: t('Minor Split Line'),
+ renderTrigger: true,
+ },
+ truncateYAxis: {
+ default: true,
+ label: t('Truncate Y Axis'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts
index 5d8eee2e57..92bef71b8e 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts
@@ -18,7 +18,7 @@
*/
import { AnnotationType, Behavior, t } from '@superset-ui/core';
import buildQuery from './buildQuery';
-import controlPanel from './controlPanel';
+import controlPanel from './MixedTimeseriesControlPanelSimple';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import example from './images/example.jpg';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/SankeyControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/RadarControlPanelSimple.tsx
similarity index 50%
copy from superset-frontend/plugins/plugin-chart-echarts/src/Sankey/SankeyControlPanelSimple.tsx
copy to superset-frontend/plugins/plugin-chart-echarts/src/Radar/RadarControlPanelSimple.tsx
index 1372757963..ca538ad444 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/SankeyControlPanelSimple.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/RadarControlPanelSimple.tsx
@@ -17,22 +17,26 @@
* under the License.
*/
import { FC, useState } from 'react';
-import { t, validateNonEmpty } from '@superset-ui/core';
+import { t } from '@superset-ui/core';
import { Tabs } from 'antd';
import {
ColorSchemeControl,
+ D3_FORMAT_OPTIONS,
+ D3_TIME_FORMAT_OPTIONS,
+ D3_FORMAT_DOCS,
+ D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
} from '@superset-ui/chart-controls';
-
-// Direct component imports
import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
import TextControl from '../../../../src/explore/components/controls/TextControl';
import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
+import SelectControl from '../../../../src/explore/components/controls/SelectControl';
import ControlHeader from '../../../../src/explore/components/ControlHeader';
import Control from '../../../../src/explore/components/Control';
+import { LABEL_POSITION } from '../constants';
-interface SankeyControlPanelProps {
+interface RadarControlPanelProps {
onChange?: (field: string, value: any) => void;
value?: Record<string, any>;
datasource?: any;
@@ -42,9 +46,9 @@ interface SankeyControlPanelProps {
}
/**
- * A modern React component-based control panel for Sankey charts.
+ * A modern React component-based control panel for Radar charts.
*/
-export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
+export const RadarControlPanel: FC<RadarControlPanelProps> = ({
onChange,
value,
datasource,
@@ -89,27 +93,23 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
// Data tab content
const dataTabContent = (
<div>
- {/* Source Column */}
+ {/* Group by */}
<div style={{ marginBottom: 16 }}>
<ControlHeader
- label={t('Source')}
- description={t('The column to be used as the source of the edge.')}
+ label={t('Group by')}
+ description={t('Columns to group by')}
hovered
/>
{safeColumns.length > 0 ? (
<DndColumnSelect
- value={formValues.source ? [formValues.source] : []}
- onChange={(val: any) => {
- // Source is single select, so take first value
- const sourceValue = Array.isArray(val) && val.length > 0 ? val[0] : null;
- handleChange('source')(sourceValue);
- }}
+ value={formValues.groupby || []}
+ onChange={handleChange('groupby')}
options={safeColumns}
- name="source"
+ name="groupby"
label=""
- multi={false}
+ multi
canDelete
- ghostButtonText={t('Select source column')}
+ ghostButtonText={t('Add dimension')}
type="DndColumnSelect"
actions={actions}
/>
@@ -120,52 +120,21 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
)}
</div>
- {/* Target Column */}
+ {/* Metrics */}
<div style={{ marginBottom: 16 }}>
<ControlHeader
- label={t('Target')}
- description={t('The column to be used as the target of the edge.')}
- hovered
- />
- {safeColumns.length > 0 ? (
- <DndColumnSelect
- value={formValues.target ? [formValues.target] : []}
- onChange={(val: any) => {
- // Target is single select, so take first value
- const targetValue = Array.isArray(val) && val.length > 0 ? val[0] : null;
- handleChange('target')(targetValue);
- }}
- options={safeColumns}
- name="target"
- label=""
- multi={false}
- canDelete
- ghostButtonText={t('Select target column')}
- type="DndColumnSelect"
- actions={actions}
- />
- ) : (
- <div style={{ padding: '10px' }}>
- {t('No columns available. Please select a dataset first.')}
- </div>
- )}
- </div>
-
- {/* Metric */}
- <div style={{ marginBottom: 16 }}>
- <ControlHeader
- label={t('Metric')}
- description={t('Metric to display')}
+ label={t('Metrics')}
+ description={t('Metrics to display')}
hovered
/>
{safeDataSource && safeDataSource.columns ? (
<DndMetricSelect
- value={formValues.metric}
- onChange={handleChange('metric')}
+ value={formValues.metrics || []}
+ onChange={handleChange('metrics')}
datasource={safeDataSource}
- name="metric"
+ name="metrics"
label=""
- multi={false}
+ multi={true}
savedMetrics={safeMetrics}
/>
) : (
@@ -189,7 +158,7 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
formData={formValues}
name="adhoc_filters"
savedMetrics={safeMetrics}
- selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ selectedMetrics={formValues.metrics || []}
type="DndFilterSelect"
actions={actions}
/>
@@ -200,33 +169,20 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
)}
</div>
- {/* Row limit and Sort by metric */}
- <div style={{ display: 'flex', gap: 16 }}>
- <div style={{ flex: 1 }}>
- <ControlHeader
- label={t('Row limit')}
- description={t('Limit the number of rows that are returned')}
- hovered
- />
- <TextControl
- value={formValues.row_limit}
- onChange={handleChange('row_limit')}
- isInt
- placeholder="100"
- controlId="row_limit"
- />
- </div>
- <div style={{ flex: 1 }}>
- <CheckboxControl
- label={t('Sort by metric')}
- description={t(
- 'Whether to sort results by the selected metric in descending order',
- )}
- value={formValues.sort_by_metric ?? true}
- onChange={handleChange('sort_by_metric')}
- hovered
- />
- </div>
+ {/* Row limit */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limit the number of rows that are returned')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit || 10}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="10"
+ controlId="row_limit"
+ />
</div>
</div>
);
@@ -259,6 +215,141 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
);
})()}
</div>
+
+ {/* Show Legend checkbox */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Legend')}
+ description={t('Whether to display a legend for the chart')}
+ value={formValues.show_legend ?? true}
+ onChange={handleChange('show_legend')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Circle radar shape */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Circle radar shape')}
+ description={t("Radar render type, whether to display 'circle' shape.")}
+ value={formValues.is_circle ?? false}
+ onChange={handleChange('is_circle')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Labels Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Labels')}</h4>
+
+ {/* Show Labels checkbox */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Labels')}
+ description={t('Whether to display the labels.')}
+ value={formValues.show_labels ?? true}
+ onChange={handleChange('show_labels')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Label Type */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Label Type')}
+ description={t('What should be shown on the label?')}
+ value={formValues.label_type || 'value'}
+ onChange={handleChange('label_type')}
+ choices={[
+ ['value', t('Value')],
+ ['key_value', t('Category and Value')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Label Position */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Label position')}
+ description={t('Position of the labels')}
+ value={formValues.label_position || 'top'}
+ onChange={handleChange('label_position')}
+ choices={LABEL_POSITION}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Number format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Number format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ value={formValues.number_format || 'SMART_NUMBER'}
+ onChange={handleChange('number_format')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Date format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Date format')}
+ description={D3_FORMAT_DOCS}
+ value={formValues.date_format || 'smart_date'}
+ onChange={handleChange('date_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Metrics Configuration Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Metrics Configuration')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metric Min Value')}
+ description={t('The minimum value of metrics. It is an optional configuration. If not set, it will be the minimum value of the data')}
+ hovered
+ />
+ <TextControl
+ value={formValues.radarMetricMinValue || 0}
+ onChange={handleChange('radarMetricMinValue')}
+ isFloat
+ placeholder="auto"
+ controlId="radarMetricMinValue"
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metric Max Value')}
+ description={t('The maximum value of metrics. It is an optional configuration')}
+ hovered
+ />
+ <TextControl
+ value={formValues.radarMetricMaxValue}
+ onChange={handleChange('radarMetricMaxValue')}
+ isFloat
+ placeholder="auto"
+ controlId="radarMetricMaxValue"
+ />
+ </div>
</div>
</div>
);
@@ -282,10 +373,10 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
{/* Chart/Viz Type Picker */}
<div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
<div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
- {t('Sankey Chart')}
+ {t('Radar Chart')}
</div>
<div style={{ fontSize: '12px', opacity: 0.65 }}>
- {t('Visualize flow data as a Sankey diagram')}
+ {t('Visualize a parallel set of metrics across multiple groups. Each group is visualized using its own line of points and each metric is represented as an edge in the chart.')}
</div>
</div>
@@ -301,7 +392,7 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
};
// Mark this component as a modern panel
-(SankeyControlPanel as any).isModernPanel = true;
+(RadarControlPanel as any).isModernPanel = true;
// Create a config that wraps our React component
const config = {
@@ -309,41 +400,76 @@ const config = {
{
label: null,
expanded: true,
- controlSetRows: [[SankeyControlPanel as any]],
+ controlSetRows: [[RadarControlPanel as any]],
},
],
controlOverrides: {
- source: {
- default: null,
- label: t('Source'),
- validators: [validateNonEmpty],
- },
- target: {
- default: null,
- label: t('Target'),
- validators: [validateNonEmpty],
+ groupby: {
+ default: [],
+ label: t('Group by'),
},
- metric: {
- default: null,
- label: t('Metric'),
+ metrics: {
+ default: [],
+ label: t('Metrics'),
},
adhoc_filters: {
default: [],
label: t('Filters'),
},
row_limit: {
- default: 100,
+ default: 10,
label: t('Row limit'),
},
- sort_by_metric: {
- default: true,
- label: t('Sort by metric'),
- },
color_scheme: {
default: 'supersetColors',
label: t('Color scheme'),
renderTrigger: true,
},
+ show_legend: {
+ default: true,
+ label: t('Show legend'),
+ renderTrigger: true,
+ },
+ show_labels: {
+ default: true,
+ label: t('Show labels'),
+ renderTrigger: true,
+ },
+ label_type: {
+ default: 'value',
+ label: t('Label Type'),
+ renderTrigger: true,
+ },
+ label_position: {
+ default: 'top',
+ label: t('Label position'),
+ renderTrigger: true,
+ },
+ number_format: {
+ default: 'SMART_NUMBER',
+ label: t('Number format'),
+ renderTrigger: true,
+ },
+ date_format: {
+ default: 'smart_date',
+ label: t('Date format'),
+ renderTrigger: true,
+ },
+ is_circle: {
+ default: false,
+ label: t('Circle radar shape'),
+ renderTrigger: true,
+ },
+ radarMetricMinValue: {
+ default: 0,
+ label: t('Metric Min Value'),
+ renderTrigger: true,
+ },
+ radarMetricMaxValue: {
+ default: null,
+ label: t('Metric Max Value'),
+ renderTrigger: true,
+ },
},
};
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts
index cac1b9f2e8..9c101cd00c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts
@@ -19,7 +19,7 @@
*/
import { Behavior, t } from '@superset-ui/core';
import buildQuery from './buildQuery';
-import controlPanel from './controlPanel';
+import controlPanel from './RadarControlPanelSimple';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import example1 from './images/example1.jpg';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/SankeyControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/SankeyControlPanelSimple.tsx
index 1372757963..54091de99a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/SankeyControlPanelSimple.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/SankeyControlPanelSimple.tsx
@@ -19,9 +19,7 @@
import { FC, useState } from 'react';
import { t, validateNonEmpty } from '@superset-ui/core';
import { Tabs } from 'antd';
-import {
- ColorSchemeControl,
-} from '@superset-ui/chart-controls';
+import { ColorSchemeControl } from '@superset-ui/chart-controls';
// Direct component imports
import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
@@ -101,7 +99,8 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
value={formValues.source ? [formValues.source] : []}
onChange={(val: any) => {
// Source is single select, so take first value
- const sourceValue = Array.isArray(val) && val.length > 0 ? val[0] : null;
+ const sourceValue =
+ Array.isArray(val) && val.length > 0 ? val[0] : null;
handleChange('source')(sourceValue);
}}
options={safeColumns}
@@ -132,7 +131,8 @@ export const SankeyControlPanel: FC<SankeyControlPanelProps> = ({
value={formValues.target ? [formValues.target] : []}
onChange={(val: any) => {
// Target is single select, so take first value
- const targetValue = Array.isArray(val) && val.length > 0 ? val[0] : null;
+ const targetValue =
+ Array.isArray(val) && val.length > 0 ? val[0] : null;
handleChange('target')(targetValue);
}}
options={safeColumns}
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/SunburstControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/SunburstControlPanelSimple.tsx
new file mode 100644
index 0000000000..b87f573385
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/SunburstControlPanelSimple.tsx
@@ -0,0 +1,538 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+ LinearColorSchemeControl,
+ D3_FORMAT_OPTIONS,
+ D3_TIME_FORMAT_OPTIONS,
+ D3_FORMAT_DOCS,
+ D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT,
+} from '@superset-ui/chart-controls';
+import { DndColumnSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndMetricSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../src/explore/components/controls/CheckboxControl';
+import SelectControl from '../../../../src/explore/components/controls/SelectControl';
+import CurrencyControl from '../../../../src/explore/components/controls/CurrencyControl';
+import ControlHeader from '../../../../src/explore/components/ControlHeader';
+import Control from '../../../../src/explore/components/Control';
+import { DEFAULT_FORM_DATA, EchartsSunburstLabelType } from './types';
+
+interface SunburstControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+/**
+ * A modern React component-based control panel for Sunburst charts.
+ */
+export const SunburstControlPanel: FC<SunburstControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State for active tab - must be before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // If no valid data yet, show loading state
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure datasource has the expected structure with arrays
+ const safeColumns = Array.isArray(datasource?.columns)
+ ? datasource.columns
+ : [];
+ const safeMetrics = Array.isArray(datasource?.metrics)
+ ? datasource.metrics
+ : [];
+
+ const safeDataSource = {
+ ...datasource,
+ columns: safeColumns,
+ metrics: safeMetrics,
+ };
+
+ // Helper to handle control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Determine if secondary metric is used (for color scheme visibility)
+ const hasSecondaryMetric =
+ formValues.secondary_metric &&
+ formValues.secondary_metric !== formValues.metric;
+
+ // Data tab content
+ const dataTabContent = (
+ <div>
+ {/* Hierarchy (Columns) */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Hierarchy')}
+ description={t(
+ 'Sets the hierarchy levels of the chart. Each level is represented by one ring with the innermost circle as the top of the hierarchy.',
+ )}
+ hovered
+ />
+ {safeColumns.length > 0 ? (
+ <DndColumnSelect
+ value={formValues.columns || []}
+ onChange={handleChange('columns')}
+ options={safeColumns}
+ name="columns"
+ label=""
+ multi
+ canDelete
+ ghostButtonText={t('Add hierarchy level')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available. Please select a dataset first.')}
+ </div>
+ )}
+ </div>
+
+ {/* Primary Metric */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Primary Metric')}
+ description={t(
+ 'The primary metric is used to define the arc segment sizes',
+ )}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.metric}
+ onChange={handleChange('metric')}
+ datasource={safeDataSource}
+ name="metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Secondary Metric */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Secondary Metric')}
+ description={t(
+ '[optional] this secondary metric is used to define the color as a ratio against the primary metric. When omitted, the color is categorical and based on labels',
+ )}
+ hovered
+ />
+ {safeDataSource && safeDataSource.columns ? (
+ <DndMetricSelect
+ value={formValues.secondary_metric}
+ onChange={handleChange('secondary_metric')}
+ datasource={safeDataSource}
+ name="secondary_metric"
+ label=""
+ multi={false}
+ savedMetrics={safeMetrics}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>{t('No metrics available.')}</div>
+ )}
+ </div>
+
+ {/* Filters */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the data')}
+ hovered
+ />
+ {safeDataSource && safeColumns.length > 0 ? (
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={safeDataSource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metric ? [formValues.metric] : []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ ) : (
+ <div style={{ padding: '10px' }}>
+ {t('No columns available for filtering.')}
+ </div>
+ )}
+ </div>
+
+ {/* Row limit and Sort by metric */}
+ <div style={{ display: 'flex', gap: 16 }}>
+ <div style={{ flex: 1 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limit the number of rows that are returned')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="100"
+ controlId="row_limit"
+ />
+ </div>
+ <div style={{ flex: 1 }}>
+ <CheckboxControl
+ label={t('Sort by metric')}
+ description={t(
+ 'Whether to sort results by the selected metric in descending order',
+ )}
+ value={formValues.sort_by_metric ?? true}
+ onChange={handleChange('sort_by_metric')}
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Customize tab content
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ {/* Color Scheme - Categorical (when no secondary metric) */}
+ {!hasSecondaryMetric && (
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Color scheme')}
+ description={t(
+ 'When only a primary metric is provided, a categorical color scale is used.',
+ )}
+ hovered
+ />
+ {(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ const { hidden, ...cleanConfig } =
+ colorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="color_scheme"
+ value={formValues.color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+ )}
+
+ {/* Linear Color Scheme - For secondary metric */}
+ {hasSecondaryMetric && (
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Linear color scheme')}
+ description={t(
+ 'When a secondary metric is provided, a linear color scale is used.',
+ )}
+ hovered
+ />
+ {(() => {
+ const linearColorSchemeControl = LinearColorSchemeControl();
+ const { hidden, ...cleanConfig } =
+ linearColorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="linear_color_scheme"
+ value={formValues.linear_color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('linear_color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+ )}
+ </div>
+
+ {/* Labels Section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Labels')}</h4>
+
+ {/* Show Labels */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Labels')}
+ description={t('Whether to display the labels.')}
+ value={formValues.show_labels ?? DEFAULT_FORM_DATA.showLabels}
+ onChange={handleChange('show_labels')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Percentage threshold */}
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Percentage threshold')}
+ description={t(
+ 'Minimum threshold in percentage points for showing labels.',
+ )}
+ renderTrigger
+ hovered
+ />
+ <TextControl
+ value={formValues.show_labels_threshold ?? 5}
+ onChange={handleChange('show_labels_threshold')}
+ isFloat
+ placeholder="5"
+ controlId="show_labels_threshold"
+ />
+ </div>
+
+ {/* Show Total */}
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Total')}
+ description={t('Whether to display the aggregate count')}
+ value={formValues.show_total ?? false}
+ onChange={handleChange('show_total')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Label Type */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Label Type')}
+ description={t('What should be shown on the label?')}
+ value={formValues.label_type || DEFAULT_FORM_DATA.labelType}
+ onChange={handleChange('label_type')}
+ choices={[
+ [EchartsSunburstLabelType.Key, t('Category Name')],
+ [EchartsSunburstLabelType.Value, t('Value')],
+ [EchartsSunburstLabelType.KeyValue, t('Category and Value')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Number format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Number format')}
+ description={`${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`}
+ value={formValues.number_format || DEFAULT_FORM_DATA.numberFormat}
+ onChange={handleChange('number_format')}
+ choices={D3_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {/* Currency format */}
+ <div style={{ marginBottom: 16 }}>
+ <CurrencyControl
+ value={formValues.currency_format}
+ onChange={handleChange('currency_format')}
+ />
+ </div>
+
+ {/* Date format */}
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Date format')}
+ description={D3_FORMAT_DOCS}
+ value={formValues.date_format || DEFAULT_FORM_DATA.dateFormat}
+ onChange={handleChange('date_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ freeForm
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ // Tab items
+ const tabItems = [
+ {
+ key: 'data',
+ label: t('Data'),
+ children: dataTabContent,
+ },
+ {
+ key: 'customize',
+ label: t('Customize'),
+ children: customizeTabContent,
+ },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ {/* Chart Description */}
+ <div style={{ marginBottom: 16, padding: '12px', borderRadius: '4px' }}>
+ <div style={{ fontSize: '16px', fontWeight: 500, marginBottom: '8px' }}>
+ {t('Sunburst Chart')}
+ </div>
+ <div style={{ fontSize: '12px', opacity: 0.65 }}>
+ {t(
+ 'Visualize flow between different entities using circles to represent different stages of a system',
+ )}
+ </div>
+ </div>
+
+ {/* Tabs for Data and Customize */}
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// Mark this component as a modern panel
+(SunburstControlPanel as any).isModernPanel = true;
+
+// Create a config that wraps our React component
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[SunburstControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ columns: {
+ default: [],
+ label: t('Hierarchy'),
+ },
+ metric: {
+ default: null,
+ label: t('Primary Metric'),
+ },
+ secondary_metric: {
+ default: null,
+ label: t('Secondary Metric'),
+ },
+ adhoc_filters: {
+ default: [],
+ label: t('Filters'),
+ },
+ row_limit: {
+ default: 100,
+ label: t('Row limit'),
+ },
+ sort_by_metric: {
+ default: true,
+ label: t('Sort by metric'),
+ },
+ color_scheme: {
+ default: 'supersetColors',
+ label: t('Color scheme'),
+ renderTrigger: true,
+ },
+ linear_color_scheme: {
+ default: 'blue_white_yellow',
+ label: t('Linear color scheme'),
+ renderTrigger: true,
+ },
+ show_labels: {
+ default: DEFAULT_FORM_DATA.showLabels,
+ label: t('Show labels'),
+ renderTrigger: true,
+ },
+ show_labels_threshold: {
+ default: 5,
+ label: t('Percentage threshold'),
+ renderTrigger: true,
+ },
+ show_total: {
+ default: false,
+ label: t('Show Total'),
+ renderTrigger: true,
+ },
+ label_type: {
+ default: DEFAULT_FORM_DATA.labelType,
+ label: t('Label Type'),
+ renderTrigger: true,
+ },
+ number_format: {
+ default: DEFAULT_FORM_DATA.numberFormat,
+ label: t('Number format'),
+ renderTrigger: true,
+ },
+ currency_format: {
+ default: undefined,
+ label: t('Currency format'),
+ renderTrigger: true,
+ },
+ date_format: {
+ default: DEFAULT_FORM_DATA.dateFormat,
+ label: t('Date format'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts
index d1715c7d81..257e7b248c 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts
@@ -19,7 +19,7 @@
import { Behavior, t } from '@superset-ui/core';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
-import controlPanel from './controlPanel';
+import controlPanel from './SunburstControlPanelSimple';
import buildQuery from './buildQuery';
import example1 from './images/Sunburst1.png';
import example2 from './images/Sunburst2.png';
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/AreaControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/AreaControlPanelSimple.tsx
new file mode 100644
index 0000000000..3577b29afe
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/AreaControlPanelSimple.tsx
@@ -0,0 +1,877 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+ D3_TIME_FORMAT_OPTIONS,
+ D3_FORMAT_OPTIONS,
+ D3_TIME_FORMAT_DOCS,
+ TimeShiftColorControl,
+ YAxisFormatControl,
+ CurrencyFormatControl,
+ ZoomableControl,
+} from '@superset-ui/chart-controls';
+
+// Direct component imports
+import { DndColumnSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndMetricSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../../src/explore/components/controls/CheckboxControl';
+import SliderControl from '../../../../../src/explore/components/controls/SliderControl';
+import SelectControl from '../../../../../src/explore/components/controls/SelectControl';
+import ControlHeader from '../../../../../src/explore/components/ControlHeader';
+import Control from '../../../../../src/explore/components/Control';
+
+import { EchartsTimeseriesSeriesType } from '../types';
+import { TIME_SERIES_DESCRIPTION_TEXT } from '../constants';
+import { AreaChartStackControlOptions } from '../../constants';
+
+interface AreaControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+export const AreaControlPanel: FC<AreaControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State hooks must be called before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // Safety checks for datasource
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure safe data structures
+ const safeColumns = Array.isArray(datasource?.columns) ? datasource.columns : [];
+ const safeMetrics = Array.isArray(datasource?.metrics) ? datasource.metrics : [];
+
+ // Helper for control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ const dataTabContent = (
+ <div>
+ {/* Query section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Query')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('X-axis')}
+ description={t('Dimension to use on x-axis.')}
+ hovered
+ />
+ <DndColumnSelect
+ value={formValues.x_axis ? [formValues.x_axis] : []}
+ onChange={(val: any) => handleChange('x_axis')(Array.isArray(val) ? val[0] : val)}
+ options={safeColumns}
+ name="x_axis"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Time column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metrics')}
+ description={t('One or many metrics to display')}
+ hovered
+ />
+ <DndMetricSelect
+ value={formValues.metrics || []}
+ onChange={handleChange('metrics')}
+ datasource={datasource}
+ name="metrics"
+ label=""
+ multi
+ savedMetrics={safeMetrics}
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Group by')}
+ description={t('One or many columns to group by')}
+ hovered
+ />
+ <DndColumnSelect
+ value={formValues.groupby || []}
+ onChange={handleChange('groupby')}
+ options={safeColumns}
+ name="groupby"
+ label=""
+ multi
+ canDelete
+ ghostButtonText={t('Add dimension')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the chart')}
+ hovered
+ />
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={datasource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metrics ? formValues.metrics : []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limits the number of rows that get displayed.')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="10000"
+ controlId="row_limit"
+ />
+ </div>
+ </div>
+
+ {/* Time options */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Time Options')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Time grain')}
+ description={t('The time granularity for the visualization')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.time_grain_sqla}
+ onChange={handleChange('time_grain_sqla')}
+ choices={datasource?.time_grain_sqla_choices || []}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Time range')}
+ description={t('The time range for the visualization')}
+ hovered
+ />
+ <TextControl
+ value={formValues.time_range}
+ onChange={handleChange('time_range')}
+ placeholder="Last week"
+ controlId="time_range"
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ const customizeTabContent = (
+ <div>
+ {/* Chart Options */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Color scheme')}
+ description={t('Color scheme for the chart')}
+ hovered
+ />
+ {(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="color_scheme"
+ value={formValues.color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Time shift color')}
+ description={t('Color scheme for time shift comparison')}
+ hovered
+ />
+ {(() => {
+ const timeShiftColorControl = TimeShiftColorControl();
+ const { hidden, ...cleanConfig } = timeShiftColorControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="time_shift_color"
+ value={formValues.time_shift_color}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('time_shift_color')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Series Style')}
+ description={t('Series chart type (line, bar etc)')}
+ value={formValues.seriesType || EchartsTimeseriesSeriesType.Line}
+ onChange={handleChange('seriesType')}
+ choices={[
+ [EchartsTimeseriesSeriesType.Line, t('Line')],
+ [EchartsTimeseriesSeriesType.Smooth, t('Smooth Line')],
+ [EchartsTimeseriesSeriesType.Start, t('Step - start')],
+ [EchartsTimeseriesSeriesType.Middle, t('Step - middle')],
+ [EchartsTimeseriesSeriesType.End, t('Step - end')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SliderControl
+ label={t('Area chart opacity')}
+ description={t('Opacity of Area Chart. Also applies to confidence band.')}
+ value={formValues.opacity ?? 0.2}
+ onChange={handleChange('opacity')}
+ {...{ min: 0, max: 1, step: 0.1 }}
+ renderTrigger
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Value')}
+ description={t('Show series values on the chart')}
+ value={formValues.show_value ?? false}
+ onChange={handleChange('show_value')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Stacked Style')}
+ description={t('Stack series on top of each other')}
+ value={formValues.stack}
+ onChange={handleChange('stack')}
+ choices={AreaChartStackControlOptions}
+ clearable
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {formValues.show_value && formValues.stack && (
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Only Total')}
+ description={t('Only show the total value on the stacked chart, and not show on the selected category')}
+ value={formValues.only_total ?? true}
+ onChange={handleChange('only_total')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ {formValues.show_value && formValues.stack && !formValues.only_total && (
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Percentage threshold')}
+ description={t('Minimum threshold in percentage points for showing labels.')}
+ value={formValues.percentage_threshold}
+ onChange={handleChange('percentage_threshold')}
+ isFloat
+ controlId="percentage_threshold"
+ />
+ </div>
+ )}
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Extra Controls')}
+ description={t('Whether to show extra controls or not. Extra controls include things like making multiBar charts stacked or side by side.')}
+ value={formValues.show_extra_controls ?? false}
+ onChange={handleChange('show_extra_controls')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Marker')}
+ description={t('Draw a marker on data points. Only applicable for line types.')}
+ value={formValues.markerEnabled ?? false}
+ onChange={handleChange('markerEnabled')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {formValues.markerEnabled && (
+ <div style={{ marginBottom: 16 }}>
+ <SliderControl
+ label={t('Marker Size')}
+ description={t('Size of marker. Also applies to forecast observations.')}
+ value={formValues.markerSize ?? 6}
+ onChange={handleChange('markerSize')}
+ {...{ min: 0, max: 20, step: 1 }}
+ renderTrigger
+ />
+ </div>
+ )}
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Minor Ticks')}
+ description={t('Show minor ticks on axes')}
+ value={formValues.minorTicks ?? false}
+ onChange={handleChange('minorTicks')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Zoomable')}
+ description={t('Enable zooming')}
+ value={formValues.zoomable ?? false}
+ onChange={handleChange('zoomable')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Legend */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Legend')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show legend')}
+ description={t('Whether to display a legend for the chart')}
+ value={formValues.show_legend ?? true}
+ onChange={handleChange('show_legend')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {formValues.show_legend && (
+ <>
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Type')}
+ description={t('Legend type')}
+ value={formValues.legendType || 'scroll'}
+ onChange={handleChange('legendType')}
+ choices={[
+ ['scroll', t('Scroll')],
+ ['plain', t('Plain')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Orientation')}
+ description={t('Legend Orientation')}
+ value={formValues.legendOrientation || 'top'}
+ onChange={handleChange('legendOrientation')}
+ choices={[
+ ['top', t('Top')],
+ ['bottom', t('Bottom')],
+ ['left', t('Left')],
+ ['right', t('Right')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Margin')}
+ description={t('Additional padding for legend.')}
+ value={formValues.legendMargin}
+ onChange={handleChange('legendMargin')}
+ isInt
+ controlId="legendMargin"
+ />
+ </div>
+ </>
+ )}
+ </div>
+
+ {/* X Axis */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('X Axis')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Time format')}
+ description={`${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`}
+ value={formValues.x_axis_time_format || 'smart_date'}
+ onChange={handleChange('x_axis_time_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SliderControl
+ label={t('Label rotation')}
+ description={t('Rotation angle for axis labels')}
+ value={formValues.xAxisLabelRotation ?? 0}
+ onChange={handleChange('xAxisLabelRotation')}
+ {...{ min: -90, max: 90, step: 15 }}
+ renderTrigger
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Label interval')}
+ description={t('Interval for axis labels')}
+ value={formValues.xAxisLabelInterval}
+ onChange={handleChange('xAxisLabelInterval')}
+ controlId="xAxisLabelInterval"
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Truncate X Axis')}
+ description={t('Truncate X Axis. Can be overridden by specifying a min or max bound.')}
+ value={formValues.truncateXAxis ?? true}
+ onChange={handleChange('truncateXAxis')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {formValues.truncateXAxis && (
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('X Axis Bounds')}
+ description={t('Bounds for the X-axis. When left empty, the bounds are dynamically defined based on the min/max of the data.')}
+ hovered
+ />
+ <div style={{ display: 'flex', gap: 8 }}>
+ <TextControl
+ value={formValues.x_axis_bounds?.[0]}
+ onChange={(val: any) => {
+ const bounds = formValues.x_axis_bounds || [null, null];
+ handleChange('x_axis_bounds')([val, bounds[1]]);
+ }}
+ placeholder="Min"
+ controlId="x_axis_bounds_min"
+ />
+ <TextControl
+ value={formValues.x_axis_bounds?.[1]}
+ onChange={(val: any) => {
+ const bounds = formValues.x_axis_bounds || [null, null];
+ handleChange('x_axis_bounds')([bounds[0], val]);
+ }}
+ placeholder="Max"
+ controlId="x_axis_bounds_max"
+ />
+ </div>
+ </div>
+ )}
+ </div>
+
+ {/* Tooltip */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Tooltip')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Rich tooltip')}
+ description={t('Shows a list of all series available at that point in time')}
+ value={formValues.rich_tooltip ?? true}
+ onChange={handleChange('rich_tooltip')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Tooltip time format')}
+ description={t('Time format for tooltip')}
+ value={formValues.tooltipTimeFormat || 'smart_date'}
+ onChange={handleChange('tooltipTimeFormat')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show total')}
+ description={t('Display total value in tooltip')}
+ value={formValues.showTooltipTotal ?? false}
+ onChange={handleChange('showTooltipTotal')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show percentage')}
+ description={t('Show percentage in tooltip')}
+ value={formValues.showTooltipPercentage ?? false}
+ onChange={handleChange('showTooltipPercentage')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Y Axis */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Y Axis')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Y axis format')}
+ description={t('Format for Y axis values')}
+ hovered
+ />
+ {(() => {
+ const yAxisFormatControl = YAxisFormatControl();
+ const { hidden, ...cleanConfig } = yAxisFormatControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="y_axis_format"
+ value={formValues.y_axis_format}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('y_axis_format')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Currency format')}
+ description={t('Format for currency values')}
+ hovered
+ />
+ {(() => {
+ const currencyFormatControl = CurrencyFormatControl();
+ const { hidden, ...cleanConfig } = currencyFormatControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="currency_format"
+ value={formValues.currency_format}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('currency_format')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Logarithmic y-axis')}
+ description={t('Logarithmic y-axis')}
+ value={formValues.logAxis ?? false}
+ onChange={handleChange('logAxis')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Minor Split Line')}
+ description={t('Draw split lines for minor y-axis ticks')}
+ value={formValues.minorSplitLine ?? false}
+ onChange={handleChange('minorSplitLine')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Truncate Y Axis')}
+ description={t('Truncate Y Axis. Can be overridden by specifying a min or max bound.')}
+ value={formValues.truncateYAxis ?? false}
+ onChange={handleChange('truncateYAxis')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {formValues.truncateYAxis && (
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Y Axis Bounds')}
+ description={t('Bounds for the Y-axis. When left empty, the bounds are dynamically defined based on the min/max of the data.')}
+ hovered
+ />
+ <div style={{ display: 'flex', gap: 8 }}>
+ <TextControl
+ value={formValues.y_axis_bounds?.[0]}
+ onChange={(val: any) => {
+ const bounds = formValues.y_axis_bounds || [null, null];
+ handleChange('y_axis_bounds')([val, bounds[1]]);
+ }}
+ placeholder="Min"
+ controlId="y_axis_bounds_min"
+ />
+ <TextControl
+ value={formValues.y_axis_bounds?.[1]}
+ onChange={(val: any) => {
+ const bounds = formValues.y_axis_bounds || [null, null];
+ handleChange('y_axis_bounds')([bounds[0], val]);
+ }}
+ placeholder="Max"
+ controlId="y_axis_bounds_max"
+ />
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ );
+
+ const tabItems = [
+ { key: 'data', label: t('Data'), children: dataTabContent },
+ { key: 'customize', label: t('Customize'), children: customizeTabContent },
+ ];
+
+ return (
+ <div style={{ padding: '16px' }}>
+ <Tabs
+ activeKey={activeTab}
+ onChange={setActiveTab}
+ items={tabItems}
+ size="large"
+ />
+ </div>
+ );
+};
+
+// CRITICAL: Mark as modern panel
+(AreaControlPanel as any).isModernPanel = true;
+
+// Export wrapper config for compatibility
+const config = {
+ controlPanelSections: [
+ {
+ label: null,
+ expanded: true,
+ controlSetRows: [[AreaControlPanel as any]],
+ },
+ ],
+ controlOverrides: {
+ row_limit: {
+ default: 10000,
+ label: t('Row limit'),
+ renderTrigger: true,
+ },
+ opacity: {
+ default: 0.2,
+ label: t('Area chart opacity'),
+ renderTrigger: true,
+ },
+ seriesType: {
+ default: EchartsTimeseriesSeriesType.Line,
+ label: t('Series Style'),
+ renderTrigger: true,
+ },
+ show_value: {
+ default: false,
+ label: t('Show Value'),
+ renderTrigger: true,
+ },
+ markerEnabled: {
+ default: false,
+ label: t('Marker'),
+ renderTrigger: true,
+ },
+ markerSize: {
+ default: 6,
+ label: t('Marker Size'),
+ renderTrigger: true,
+ },
+ logAxis: {
+ default: false,
+ label: t('Logarithmic y-axis'),
+ renderTrigger: true,
+ },
+ minorSplitLine: {
+ default: false,
+ label: t('Minor Split Line'),
+ renderTrigger: true,
+ },
+ truncateXAxis: {
+ default: true,
+ label: t('Truncate X Axis'),
+ renderTrigger: true,
+ },
+ truncateYAxis: {
+ default: false,
+ label: t('Truncate Y Axis'),
+ renderTrigger: true,
+ },
+ x_axis_time_format: {
+ default: 'smart_date',
+ label: t('Time format'),
+ renderTrigger: true,
+ },
+ rich_tooltip: {
+ default: true,
+ label: t('Rich tooltip'),
+ renderTrigger: true,
+ },
+ tooltipTimeFormat: {
+ default: 'smart_date',
+ label: t('Tooltip time format'),
+ renderTrigger: true,
+ },
+ zoomable: {
+ default: false,
+ label: t('Zoomable'),
+ renderTrigger: true,
+ },
+ show_legend: {
+ default: true,
+ label: t('Show legend'),
+ renderTrigger: true,
+ },
+ legendType: {
+ default: 'scroll',
+ label: t('Legend type'),
+ renderTrigger: true,
+ },
+ legendOrientation: {
+ default: 'top',
+ label: t('Legend orientation'),
+ renderTrigger: true,
+ },
+ minorTicks: {
+ default: false,
+ label: t('Minor Ticks'),
+ renderTrigger: true,
+ },
+ show_extra_controls: {
+ default: false,
+ label: t('Extra Controls'),
+ renderTrigger: true,
+ },
+ only_total: {
+ default: true,
+ label: t('Only Total'),
+ renderTrigger: true,
+ },
+ percentage_threshold: {
+ default: 0,
+ label: t('Percentage threshold'),
+ renderTrigger: true,
+ },
+ },
+};
+
+export default config;
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts
index cac7237976..712f5b8db0 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts
@@ -18,7 +18,7 @@
*/
import { t, AnnotationType, Behavior } from '@superset-ui/core';
import buildQuery from '../buildQuery';
-import controlPanel from './controlPanel';
+import controlPanel from './AreaControlPanelSimple';
import transformProps from '../transformProps';
import thumbnail from './images/thumbnail.png';
import {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/BarControlPanelSimple.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/BarControlPanelSimple.tsx
new file mode 100644
index 0000000000..190ccad610
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/BarControlPanelSimple.tsx
@@ -0,0 +1,1075 @@
+/**
+ * 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 { FC, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { Tabs } from 'antd';
+import {
+ ColorSchemeControl,
+ D3_TIME_FORMAT_OPTIONS,
+ D3_FORMAT_OPTIONS,
+ D3_TIME_FORMAT_DOCS,
+ TimeShiftColorControl,
+ YAxisFormatControl,
+ CurrencyFormatControl,
+ ZoomableControl,
+} from '@superset-ui/chart-controls';
+
+// Direct component imports
+import { DndColumnSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndColumnSelect';
+import { DndMetricSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndMetricSelect';
+import { DndFilterSelect } from '../../../../../src/explore/components/controls/DndColumnSelectControl/DndFilterSelect';
+import TextControl from '../../../../../src/explore/components/controls/TextControl';
+import CheckboxControl from '../../../../../src/explore/components/controls/CheckboxControl';
+import SliderControl from '../../../../../src/explore/components/controls/SliderControl';
+import SelectControl from '../../../../../src/explore/components/controls/SelectControl';
+import RadioButtonControl from '../../../../../src/explore/components/controls/RadioButtonControl';
+import ControlHeader from '../../../../../src/explore/components/ControlHeader';
+import Control from '../../../../../src/explore/components/Control';
+
+import { OrientationType } from '../../types';
+import { TIME_SERIES_DESCRIPTION_TEXT } from '../../constants';
+import { StackControlsValue } from '../../../constants';
+
+interface BarControlPanelProps {
+ onChange?: (field: string, value: any) => void;
+ value?: Record<string, any>;
+ datasource?: any;
+ actions?: any;
+ controls?: any;
+ form_data?: any;
+}
+
+export const BarControlPanel: FC<BarControlPanelProps> = ({
+ onChange,
+ value,
+ datasource,
+ form_data,
+ actions,
+ controls,
+}) => {
+ // State hooks must be called before any early returns
+ const [activeTab, setActiveTab] = useState('data');
+
+ // Safety checks for datasource
+ if (!datasource || !form_data) {
+ return <div>Loading control panel...</div>;
+ }
+
+ // Ensure safe data structures
+ const safeColumns = Array.isArray(datasource?.columns) ? datasource.columns : [];
+ const safeMetrics = Array.isArray(datasource?.metrics) ? datasource.metrics : [];
+
+ // Helper for control changes
+ const handleChange = (field: string) => (val: any) => {
+ if (actions?.setControlValue) {
+ actions.setControlValue(field, val);
+ } else if (onChange) {
+ onChange(field, val);
+ }
+ };
+
+ // Get form values
+ const formValues = form_data || value || {};
+
+ // Helpers for orientation-dependent axis controls
+ const isVertical = formValues.orientation === OrientationType.Vertical;
+ const isHorizontal = formValues.orientation === OrientationType.Horizontal;
+
+ const dataTabContent = (
+ <div>
+ {/* Query section */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Query')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('X-axis')}
+ description={t('Dimension to use on x-axis.')}
+ hovered
+ />
+ <DndColumnSelect
+ value={formValues.x_axis ? [formValues.x_axis] : []}
+ onChange={(val: any) => handleChange('x_axis')(Array.isArray(val) ? val[0] : val)}
+ options={safeColumns}
+ name="x_axis"
+ label=""
+ multi={false}
+ canDelete
+ ghostButtonText={t('Time column')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Metrics')}
+ description={t('One or many metrics to display')}
+ hovered
+ />
+ <DndMetricSelect
+ value={formValues.metrics || []}
+ onChange={handleChange('metrics')}
+ datasource={datasource}
+ name="metrics"
+ label=""
+ multi
+ savedMetrics={safeMetrics}
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Group by')}
+ description={t('One or many columns to group by')}
+ hovered
+ />
+ <DndColumnSelect
+ value={formValues.groupby || []}
+ onChange={handleChange('groupby')}
+ options={safeColumns}
+ name="groupby"
+ label=""
+ multi
+ canDelete
+ ghostButtonText={t('Add dimension')}
+ type="DndColumnSelect"
+ actions={actions}
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Filters')}
+ description={t('Filters to apply to the chart')}
+ hovered
+ />
+ <DndFilterSelect
+ value={formValues.adhoc_filters || []}
+ onChange={handleChange('adhoc_filters')}
+ datasource={datasource}
+ columns={safeColumns}
+ formData={formValues}
+ name="adhoc_filters"
+ savedMetrics={safeMetrics}
+ selectedMetrics={formValues.metrics ? formValues.metrics : []}
+ type="DndFilterSelect"
+ actions={actions}
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Row limit')}
+ description={t('Limits the number of rows that get displayed.')}
+ hovered
+ />
+ <TextControl
+ value={formValues.row_limit}
+ onChange={handleChange('row_limit')}
+ isInt
+ placeholder="10000"
+ controlId="row_limit"
+ />
+ </div>
+ </div>
+
+ {/* Time options */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Time Options')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Time grain')}
+ description={t('The time granularity for the visualization')}
+ hovered
+ />
+ <SelectControl
+ value={formValues.time_grain_sqla}
+ onChange={handleChange('time_grain_sqla')}
+ choices={datasource?.time_grain_sqla_choices || []}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Time range')}
+ description={t('The time range for the visualization')}
+ hovered
+ />
+ <TextControl
+ value={formValues.time_range}
+ onChange={handleChange('time_range')}
+ placeholder="Last week"
+ controlId="time_range"
+ />
+ </div>
+ </div>
+ </div>
+ );
+
+ const customizeTabContent = (
+ <div>
+ {/* Chart Orientation */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Orientation')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <RadioButtonControl
+ label={t('Bar orientation')}
+ description={t('Orientation of bar chart')}
+ value={formValues.orientation || OrientationType.Vertical}
+ onChange={handleChange('orientation')}
+ options={[
+ [OrientationType.Vertical, t('Vertical')],
+ [OrientationType.Horizontal, t('Horizontal')],
+ ]}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Chart Options */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Chart Options')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Color scheme')}
+ description={t('Color scheme for the chart')}
+ hovered
+ />
+ {(() => {
+ const colorSchemeControl = ColorSchemeControl();
+ const { hidden, ...cleanConfig } = colorSchemeControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="color_scheme"
+ value={formValues.color_scheme}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('color_scheme')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Time shift color')}
+ description={t('Color scheme for time shift comparison')}
+ hovered
+ />
+ {(() => {
+ const timeShiftColorControl = TimeShiftColorControl();
+ const { hidden, ...cleanConfig } = timeShiftColorControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="time_shift_color"
+ value={formValues.time_shift_color}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('time_shift_color')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show Value')}
+ description={t('Show series values on the chart')}
+ value={formValues.show_value ?? false}
+ onChange={handleChange('show_value')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {formValues.stack === StackControlsValue.Stack && (
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Split stack by')}
+ description={t('Stack in groups, where each group corresponds to a dimension')}
+ value={formValues.stackDimension}
+ onChange={handleChange('stackDimension')}
+ choices={formValues.groupby ? formValues.groupby.map((val: any) => [val.toString(), val.toString()]) : []}
+ clearable
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Minor Ticks')}
+ description={t('Show minor ticks on axes')}
+ value={formValues.minorTicks ?? false}
+ onChange={handleChange('minorTicks')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Zoomable')}
+ description={t('Enable zooming')}
+ value={formValues.zoomable ?? false}
+ onChange={handleChange('zoomable')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </div>
+
+ {/* Legend */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('Legend')}</h4>
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Show legend')}
+ description={t('Whether to display a legend for the chart')}
+ value={formValues.show_legend ?? true}
+ onChange={handleChange('show_legend')}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ {formValues.show_legend && (
+ <>
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Type')}
+ description={t('Legend type')}
+ value={formValues.legendType || 'scroll'}
+ onChange={handleChange('legendType')}
+ choices={[
+ ['scroll', t('Scroll')],
+ ['plain', t('Plain')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Orientation')}
+ description={t('Legend Orientation')}
+ value={formValues.legendOrientation || 'top'}
+ onChange={handleChange('legendOrientation')}
+ choices={[
+ ['top', t('Top')],
+ ['bottom', t('Bottom')],
+ ['left', t('Left')],
+ ['right', t('Right')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Margin')}
+ description={t('Additional padding for legend.')}
+ value={formValues.legendMargin}
+ onChange={handleChange('legendMargin')}
+ isInt
+ controlId="legendMargin"
+ />
+ </div>
+ </>
+ )}
+ </div>
+
+ {/* X Axis */}
+ <div style={{ marginBottom: 24 }}>
+ <h4>{t('X Axis')}</h4>
+
+ {/* X-axis title controls - show when vertical */}
+ {isVertical && (
+ <>
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Axis Title')}
+ description={t('Title for the X axis')}
+ value={formValues.x_axis_title}
+ onChange={handleChange('x_axis_title')}
+ controlId="x_axis_title"
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Axis Title Margin')}
+ description={t('Margin for axis title')}
+ value={formValues.x_axis_title_margin}
+ onChange={handleChange('x_axis_title_margin')}
+ choices={[
+ [5, '5'],
+ [10, '10'],
+ [15, '15'],
+ [20, '20'],
+ [25, '25'],
+ [30, '30'],
+ ]}
+ clearable
+ renderTrigger
+ hovered
+ />
+ </div>
+ </>
+ )}
+
+ {/* X-axis format controls - show when vertical */}
+ {isVertical && (
+ <>
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Time format')}
+ description={`${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`}
+ value={formValues.x_axis_time_format || 'smart_date'}
+ onChange={handleChange('x_axis_time_format')}
+ choices={D3_TIME_FORMAT_OPTIONS}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SliderControl
+ label={t('Label rotation')}
+ description={t('Rotation angle for axis labels')}
+ value={formValues.xAxisLabelRotation ?? 0}
+ onChange={handleChange('xAxisLabelRotation')}
+ {...{ min: -90, max: 90, step: 15 }}
+ renderTrigger
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Label interval')}
+ description={t('Interval for axis labels')}
+ value={formValues.xAxisLabelInterval}
+ onChange={handleChange('xAxisLabelInterval')}
+ controlId="xAxisLabelInterval"
+ />
+ </div>
+ </>
+ )}
+
+ {/* Y-axis title controls - show when horizontal */}
+ {isHorizontal && (
+ <>
+ <div style={{ marginBottom: 16 }}>
+ <TextControl
+ label={t('Axis Title')}
+ description={t('Title for the Y axis')}
+ value={formValues.y_axis_title}
+ onChange={handleChange('y_axis_title')}
+ controlId="y_axis_title"
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Axis Title Margin')}
+ description={t('Margin for axis title')}
+ value={formValues.y_axis_title_margin}
+ onChange={handleChange('y_axis_title_margin')}
+ choices={[
+ [5, '5'],
+ [10, '10'],
+ [15, '15'],
+ [20, '20'],
+ [25, '25'],
+ [30, '30'],
+ ]}
+ clearable
+ renderTrigger
+ hovered
+ />
+ </div>
+
+ <div style={{ marginBottom: 16 }}>
+ <SelectControl
+ label={t('Axis Title Position')}
+ description={t('Position of the axis title')}
+ value={formValues.y_axis_title_position || 'Left'}
+ onChange={handleChange('y_axis_title_position')}
+ choices={[
+ ['Left', t('Left')],
+ ['Top', t('Top')],
+ ]}
+ clearable={false}
+ renderTrigger
+ hovered
+ />
+ </div>
+ </>
+ )}
+
+ {/* Y-axis format controls - show when horizontal */}
+ {isHorizontal && (
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Axis Format')}
+ description={t('Format for axis values')}
+ hovered
+ />
+ {(() => {
+ const yAxisFormatControl = YAxisFormatControl();
+ const { hidden, ...cleanConfig } = yAxisFormatControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="y_axis_format"
+ value={formValues.y_axis_format}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('y_axis_format')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+ )}
+
+ <div style={{ marginBottom: 16 }}>
+ <ControlHeader
+ label={t('Currency format')}
+ description={t('Format for currency values')}
+ hovered
+ />
+ {(() => {
+ const currencyFormatControl = CurrencyFormatControl();
+ const { hidden, ...cleanConfig } = currencyFormatControl.config || {};
+ return (
+ <Control
+ {...cleanConfig}
+ name="currency_format"
+ value={formValues.currency_format}
+ actions={{
+ ...actions,
+ setControlValue: (field: string, val: any) => {
+ handleChange('currency_format')(val);
+ },
+ }}
+ renderTrigger
+ />
+ );
+ })()}
+ </div>
+
+ {/* Logarithmic axis - show for value axis based on orientation */}
+ {((isVertical && isHorizontal) || isHorizontal) && (
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Logarithmic axis')}
+ description={t('Logarithmic axis')}
+ value={formValues.logAxis ?? false}
+ onChange={handleChange('logAxis')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ {/* Minor split line - show for value axis based on orientation */}
+ {((isVertical && isHorizontal) || isHorizontal) && (
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
+ label={t('Minor Split Line')}
+ description={t('Draw split lines for minor axis ticks')}
+ value={formValues.minorSplitLine ?? false}
+ onChange={handleChange('minorSplitLine')}
+ renderTrigger
+ hovered
+ />
+ </div>
+ )}
+
+ <div style={{ marginBottom: 16 }}>
+ <CheckboxControl
... 4887 lines suppressed ...