diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.test.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.test.tsx
new file mode 100644
index 00000000000..eac5d13df7c
--- /dev/null
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.test.tsx
@@ -0,0 +1,524 @@
+/**
+ * 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 { render, screen, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { supersetTheme, ThemeProvider } from '@apache-superset/core/ui';
+import { Provider } from 'react-redux';
+import { configureStore } from '@reduxjs/toolkit';
+import { DatasourceType, SupersetClient } from '@superset-ui/core';
+import DeckMulti from './Multi';
+import * as fitViewportModule from '../utils/fitViewport';
+
+// Mock DeckGLContainer
+jest.mock('../DeckGLContainer', () => ({
+ DeckGLContainerStyledWrapper: ({ viewport, layers }: any) => (
+
+ DeckGL Container Mock
+
+ ),
+}));
+
+// Mock SupersetClient
+jest.mock('@superset-ui/core', () => ({
+ ...jest.requireActual('@superset-ui/core'),
+ SupersetClient: {
+ get: jest.fn(),
+ },
+}));
+
+const mockStore = configureStore({
+ reducer: {
+ dataMask: () => ({}),
+ },
+});
+
+const baseMockProps = {
+ formData: {
+ datasource: 'test_datasource',
+ viz_type: 'deck_multi',
+ deck_slices: [1, 2],
+ autozoom: false,
+ mapbox_style: 'mapbox://styles/mapbox/light-v9',
+ },
+ payload: {
+ data: {
+ slices: [
+ {
+ slice_id: 1,
+ form_data: {
+ viz_type: 'deck_scatter',
+ datasource: 'test_datasource',
+ },
+ },
+ {
+ slice_id: 2,
+ form_data: {
+ viz_type: 'deck_polygon',
+ datasource: 'test_datasource',
+ },
+ },
+ ],
+ features: {
+ deck_scatter: [{ position: [0, 0] }],
+ deck_polygon: [
+ {
+ polygon: [
+ [1, 1],
+ [2, 2],
+ ],
+ },
+ ],
+ deck_path: [],
+ deck_grid: [],
+ deck_contour: [],
+ deck_heatmap: [],
+ deck_hex: [],
+ deck_arc: [],
+ deck_geojson: [],
+ deck_screengrid: [],
+ },
+ mapboxApiKey: 'test-key',
+ },
+ },
+ setControlValue: jest.fn(),
+ viewport: { longitude: 0, latitude: 0, zoom: 1 },
+ onAddFilter: jest.fn(),
+ height: 600,
+ width: 800,
+ datasource: {
+ id: 1,
+ type: DatasourceType.Table,
+ name: 'test_datasource',
+ columns: [],
+ metrics: [],
+ columnFormats: {},
+ currencyFormats: {},
+ verboseMap: {},
+ },
+ onSelect: jest.fn(),
+};
+
+const renderWithProviders = (component: React.ReactElement) =>
+ render(
+
+ {component}
+ ,
+ );
+
+describe('DeckMulti Autozoom Functionality', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (SupersetClient.get as jest.Mock).mockResolvedValue({
+ json: {
+ data: {
+ features: [],
+ },
+ },
+ });
+ });
+
+ it('should NOT apply autozoom when autozoom is false', () => {
+ const fitViewportSpy = jest.spyOn(fitViewportModule, 'default');
+
+ const props = {
+ ...baseMockProps,
+ formData: {
+ ...baseMockProps.formData,
+ autozoom: false,
+ },
+ };
+
+ renderWithProviders();
+
+ // fitViewport should not be called when autozoom is false
+ expect(fitViewportSpy).not.toHaveBeenCalled();
+
+ fitViewportSpy.mockRestore();
+ });
+
+ it('should apply autozoom when autozoom is true', () => {
+ const fitViewportSpy = jest.spyOn(fitViewportModule, 'default');
+ fitViewportSpy.mockReturnValue({
+ longitude: -122.4,
+ latitude: 37.8,
+ zoom: 10,
+ });
+
+ const props = {
+ ...baseMockProps,
+ formData: {
+ ...baseMockProps.formData,
+ autozoom: true,
+ },
+ };
+
+ renderWithProviders();
+
+ // fitViewport should be called with the points from all layers
+ expect(fitViewportSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ longitude: 0,
+ latitude: 0,
+ zoom: 1,
+ }),
+ expect.objectContaining({
+ width: 800,
+ height: 600,
+ points: expect.any(Array),
+ }),
+ );
+
+ fitViewportSpy.mockRestore();
+ });
+
+ it('should use adjusted viewport when autozoom is enabled', async () => {
+ const fitViewportSpy = jest.spyOn(fitViewportModule, 'default');
+ const adjustedViewport = {
+ longitude: -122.4,
+ latitude: 37.8,
+ zoom: 12,
+ };
+ fitViewportSpy.mockReturnValue(adjustedViewport);
+
+ const props = {
+ ...baseMockProps,
+ formData: {
+ ...baseMockProps.formData,
+ autozoom: true,
+ },
+ };
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ const container = screen.getByTestId('deckgl-container');
+ const viewportData = JSON.parse(
+ container.getAttribute('data-viewport') || '{}',
+ );
+
+ expect(viewportData.longitude).toBe(adjustedViewport.longitude);
+ expect(viewportData.latitude).toBe(adjustedViewport.latitude);
+ expect(viewportData.zoom).toBe(adjustedViewport.zoom);
+ });
+
+ fitViewportSpy.mockRestore();
+ });
+
+ it('should set zoom to 0 when calculated zoom is negative', async () => {
+ const fitViewportSpy = jest.spyOn(fitViewportModule, 'default');
+ fitViewportSpy.mockReturnValue({
+ longitude: 0,
+ latitude: 0,
+ zoom: -5, // negative zoom
+ });
+
+ const props = {
+ ...baseMockProps,
+ formData: {
+ ...baseMockProps.formData,
+ autozoom: true,
+ },
+ };
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ const container = screen.getByTestId('deckgl-container');
+ const viewportData = JSON.parse(
+ container.getAttribute('data-viewport') || '{}',
+ );
+
+ // Zoom should be 0, not negative
+ expect(viewportData.zoom).toBe(0);
+ });
+
+ fitViewportSpy.mockRestore();
+ });
+
+ it('should handle empty features gracefully when autozoom is enabled', () => {
+ const fitViewportSpy = jest.spyOn(fitViewportModule, 'default');
+
+ const props = {
+ ...baseMockProps,
+ formData: {
+ ...baseMockProps.formData,
+ autozoom: true,
+ },
+ payload: {
+ ...baseMockProps.payload,
+ data: {
+ ...baseMockProps.payload.data,
+ features: {
+ deck_scatter: [],
+ deck_polygon: [],
+ deck_path: [],
+ deck_grid: [],
+ deck_contour: [],
+ deck_heatmap: [],
+ deck_hex: [],
+ deck_arc: [],
+ deck_geojson: [],
+ deck_screengrid: [],
+ },
+ },
+ },
+ };
+
+ renderWithProviders();
+
+ // fitViewport should not be called when there are no points
+ expect(fitViewportSpy).not.toHaveBeenCalled();
+
+ fitViewportSpy.mockRestore();
+ });
+
+ it('should collect points from all layer types when autozoom is enabled', () => {
+ const fitViewportSpy = jest.spyOn(fitViewportModule, 'default');
+ fitViewportSpy.mockReturnValue({
+ longitude: 0,
+ latitude: 0,
+ zoom: 10,
+ });
+
+ const props = {
+ ...baseMockProps,
+ formData: {
+ ...baseMockProps.formData,
+ autozoom: true,
+ },
+ payload: {
+ ...baseMockProps.payload,
+ data: {
+ ...baseMockProps.payload.data,
+ features: {
+ deck_scatter: [{ position: [1, 1] }, { position: [2, 2] }],
+ deck_polygon: [
+ {
+ polygon: [
+ [3, 3],
+ [4, 4],
+ ],
+ },
+ ],
+ deck_arc: [{ sourcePosition: [5, 5], targetPosition: [6, 6] }],
+ deck_path: [],
+ deck_grid: [],
+ deck_contour: [],
+ deck_heatmap: [],
+ deck_hex: [],
+ deck_geojson: [],
+ deck_screengrid: [],
+ },
+ },
+ },
+ };
+
+ renderWithProviders();
+
+ expect(fitViewportSpy).toHaveBeenCalled();
+ const callArgs = fitViewportSpy.mock.calls[0];
+ const { points } = callArgs[1];
+
+ // Should have points from scatter (2), polygon (2), and arc (2) = 6 points total
+ expect(points.length).toBeGreaterThan(0);
+
+ fitViewportSpy.mockRestore();
+ });
+
+ it('should use original viewport when autozoom is disabled', async () => {
+ const fitViewportSpy = jest.spyOn(fitViewportModule, 'default');
+
+ const originalViewport = { longitude: -100, latitude: 40, zoom: 5 };
+ const props = {
+ ...baseMockProps,
+ viewport: originalViewport,
+ formData: {
+ ...baseMockProps.formData,
+ autozoom: false,
+ },
+ };
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ const container = screen.getByTestId('deckgl-container');
+ const viewportData = JSON.parse(
+ container.getAttribute('data-viewport') || '{}',
+ );
+
+ // Should use original viewport without modification
+ expect(viewportData.longitude).toBe(originalViewport.longitude);
+ expect(viewportData.latitude).toBe(originalViewport.latitude);
+ expect(viewportData.zoom).toBe(originalViewport.zoom);
+ });
+
+ // fitViewport should not have been called
+ expect(fitViewportSpy).not.toHaveBeenCalled();
+
+ fitViewportSpy.mockRestore();
+ });
+
+ it('should apply autozoom when autozoom is undefined (backward compatibility)', () => {
+ const fitViewportSpy = jest.spyOn(fitViewportModule, 'default');
+ fitViewportSpy.mockReturnValue({
+ longitude: -122.4,
+ latitude: 37.8,
+ zoom: 10,
+ });
+
+ const props = {
+ ...baseMockProps,
+ formData: {
+ ...baseMockProps.formData,
+ autozoom: undefined, // Simulating existing charts created before this feature
+ },
+ };
+
+ renderWithProviders();
+
+ // fitViewport should be called for backward compatibility with existing charts
+ expect(fitViewportSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ longitude: 0,
+ latitude: 0,
+ zoom: 1,
+ }),
+ expect.objectContaining({
+ width: 800,
+ height: 600,
+ points: expect.any(Array),
+ }),
+ );
+
+ fitViewportSpy.mockRestore();
+ });
+
+ it('should use adjusted viewport when autozoom is undefined', async () => {
+ const fitViewportSpy = jest.spyOn(fitViewportModule, 'default');
+ const adjustedViewport = {
+ longitude: -122.4,
+ latitude: 37.8,
+ zoom: 12,
+ };
+ fitViewportSpy.mockReturnValue(adjustedViewport);
+
+ const props = {
+ ...baseMockProps,
+ formData: {
+ ...baseMockProps.formData,
+ autozoom: undefined, // Simulating existing charts
+ },
+ };
+
+ renderWithProviders();
+
+ await waitFor(() => {
+ const container = screen.getByTestId('deckgl-container');
+ const viewportData = JSON.parse(
+ container.getAttribute('data-viewport') || '{}',
+ );
+
+ // Should use adjusted viewport for backward compatibility
+ expect(viewportData.longitude).toBe(adjustedViewport.longitude);
+ expect(viewportData.latitude).toBe(adjustedViewport.latitude);
+ expect(viewportData.zoom).toBe(adjustedViewport.zoom);
+ });
+
+ fitViewportSpy.mockRestore();
+ });
+});
+
+describe('DeckMulti Component Rendering', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (SupersetClient.get as jest.Mock).mockResolvedValue({
+ json: {
+ data: {
+ features: [],
+ },
+ },
+ });
+ });
+
+ it('should render DeckGLContainer', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('deckgl-container')).toBeInTheDocument();
+ });
+ });
+
+ it('should pass correct props to DeckGLContainer', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const container = screen.getByTestId('deckgl-container');
+ const viewportData = JSON.parse(
+ container.getAttribute('data-viewport') || '{}',
+ );
+
+ expect(viewportData).toMatchObject({
+ longitude: baseMockProps.viewport.longitude,
+ latitude: baseMockProps.viewport.latitude,
+ zoom: baseMockProps.viewport.zoom,
+ });
+ });
+ });
+
+ it('should handle viewport changes', async () => {
+ const { rerender } = renderWithProviders();
+
+ // Wait for initial render
+ await waitFor(() => {
+ expect(screen.getByTestId('deckgl-container')).toBeInTheDocument();
+ });
+
+ const newViewport = { longitude: 10, latitude: 20, zoom: 8 };
+ const updatedProps = {
+ ...baseMockProps,
+ viewport: newViewport,
+ formData: {
+ ...baseMockProps.formData,
+ deck_slices: [1, 2, 3], // Change deck_slices to trigger loadLayers
+ },
+ };
+
+ rerender(
+
+
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ const container = screen.getByTestId('deckgl-container');
+ const viewportData = JSON.parse(
+ container.getAttribute('data-viewport') || '{}',
+ );
+
+ expect(viewportData.longitude).toBe(newViewport.longitude);
+ expect(viewportData.latitude).toBe(newViewport.latitude);
+ });
+ });
+});
diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx
index c6d2b48e320..1ee7ee12707 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx
@@ -115,26 +115,33 @@ const DeckMulti = (props: DeckMultiProps) => {
const getAdjustedViewport = useCallback(() => {
let viewport = { ...props.viewport };
- const points = [
- ...getPointsPolygon(props.payload.data.features.deck_polygon || []),
- ...getPointsPath(props.payload.data.features.deck_path || []),
- ...getPointsGrid(props.payload.data.features.deck_grid || []),
- ...getPointsScatter(props.payload.data.features.deck_scatter || []),
- ...getPointsContour(props.payload.data.features.deck_contour || []),
- ...getPointsHeatmap(props.payload.data.features.deck_heatmap || []),
- ...getPointsHex(props.payload.data.features.deck_hex || []),
- ...getPointsArc(props.payload.data.features.deck_arc || []),
- ...getPointsGeojson(props.payload.data.features.deck_geojson || []),
- ...getPointsScreengrid(props.payload.data.features.deck_screengrid || []),
- ];
- if (props.formData) {
- viewport = fitViewport(viewport, {
- width: props.width,
- height: props.height,
- points,
- });
+ // Default to autozoom enabled for backward compatibility (undefined treated as true)
+ if (props.formData.autozoom !== false) {
+ const points = [
+ ...getPointsPolygon(props.payload.data.features.deck_polygon || []),
+ ...getPointsPath(props.payload.data.features.deck_path || []),
+ ...getPointsGrid(props.payload.data.features.deck_grid || []),
+ ...getPointsScatter(props.payload.data.features.deck_scatter || []),
+ ...getPointsContour(props.payload.data.features.deck_contour || []),
+ ...getPointsHeatmap(props.payload.data.features.deck_heatmap || []),
+ ...getPointsHex(props.payload.data.features.deck_hex || []),
+ ...getPointsArc(props.payload.data.features.deck_arc || []),
+ ...getPointsGeojson(props.payload.data.features.deck_geojson || []),
+ ...getPointsScreengrid(
+ props.payload.data.features.deck_screengrid || [],
+ ),
+ ];
+
+ if (props.formData && points.length > 0) {
+ viewport = fitViewport(viewport, {
+ width: props.width,
+ height: props.height,
+ points,
+ });
+ }
}
+
if (viewport.zoom < 0) {
viewport.zoom = 0;
}
diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.test.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.test.ts
new file mode 100644
index 00000000000..dc37d54db3b
--- /dev/null
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.test.ts
@@ -0,0 +1,351 @@
+/**
+ * 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 type {
+ ControlPanelSectionConfig,
+ ControlSetRow,
+ ControlSetItem,
+} from '@superset-ui/chart-controls';
+import controlPanel from './controlPanel';
+
+test('controlPanel should have Map section', () => {
+ const mapSection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Map',
+ );
+
+ expect(mapSection).toBeDefined();
+ expect(mapSection?.expanded).toBe(true);
+});
+
+test('controlPanel should have Query section', () => {
+ const querySection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Query',
+ );
+
+ expect(querySection).toBeDefined();
+ expect(querySection?.expanded).toBe(true);
+});
+
+test('controlPanel Map section should include viewport control', () => {
+ const mapSection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Map',
+ );
+
+ // viewport is imported from Shared_DeckGL and included in controlSetRows
+ expect(mapSection?.controlSetRows).toBeDefined();
+ expect(mapSection?.controlSetRows.length).toBeGreaterThan(0);
+});
+
+test('controlPanel Map section should include autozoom control', () => {
+ const mapSection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Map',
+ );
+
+ // autozoom is imported from Shared_DeckGL and included in controlSetRows
+ expect(mapSection?.controlSetRows).toBeDefined();
+ expect(mapSection?.controlSetRows.length).toBeGreaterThan(0);
+});
+
+test('controlPanel should include deck_slices control with validation', () => {
+ const mapSection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Map',
+ );
+
+ const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) =>
+ row.some(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ),
+ );
+
+ const deckSlicesControl = deckSlicesRow?.find(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ) as any;
+
+ expect(deckSlicesControl).toBeDefined();
+ expect(deckSlicesControl?.config?.validators).toBeDefined();
+ expect(deckSlicesControl?.config?.validators.length).toBeGreaterThan(0);
+ expect(deckSlicesControl?.config?.multi).toBe(true);
+ expect(deckSlicesControl?.config?.type).toBe('SelectAsyncControl');
+});
+
+test('deck_slices control should have correct label and description', () => {
+ const mapSection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Map',
+ );
+
+ const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) =>
+ row.some(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ),
+ );
+
+ const deckSlicesControl = deckSlicesRow?.find(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ) as any;
+
+ expect(deckSlicesControl?.config?.label).toBeDefined();
+ expect(deckSlicesControl?.config?.description).toBeDefined();
+ expect(deckSlicesControl?.config?.placeholder).toBeDefined();
+});
+
+test('deck_slices control should have correct API endpoint', () => {
+ const mapSection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Map',
+ );
+
+ const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) =>
+ row.some(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ),
+ );
+
+ const deckSlicesControl = deckSlicesRow?.find(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ) as any;
+
+ expect(deckSlicesControl?.config?.dataEndpoint).toBe(
+ 'api/v1/chart/?q=(filters:!((col:viz_type,opr:sw,value:deck)))',
+ );
+});
+
+test('deck_slices mutator should add index labels to selected charts', () => {
+ const mapSection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Map',
+ );
+
+ const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) =>
+ row.some(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ),
+ );
+
+ const deckSlicesControl = deckSlicesRow?.find(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ) as any;
+
+ const mockData = {
+ result: [
+ { id: 1, slice_name: 'Chart A' },
+ { id: 2, slice_name: 'Chart B' },
+ { id: 3, slice_name: 'Chart C' },
+ ],
+ };
+
+ const selectedIds = [2, 3];
+ const result = deckSlicesControl.config.mutator(mockData, selectedIds);
+
+ expect(result).toEqual([
+ { value: 1, label: 'Chart A' },
+ { value: 2, label: 'Chart B [1]' },
+ { value: 3, label: 'Chart C [2]' },
+ ]);
+});
+
+test('deck_slices mutator should handle empty result', () => {
+ const mapSection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Map',
+ );
+
+ const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) =>
+ row.some(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ),
+ );
+
+ const deckSlicesControl = deckSlicesRow?.find(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ) as any;
+
+ const mockData = {
+ result: undefined,
+ };
+
+ const result = deckSlicesControl.config.mutator(mockData, []);
+
+ expect(result).toEqual([]);
+});
+
+test('deck_slices mutator should handle undefined selected values', () => {
+ const mapSection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Map',
+ );
+
+ const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) =>
+ row.some(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ),
+ );
+
+ const deckSlicesControl = deckSlicesRow?.find(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ) as any;
+
+ const mockData = {
+ result: [
+ { id: 1, slice_name: 'Chart A' },
+ { id: 2, slice_name: 'Chart B' },
+ ],
+ };
+
+ const result = deckSlicesControl.config.mutator(mockData, undefined);
+
+ expect(result).toEqual([
+ { value: 1, label: 'Chart A' },
+ { value: 2, label: 'Chart B' },
+ ]);
+});
+
+test('deck_slices mutator should preserve order based on selection', () => {
+ const mapSection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Map',
+ );
+
+ const deckSlicesRow = mapSection?.controlSetRows.find((row: ControlSetRow) =>
+ row.some(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ),
+ );
+
+ const deckSlicesControl = deckSlicesRow?.find(
+ (control: ControlSetItem) =>
+ control &&
+ typeof control === 'object' &&
+ 'name' in control &&
+ control.name === 'deck_slices',
+ ) as any;
+
+ const mockData = {
+ result: [
+ { id: 1, slice_name: 'Chart A' },
+ { id: 2, slice_name: 'Chart B' },
+ { id: 3, slice_name: 'Chart C' },
+ { id: 4, slice_name: 'Chart D' },
+ ],
+ };
+
+ // Select in specific order: 3, 1, 4
+ const selectedIds = [3, 1, 4];
+ const result = deckSlicesControl.config.mutator(mockData, selectedIds);
+
+ expect(result).toEqual([
+ { value: 1, label: 'Chart A [2]' }, // second in selection
+ { value: 2, label: 'Chart B' }, // not selected
+ { value: 3, label: 'Chart C [1]' }, // first in selection
+ { value: 4, label: 'Chart D [3]' }, // third in selection
+ ]);
+});
+
+test('Query section should include adhoc_filters control', () => {
+ const querySection = controlPanel.controlPanelSections.find(
+ (
+ section: ControlPanelSectionConfig | null,
+ ): section is ControlPanelSectionConfig =>
+ section !== null && section.label === 'Query',
+ );
+
+ const hasAdhocFilters = querySection?.controlSetRows.some(
+ (row: ControlSetRow) => row.includes('adhoc_filters'),
+ );
+
+ expect(hasAdhocFilters).toBe(true);
+});
diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts
index 1ad60394d7c..3bf2613e8a5 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts
@@ -18,7 +18,7 @@
*/
import { t } from '@apache-superset/core';
import { validateNonEmpty } from '@superset-ui/core';
-import { viewport, mapboxStyle } from '../utilities/Shared_DeckGL';
+import { viewport, mapboxStyle, autozoom } from '../utilities/Shared_DeckGL';
export default {
controlPanelSections: [
@@ -28,6 +28,7 @@ export default {
controlSetRows: [
[mapboxStyle],
[viewport],
+ [autozoom],
[
{
name: 'deck_slices',