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 2026/07/03 15:43:51 UTC

(superset) branch legacy-viz-phase2-removal created (now 01ab3dd078b)

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

rusackas pushed a change to branch legacy-viz-phase2-removal
in repository https://gitbox.apache.org/repos/asf/superset.git


      at 01ab3dd078b chore(viz)!: remove explore_json endpoints, viz.py and the legacy chart data pipeline

This branch includes the following new commits:

     new 01ab3dd078b chore(viz)!: remove explore_json endpoints, viz.py and the legacy chart data pipeline

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 01/01: chore(viz)!: remove explore_json endpoints, viz.py and the legacy chart data pipeline

Posted by ru...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch legacy-viz-phase2-removal
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 01ab3dd078b9286db8c8f2d4021f6671c33961c4
Author: Claude Code <no...@anthropic.com>
AuthorDate: Fri Jul 3 08:41:59 2026 -0700

    chore(viz)!: remove explore_json endpoints, viz.py and the legacy chart data pipeline
    
    With every legacy chart migrated to /api/v1/chart/data, this removes
    the deprecated pipeline at its declared 5.0.0 EOL:
    
    Backend:
    - /superset/explore_json/ and /superset/explore_json/data/<cache_key>
      routes plus their viz-only helpers (generate_json, CSV/XLSX/query/
      results/samples responses) and the CSRF exemption
    - superset/viz.py and all remaining importers: the get_viz factory and
      explore-cache permission checkers in views/utils, the Slice.viz
      property and explore_json_url, the legacy warm-up-cache path, the
      legacy annotation-data path in the query context processor, and the
      BaseViz overload of security_manager.raise_for_access
    - the load_explore_json_into_cache celery task and the async query
      manager's explore_json job plumbing
    - the can_explore_json permission grant for the public role and the
      legacy-prefix redirect entries for the removed routes
    
    Frontend:
    - the useLegacyApi flag on ChartMetadata and every branch keyed on it:
      chartAction's legacy request/response handling, exportChart's legacy
      endpoint, ChartClient and StatefulChart explore_json endpoints,
      DrillBy legacy handling, and the explore panel's dataset-required
      alert for legacy charts on non-table datasources
    - getURIDirectory now always resolves to /explore/ and the legacy
      endpoint-type helpers are gone
    
    Tests covering the removed pipeline are removed or retargeted; the
    LegacyPrefixRedirectMiddleware itself remains for the other legacy
    prefixes.
    
    Co-Authored-By: Claude Fable 5 <no...@anthropic.com>
---
 .../src/chart/clients/ChartClient.ts               |   23 +-
 .../src/chart/components/StatefulChart.test.tsx    |    4 +-
 .../src/chart/components/StatefulChart.tsx         |   30 +-
 .../src/chart/models/ChartMetadata.ts              |    5 -
 .../test/chart/clients/ChartClient.test.ts         |   36 -
 .../src/vendor/superset/exploreUtils.ts            |   69 -
 .../preset-chart-deckgl/src/utils/explore.ts       |   68 -
 .../src/components/Chart/ChartRenderer.test.tsx    |    1 -
 .../src/components/Chart/DrillBy/DrillByModal.tsx  |   13 +-
 .../Chart/DrillBy/DrillBySubmenu.test.tsx          |    1 -
 .../src/components/Chart/chartAction.ts            |   89 +-
 .../src/components/Chart/chartActions.test.ts      |  222 +-
 .../ExploreChartPanel/ExploreChartPanel.test.tsx   |    1 -
 .../explore/components/ExploreChartPanel/index.tsx |   48 -
 .../ExploreViewContainer.test.tsx                  |    2 -
 .../src/explore/exploreUtils/exploreUtils.test.tsx |   57 +-
 .../src/explore/exploreUtils/exportChart.test.ts   |  133 -
 .../src/explore/exploreUtils/getExploreUrl.test.ts |   28 +-
 .../exploreUtils/getLegacyEndpointType.test.ts     |   40 -
 .../explore/exploreUtils/getURIDirectory.test.ts   |   14 +-
 .../src/explore/exploreUtils/index.ts              |   77 +-
 .../exploreUtils/shouldUseLegacyApi.test.ts        |   59 -
 superset/async_events/async_query_manager.py       |   30 +-
 superset/charts/api.py                             |    2 +-
 superset/commands/chart/warm_up_cache.py           |   33 +-
 superset/common/query_context_processor.py         |   25 -
 superset/config.py                                 |    1 -
 superset/datasets/api.py                           |    2 +-
 superset/mcp_service/utils/permissions_utils.py    |    2 +-
 superset/middleware/legacy_prefix_redirect.py      |    4 +-
 superset/models/slice.py                           |   18 +-
 superset/security/manager.py                       |    9 +-
 superset/tasks/async_queries.py                    |   81 +-
 superset/views/core.py                             |  258 +-
 superset/views/utils.py                            |  103 +-
 superset/viz.py                                    | 2742 --------------------
 tests/integration_tests/core_tests.py              |  103 +-
 tests/integration_tests/security_tests.py          |   22 +-
 .../integration_tests/tasks/async_queries_tests.py |  118 -
 tests/integration_tests/utils_tests.py             |   33 +-
 tests/integration_tests/viz_tests.py               | 2453 -----------------
 .../async_events/async_query_manager_tests.py      |   51 -
 .../commands/chart/warm_up_cache_test.py           |  342 +--
 .../middleware/test_legacy_prefix_redirect.py      |    8 -
 tests/unit_tests/tasks/test_async_queries.py       |  169 --
 tests/unit_tests/test_subdirectory_url_for.py      |   13 -
 tests/unit_tests/test_viz_cache_key.py             |  111 -
 tests/unit_tests/test_viz_get_df_payload.py        |  244 --
 48 files changed, 172 insertions(+), 7825 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-core/src/chart/clients/ChartClient.ts b/superset-frontend/packages/superset-ui-core/src/chart/clients/ChartClient.ts
index 397a854a2a5..52c8d76c6e4 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/clients/ChartClient.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/clients/ChartClient.ts
@@ -104,24 +104,15 @@ export default class ChartClient {
     const buildQueryRegistry = getChartBuildQueryRegistry();
 
     if (metaDataRegistry.has(visType)) {
-      const { useLegacyApi } = metaDataRegistry.get(visType)!;
       const buildQuery =
         (await buildQueryRegistry.get(visType)) ?? (() => formData);
-      const requestConfig: RequestConfig = useLegacyApi
-        ? {
-            endpoint: '/explore_json/',
-            postPayload: {
-              form_data: buildQuery(formData),
-            },
-            ...options,
-          }
-        : {
-            endpoint: '/api/v1/chart/data',
-            jsonPayload: {
-              query_context: buildQuery(formData),
-            },
-            ...options,
-          };
+      const requestConfig: RequestConfig = {
+        endpoint: '/api/v1/chart/data',
+        jsonPayload: {
+          query_context: buildQuery(formData),
+        },
+        ...options,
+      };
 
       return this.client
         .post(requestConfig)
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx
index 3d73326b45e..8cb4c2b2f25 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx
@@ -68,9 +68,7 @@ beforeEach(() => {
 
   // Setup default registry mocks
   (getChartMetadataRegistry as any).mockReturnValue({
-    get: jest.fn().mockReturnValue({
-      useLegacyApi: false,
-    }),
+    get: jest.fn().mockReturnValue({}),
   });
 
   (getChartBuildQueryRegistry as any).mockReturnValue({
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx
index baae1ae2ce4..9e77e75c33d 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx
@@ -245,9 +245,6 @@ export default function StatefulChart(props: StatefulChartProps) {
       }
       finalFormData.viz_type = vizType;
 
-      // Get chart metadata
-      const { useLegacyApi } = getChartMetadataRegistry().get(vizType) || {};
-
       // Build query using the chart's buildQuery function
       const buildQuery = await getChartBuildQueryRegistry().get(vizType);
       let queryContext;
@@ -259,39 +256,28 @@ export default function StatefulChart(props: StatefulChartProps) {
         queryContext = buildQueryContext(finalFormData);
       }
 
-      // Ensure query_context is properly formatted for new API
-      if (!useLegacyApi && !queryContext.queries) {
+      // Ensure query_context is properly formatted for the API
+      if (!queryContext.queries) {
         queryContext = { queries: [queryContext] };
       }
-      const endpoint = useLegacyApi ? '/explore_json/' : '/api/v1/chart/data';
 
       const requestConfig: RequestConfig = {
-        endpoint,
+        endpoint: '/api/v1/chart/data',
         signal: abortControllerRef.current.signal,
         ...(timeout && { timeout: timeout * 1000 }),
-      };
-
-      if (useLegacyApi) {
-        requestConfig.postPayload = {
-          form_data: {
-            ...finalFormData,
-            ...(force && { force: true }),
-          },
-        };
-      } else {
-        requestConfig.jsonPayload = {
+        jsonPayload: {
           ...queryContext,
           ...(force && { force: true }),
-        };
-      }
+        },
+      };
 
       const response = await chartClientRef.current!.client.post(requestConfig);
       let responseData = Array.isArray(response.json)
         ? response.json
         : [response.json];
 
-      // Handle the nested result structure from the new API
-      if (!useLegacyApi && responseData[0]?.result) {
+      // Handle the nested result structure from the API
+      if (responseData[0]?.result) {
         responseData = responseData[0].result;
       }
 
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts
index 3b5073f02b3..f8bd22d4fb2 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts
@@ -40,7 +40,6 @@ export interface ChartMetadataConfig {
   supportedAnnotationTypes?: string[];
   thumbnail: string;
   thumbnailDark?: string;
-  useLegacyApi?: boolean;
   behaviors?: Behavior[];
   exampleGallery?: ExampleImage[];
   tags?: string[];
@@ -75,8 +74,6 @@ export default class ChartMetadata {
 
   thumbnailDark?: string;
 
-  useLegacyApi: boolean;
-
   behaviors: Behavior[];
 
   datasourceCount: number;
@@ -112,7 +109,6 @@ export default class ChartMetadata {
       supportedAnnotationTypes = [],
       thumbnail,
       thumbnailDark,
-      useLegacyApi = false,
       behaviors = [],
       datasourceCount = 1,
       enableNoResults = true,
@@ -144,7 +140,6 @@ export default class ChartMetadata {
     this.supportedAnnotationTypes = supportedAnnotationTypes;
     this.thumbnail = thumbnail;
     this.thumbnailDark = thumbnailDark;
-    this.useLegacyApi = useLegacyApi;
     this.behaviors = behaviors;
     this.datasourceCount = datasourceCount;
     this.enableNoResults = enableNoResults;
diff --git a/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts b/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts
index 8d11b98d175..6ec2ef0f487 100644
--- a/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts
@@ -160,42 +160,6 @@ describe('ChartClient', () => {
           datasource: '1__table',
         }),
       ).rejects.toEqual(new Error('Unknown chart type: rainbow_3d_pie')));
-
-    test('fetches data from the legacy API if ChartMetadata has useLegacyApi=true,', () => {
-      // note legacy charts do not register a buildQuery function in the registry
-      getChartMetadataRegistry().registerValue(
-        'word_cloud_legacy',
-        new ChartMetadata({
-          name: 'Legacy Word Cloud',
-          thumbnail: '.png',
-          useLegacyApi: true,
-        }),
-      );
-
-      fetchMock.post('glob:*/api/v1/chart/data', () =>
-        Promise.reject(new Error('Unexpected all to v1 API')),
-      );
-
-      // post `Superset.route_base = ""`, the legacy endpoint
-      // collapsed from `/superset/explore_json/` to `/explore_json/`.
-      fetchMock.post('glob:*/explore_json/', {
-        field1: 'abc',
-        field2: 'def',
-      });
-
-      return expect(
-        chartClient.loadQueryData({
-          granularity: 'minute',
-          viz_type: 'word_cloud_legacy',
-          datasource: '1__table',
-        }),
-      ).resolves.toEqual([
-        {
-          field1: 'abc',
-          field2: 'def',
-        },
-      ]);
-    });
   });
 
   describe('.loadDatasource(datasourceKey, options)', () => {
diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/exploreUtils.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/exploreUtils.ts
deleted file mode 100644
index 098b3a73f2e..00000000000
--- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/exploreUtils.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * 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.
- */
-// @ts-nocheck -- vendor file; not fully typed
-/* eslint camelcase: 0 */
-import URI from 'urijs';
-import safeStringify from 'fast-safe-stringify';
-
-const MAX_URL_LENGTH = 8000;
-
-export function getURIDirectory(formData, endpointType = 'base') {
-  // Building the directory part of the URI
-  let directory = '/explore/';
-  if (['json', 'csv', 'query', 'results', 'samples'].includes(endpointType)) {
-    directory = '/superset/explore_json/';
-  }
-
-  return directory;
-}
-
-export function getExploreLongUrl(
-  formData,
-  endpointType,
-  allowOverflow = true,
-  extraSearch = {},
-) {
-  if (!formData.datasource) {
-    return null;
-  }
-
-  const uri = new URI('/');
-  const directory = getURIDirectory(formData, endpointType);
-  const search = uri.search(true);
-  Object.keys(extraSearch).forEach(key => {
-    search[key] = extraSearch[key];
-  });
-  search.form_data = safeStringify(formData);
-  if (endpointType === 'standalone') {
-    search.standalone = 'true';
-  }
-  const url = uri.directory(directory).search(search).toString();
-  if (!allowOverflow && url.length > MAX_URL_LENGTH) {
-    const minimalFormData = {
-      datasource: formData.datasource,
-      viz_type: formData.viz_type,
-    };
-
-    return getExploreLongUrl(minimalFormData, endpointType, false, {
-      URL_IS_TOO_LONG_TO_SHARE: null,
-    });
-  }
-
-  return url;
-}
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/utils/explore.ts b/superset-frontend/plugins/preset-chart-deckgl/src/utils/explore.ts
deleted file mode 100644
index 8b8ad24aabe..00000000000
--- a/superset-frontend/plugins/preset-chart-deckgl/src/utils/explore.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * 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 URI from 'urijs';
-import { JsonObject } from '@superset-ui/core';
-import { safeStringify } from './safeStringify';
-
-const MAX_URL_LENGTH = 8000;
-
-export function getURIDirectory(endpointType = 'base') {
-  // Building the directory part of the URI
-  let directory = '/explore/';
-  if (['json', 'csv', 'query', 'results', 'samples'].includes(endpointType)) {
-    directory = '/superset/explore_json/';
-  }
-
-  return directory;
-}
-
-export function getExploreLongUrl(
-  formData: JsonObject,
-  endpointType: string,
-  allowOverflow = true,
-  extraSearch: Record<string, any> = {},
-): string | undefined {
-  if (!formData.datasource) {
-    return undefined;
-  }
-
-  const uri = new URI('/');
-  const directory = getURIDirectory(endpointType);
-  const search = uri.search(true);
-  Object.keys(extraSearch).forEach(key => {
-    search[key] = extraSearch[key];
-  });
-  search.form_data = safeStringify(formData);
-  if (endpointType === 'standalone') {
-    search.standalone = 'true';
-  }
-  const url = uri.directory(directory).search(search).toString();
-  if (!allowOverflow && url.length > MAX_URL_LENGTH) {
-    const minimalFormData = {
-      datasource: formData.datasource,
-      viz_type: formData.viz_type,
-    };
-
-    return getExploreLongUrl(minimalFormData, endpointType, false, {
-      URL_IS_TOO_LONG_TO_SHARE: null,
-    });
-  }
-
-  return url;
-}
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx
index 92a2b8c52db..672a3995c7b 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx
@@ -138,7 +138,6 @@ test('should not render chart context menu if the context menu is suppressed for
     new ChartMetadata({
       name: 'chart with suppressed context menu',
       thumbnail: '.png',
-      useLegacyApi: false,
       suppressContextMenu: true,
     }),
   );
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
index 57149ee55e0..89773d23f4f 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx
@@ -54,7 +54,7 @@ import {
   LOG_ACTIONS_FURTHER_DRILL_BY,
 } from 'src/logger/LogUtils';
 import { findPermission } from 'src/utils/findPermission';
-import { getQuerySettings, exportChart } from 'src/explore/exploreUtils';
+import { exportChart } from 'src/explore/exploreUtils';
 import { isEmbedded } from 'src/dashboard/util/isEmbedded';
 import { Dataset, DrillByType } from '../types';
 import DrillByChart from './DrillByChart';
@@ -401,13 +401,11 @@ export default function DrillByModal({
   const handleReload = useCallback(() => {
     setChartDataResult(undefined);
     setIsChartDataLoading(true);
-    const [useLegacyApi] = getQuerySettings(drilledFormData);
+
     getChartDataRequest({
       formData: drilledFormData,
     })
-      .then(({ response, json }) =>
-        handleChartDataResponse(response, json, useLegacyApi),
-      )
+      .then(({ response, json }) => handleChartDataResponse(response, json))
       .then(queriesResponse => {
         setChartDataResult(queriesResponse);
       })
@@ -491,15 +489,12 @@ export default function DrillByModal({
 
   useEffect(() => {
     if (drilledFormData) {
-      const [useLegacyApi] = getQuerySettings(drilledFormData);
       setIsChartDataLoading(true);
       setChartDataResult(undefined);
       getChartDataRequest({
         formData: drilledFormData,
       })
-        .then(({ response, json }) =>
-          handleChartDataResponse(response, json, useLegacyApi),
-        )
+        .then(({ response, json }) => handleChartDataResponse(response, json))
         .then(queriesResponse => {
           setChartDataResult(queriesResponse);
         })
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx
index 4da5b103ad0..d36ab01e29a 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx
@@ -129,7 +129,6 @@ getChartMetadataRegistry().registerValue(
   new ChartMetadata({
     name: 'fake pie',
     thumbnail: '.png',
-    useLegacyApi: false,
     behaviors: [Behavior.DrillBy],
   }),
 );
diff --git a/superset-frontend/src/components/Chart/chartAction.ts b/superset-frontend/src/components/Chart/chartAction.ts
index 65b94f231b0..b7b260f1e5c 100644
--- a/superset-frontend/src/components/Chart/chartAction.ts
+++ b/superset-frontend/src/components/Chart/chartAction.ts
@@ -36,8 +36,6 @@ import type { ControlStateMapping } from '@superset-ui/chart-controls';
 import { getControlsState } from 'src/explore/store';
 import {
   getAnnotationJsonUrl,
-  getExploreUrl,
-  getLegacyEndpointType,
   buildV1ChartDataPayload,
   getQuerySettings,
   getChartDataUri,
@@ -405,51 +403,6 @@ export const dynamicPluginControlsReady =
     });
   };
 
-const legacyChartDataRequest = async (
-  formData: QueryFormData | LatestQueryFormData,
-  resultFormat: string,
-  resultType: string,
-  force: boolean,
-  method: 'GET' | 'POST' = 'POST',
-  requestParams: RequestParams = {},
-  parseMethod?: string,
-): Promise<ChartDataRequestResponse> => {
-  const endpointType = getLegacyEndpointType({ resultFormat, resultType });
-  const allowDomainSharding = Boolean(
-    // eslint-disable-next-line camelcase
-    domainShardingEnabled && requestParams?.dashboard_id,
-  );
-  const url = getExploreUrl({
-    formData: formData as QueryFormData & {
-      label_colors?: Record<string, string>;
-    },
-    endpointType,
-    force,
-    allowDomainSharding,
-    method,
-    requestParams: requestParams.dashboard_id
-      ? { dashboard_id: String(requestParams.dashboard_id) }
-      : {},
-  });
-  const querySettings: QuerySettings = {
-    ...requestParams,
-    url: url ?? undefined,
-    postPayload: { form_data: formData },
-    parseMethod,
-  };
-
-  return SupersetClient.post(
-    querySettings as Parameters<typeof SupersetClient.post>[0],
-  ).then(({ json, response }: { json: JsonObject; response: Response }) =>
-    // Make the legacy endpoint return a payload that corresponds to the
-    // V1 chart data endpoint response signature.
-    ({
-      response,
-      json: { result: [json] },
-    }),
-  );
-};
-
 const v1ChartDataRequest = async (
   formData: QueryFormData | LatestQueryFormData,
   resultFormat: string,
@@ -522,18 +475,7 @@ export async function getChartDataRequest({
       credentials: 'include',
     };
   }
-  const [useLegacyApi, parseMethod] = getQuerySettings(formData);
-  if (useLegacyApi) {
-    return legacyChartDataRequest(
-      formData,
-      resultFormat,
-      resultType,
-      force,
-      method,
-      querySettings,
-      parseMethod,
-    );
-  }
+  const [parseMethod] = getQuerySettings(formData);
   return v1ChartDataRequest(
     formData,
     resultFormat,
@@ -698,7 +640,6 @@ export function addChart(
 export function handleChartDataResponse(
   response: Response,
   json: { result: QueryData[] },
-  useLegacyApi?: boolean,
 ): Promise<QueryData[]> | QueryData[] {
   if (isFeatureEnabled(FeatureFlag.GlobalAsyncQueries)) {
     // deal with getChartDataRequest transforming the response data
@@ -711,11 +652,6 @@ export function handleChartDataResponse(
         // Query is running asynchronously and we must await the results.
         // When status is 202, result contains async event data (job_id, channel_id, etc.)
         // which differs from QueryData. We cast through unknown to handle this safely.
-        if (useLegacyApi) {
-          return waitForAsyncData(
-            result[0] as unknown as Parameters<typeof waitForAsyncData>[0],
-          ) as Promise<QueryData[]>;
-        }
         return waitForAsyncData(
           result as unknown as Parameters<typeof waitForAsyncData>[0],
         ) as Promise<QueryData[]>;
@@ -777,11 +713,8 @@ export function exploreJSON(
       ownState,
     });
 
-    const [useLegacyApi] = getQuerySettings(formData);
     const chartDataRequestCaught = chartDataRequest
-      .then(({ response, json }) =>
-        handleChartDataResponse(response, json, useLegacyApi),
-      )
+      .then(({ response, json }) => handleChartDataResponse(response, json))
       .then(queriesResponse => {
         // Drop stale responses: if a newer query has started for this chart,
         // its controller will have replaced ours in state, so ignore this
@@ -897,28 +830,10 @@ export function exploreJSON(
         },
       );
 
-    // only retrieve annotations when calling the legacy API
-    const annotationLayers: AnnotationLayer[] = useLegacyApi
-      ? (formData.annotation_layers as AnnotationLayer[]) || []
-      : [];
-    const isDashboardRequest = (dashboardId ?? 0) > 0;
-
     return Promise.all([
       chartDataRequestCaught,
       dispatch(triggerQuery(false, key as string | number)),
       dispatch(updateQueryFormData(formData, key as string | number)),
-      ...annotationLayers.map((annotation: AnnotationLayer) =>
-        dispatch(
-          runAnnotationQuery({
-            annotation,
-            timeout,
-            formData,
-            key,
-            isDashboardRequest,
-            force,
-          }),
-        ),
-      ),
     ]);
   };
 }
diff --git a/superset-frontend/src/components/Chart/chartActions.test.ts b/superset-frontend/src/components/Chart/chartActions.test.ts
index 30ba44359aa..e211eea72c4 100644
--- a/superset-frontend/src/components/Chart/chartActions.test.ts
+++ b/superset-frontend/src/components/Chart/chartActions.test.ts
@@ -32,7 +32,6 @@ import {
   AnnotationStyle,
 } from '@superset-ui/core';
 import * as toastActions from 'src/components/MessageToasts/actions';
-import { LOG_EVENT } from 'src/logger/actions';
 import * as exploreUtils from 'src/explore/exploreUtils';
 import * as actions from 'src/components/Chart/chartAction';
 import * as asyncEvent from 'src/middleware/asyncEvent';
@@ -100,7 +99,7 @@ describe('chart actions', () => {
   let getChartDataUriStub: jest.SpyInstance;
   let buildV1ChartDataPayloadStub: jest.SpyInstance;
   let waitForAsyncDataStub: jest.SpyInstance;
-  let fakeMetadata: { useLegacyApi?: boolean; viz_type?: string };
+  let fakeMetadata: { viz_type?: string };
 
   beforeAll(() => {
     fetchMock.get('glob:*api/v1/security/csrf_token/*', { result: '1234' });
@@ -135,7 +134,7 @@ describe('chart actions', () => {
       } as unknown as Awaited<
         ReturnType<typeof exploreUtils.buildV1ChartDataPayload>
       >);
-    fakeMetadata = { useLegacyApi: true };
+    fakeMetadata = {};
     mockedGetChartMetadataRegistry.mockImplementation(
       () =>
         ({
@@ -322,7 +321,7 @@ describe('chart actions', () => {
   // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
   describe('v1 API', () => {
     beforeEach(() => {
-      fakeMetadata = { viz_type: 'my_viz', useLegacyApi: false };
+      fakeMetadata = { viz_type: 'my_viz' };
     });
 
     test('should query with the built query', async () => {
@@ -465,221 +464,6 @@ describe('chart actions', () => {
     });
   });
 
-  // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
-  describe('legacy API', () => {
-    beforeEach(() => {
-      fakeMetadata = { useLegacyApi: true };
-    });
-
-    test('should dispatch CHART_UPDATE_STARTED action before the query', () => {
-      const actionThunk = actions.postChartFormData({
-        viz_type: 'table',
-      } as QueryFormData);
-
-      return actionThunk(
-        dispatch as unknown as actions.ChartThunkDispatch,
-        mockGetState as unknown as () => actions.RootState,
-        undefined,
-      ).then(() => {
-        // chart update, trigger query, update form data, success
-        expect(dispatch.mock.calls.length).toBe(5);
-        expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
-        expect(dispatch.mock.calls[0][0].type).toBe(
-          actions.CHART_UPDATE_STARTED,
-        );
-      });
-    });
-
-    test('should dispatch TRIGGER_QUERY action with the query', () => {
-      const actionThunk = actions.postChartFormData({
-        viz_type: 'table',
-      } as QueryFormData);
-      return actionThunk(
-        dispatch as unknown as actions.ChartThunkDispatch,
-        mockGetState as unknown as () => actions.RootState,
-        undefined,
-      ).then(() => {
-        // chart update, trigger query, update form data, success
-        expect(dispatch.mock.calls.length).toBe(5);
-        expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
-        expect(dispatch.mock.calls[1][0].type).toBe(actions.TRIGGER_QUERY);
-      });
-    });
-
-    test('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => {
-      const actionThunk = actions.postChartFormData({
-        viz_type: 'table',
-      } as QueryFormData);
-      return actionThunk(
-        dispatch as unknown as actions.ChartThunkDispatch,
-        mockGetState as unknown as () => actions.RootState,
-        undefined,
-      ).then(() => {
-        // chart update, trigger query, update form data, success
-        expect(dispatch.mock.calls.length).toBe(5);
-        expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
-        expect(dispatch.mock.calls[2][0].type).toBe(
-          actions.UPDATE_QUERY_FORM_DATA,
-        );
-      });
-    });
-
-    test('should dispatch logEvent async action', () => {
-      const actionThunk = actions.postChartFormData({
-        viz_type: 'table',
-      } as QueryFormData);
-      return actionThunk(
-        dispatch as unknown as actions.ChartThunkDispatch,
-        mockGetState as unknown as () => actions.RootState,
-        undefined,
-      ).then(() => {
-        // chart update, trigger query, update form data, success
-        expect(dispatch.mock.calls.length).toBe(5);
-        expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
-        expect(typeof dispatch.mock.calls[3][0]).toBe('function');
-
-        dispatch.mock.calls[3][0](dispatch);
-        expect(dispatch.mock.calls.length).toBe(6);
-        expect(dispatch.mock.calls[5][0].type).toBe(LOG_EVENT);
-      });
-    });
-
-    test('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
-      // Pass a viz_type so getQuerySettings returns useLegacyApi from the mocked registry
-      const actionThunk = actions.postChartFormData({
-        viz_type: 'table',
-      } as QueryFormData);
-      return actionThunk(
-        dispatch as unknown as actions.ChartThunkDispatch,
-        mockGetState as unknown as () => actions.RootState,
-        undefined,
-      ).then(() => {
-        // chart update, trigger query, update form data, success
-        expect(dispatch.mock.calls.length).toBe(5);
-        expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
-        expect(dispatch.mock.calls[4][0].type).toBe(
-          actions.CHART_UPDATE_SUCCEEDED,
-        );
-      });
-    });
-
-    test('should dispatch CHART_UPDATE_FAILED action upon query timeout', () => {
-      const unresolvingPromise = new Promise(() => {});
-      fetchMock.removeRoute(MOCK_URL);
-      fetchMock.post(MOCK_URL, () => unresolvingPromise, {
-        name: MOCK_URL,
-      });
-
-      const timeoutInSec = 1 / 1000;
-      const actionThunk = actions.postChartFormData(
-        {} as QueryFormData,
-        false,
-        timeoutInSec,
-      );
-
-      return actionThunk(
-        dispatch as unknown as actions.ChartThunkDispatch,
-        mockGetState as unknown as () => actions.RootState,
-        undefined,
-      ).then(() => {
-        // chart update, trigger query, update form data, fail
-        expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1);
-        expect(dispatch.mock.calls.length).toBe(5);
-        expect(dispatch.mock.calls[4][0].type).toBe(
-          actions.CHART_UPDATE_FAILED,
-        );
-
-        fetchMock.removeRoute(MOCK_URL);
-        setupDefaultFetchMock();
-      });
-    });
-
-    test('should dispatch CHART_UPDATE_FAILED action upon non-timeout non-abort failure', () => {
-      fetchMock.removeRoute(MOCK_URL);
-      fetchMock.post(
-        MOCK_URL,
-        { throws: { statusText: 'misc error' } },
-        { name: MOCK_URL },
-      );
-
-      const timeoutInSec = 100; // Set to a time that is longer than the time this will take to fail
-      const actionThunk = actions.postChartFormData(
-        {} as QueryFormData,
-        false,
-        timeoutInSec,
-      );
-
-      return actionThunk(
-        dispatch as unknown as actions.ChartThunkDispatch,
-        mockGetState as unknown as () => actions.RootState,
-        undefined,
-      ).then(() => {
-        // chart update, trigger query, update form data, fail
-        expect(dispatch.mock.calls.length).toBe(5);
-        const updateFailedAction = dispatch.mock.calls[4][0];
-        expect(updateFailedAction.type).toBe(actions.CHART_UPDATE_FAILED);
-        expect(updateFailedAction.queriesResponse[0].error).toBe('misc error');
-
-        fetchMock.removeRoute(MOCK_URL);
-        setupDefaultFetchMock();
-      });
-    });
-
-    test('should dispatch CHART_UPDATE_STOPPED action upon abort', () => {
-      fetchMock.removeRoute(MOCK_URL);
-      fetchMock.post(
-        MOCK_URL,
-        { throws: { name: 'AbortError' } },
-        { name: MOCK_URL },
-      );
-
-      const timeoutInSec = 100;
-      const actionThunk = actions.postChartFormData(
-        {} as QueryFormData,
-        false,
-        timeoutInSec,
-      );
-
-      return actionThunk(
-        dispatch as unknown as actions.ChartThunkDispatch,
-        mockGetState as unknown as () => actions.RootState,
-        undefined,
-      ).then(() => {
-        const types = dispatch.mock.calls
-          .map((call: [{ type?: string }]) => call[0] && call[0].type)
-          .filter(Boolean);
-
-        expect(types).toContain(actions.CHART_UPDATE_STOPPED);
-        expect(types).not.toContain(actions.CHART_UPDATE_FAILED);
-
-        fetchMock.removeRoutes();
-        setupDefaultFetchMock();
-      });
-    });
-
-    test('should handle the bigint without regression', async () => {
-      getExploreUrlStub.mockRestore();
-      const mockBigIntUrl = '/mock/chart/data/bigint';
-      const expectedBigNumber = '9223372036854775807';
-      fetchMock.post(mockBigIntUrl, `{ "value": ${expectedBigNumber} }`, {
-        name: mockBigIntUrl,
-      });
-      getExploreUrlStub = jest
-        .spyOn(exploreUtils, 'getExploreUrl')
-        .mockImplementation(() => mockBigIntUrl);
-
-      // Need viz_type to trigger the mocked getChartMetadataRegistry for legacy API
-      const { json } = await actions.getChartDataRequest({
-        formData: { ...fakeMetadata, viz_type: 'table' } as QueryFormData,
-      });
-
-      expect(fetchMock.callHistory.calls(mockBigIntUrl)).toHaveLength(1);
-      expect((json.result[0] as JsonObject).value.toString()).toEqual(
-        expectedBigNumber,
-      );
-    });
-  });
-
   // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
   describe('runAnnotationQuery', () => {
     const mockDispatch = jest.fn();
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx b/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx
index e0375f9d67f..ab9afb3beab 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx
@@ -90,7 +90,6 @@ describe('ChartContainer', () => {
       new ChartMetadata({
         name: 'fake table',
         thumbnail: '.png',
-        useLegacyApi: false,
       }),
     );
     render(<ChartContainer {...props} />, { useRedux: true });
diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
index 55a3b7647cf..11ae06ae86e 100644
--- a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx
@@ -21,17 +21,14 @@ import { useDispatch, useSelector } from 'react-redux';
 import Split from 'react-split';
 import { t } from '@apache-superset/core/translation';
 import {
-  DatasourceType,
   ensureIsArray,
   isFeatureEnabled,
   FeatureFlag,
-  getChartMetadataRegistry,
   SupersetClient,
   QueryFormData,
   JsonObject,
   getExtensionsRegistry,
 } from '@superset-ui/core';
-import { Alert } from '@apache-superset/core/components';
 import { css, styled, useTheme } from '@apache-superset/core/theme';
 import ChartContainer from 'src/components/Chart/ChartContainer';
 import { updateExploreChartState } from 'src/explore/actions/exploreActions';
@@ -44,8 +41,6 @@ import {
   setItem,
   LocalStorageKeys,
 } from 'src/utils/localStorageHelpers';
-import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
-import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
 import { buildV1ChartDataPayload } from 'src/explore/exploreUtils';
 import { getChartRequiredFieldsMissingMessage } from 'src/utils/getChartRequiredFieldsMissingMessage';
 import type { ChartState, Datasource } from 'src/explore/types';
@@ -223,17 +218,10 @@ const ExploreChartPanel = ({
       : getItem(LocalStorageKeys.IsDatapanelOpen, false),
   );
 
-  const [showDatasetModal, setShowDatasetModal] = useState(false);
-
-  const metaDataRegistry = getChartMetadataRegistry();
-  const { useLegacyApi } = metaDataRegistry.get(vizType) ?? {};
-  const vizTypeNeedsDataset =
-    useLegacyApi && datasource.type !== DatasourceType.Table;
   // added boolean column to below show boolean so that the errors aren't overlapping
   const showAlertBanner =
     !chartAlert &&
     chartIsStale &&
-    !vizTypeNeedsDataset &&
     chart.chartStatus !== 'failed' &&
     ensureIsArray(chart.queriesResponse).length > 0;
 
@@ -380,31 +368,6 @@ const ExploreChartPanel = ({
           padding-top: ${theme.sizeUnit * 2}px;
         `}
       >
-        {vizTypeNeedsDataset && (
-          <Alert
-            message={t('Chart type requires a dataset')}
-            type="error"
-            css={theme => css`
-              margin: 0 0 ${theme.sizeUnit * 4}px 0;
-            `}
-            description={
-              <>
-                {t(
-                  'This chart type is not supported when using an unsaved query as a chart source. ',
-                )}
-                <span
-                  role="button"
-                  tabIndex={0}
-                  onClick={() => setShowDatasetModal(true)}
-                  css={{ textDecoration: 'underline' }}
-                >
-                  {t('Create a dataset')}
-                </span>
-                {t(' to visualize your data.')}
-              </>
-            }
-          />
-        )}
         {showAlertBanner && (
           <ExploreAlert
             title={
@@ -562,17 +525,6 @@ const ExploreChartPanel = ({
           canDownload={canDownload}
         />
       </Split>
-      {showDatasetModal && (
-        <SaveDatasetModal
-          visible={showDatasetModal}
-          onHide={() => setShowDatasetModal(false)}
-          buttonTextOnSave={t('Save')}
-          buttonTextOnOverwrite={t('Overwrite')}
-          datasource={getDatasourceAsSaveableDataset(datasource)}
-          openWindow={false}
-          formData={formData}
-        />
-      )}
     </Styles>
   );
 };
diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx
index 4ad5d3aed65..5a49ff50126 100644
--- a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx
+++ b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx
@@ -163,7 +163,6 @@ test('generates a new form_data param when none is available', async () => {
     new ChartMetadata({
       name: 'fake table',
       thumbnail: '.png',
-      useLegacyApi: false,
     }),
   );
   const history = createMemoryHistory({ initialEntries: [defaultPath] });
@@ -226,7 +225,6 @@ test('doesnt call replace when pathname is not /explore', async () => {
     new ChartMetadata({
       name: 'fake table',
       thumbnail: '.png',
-      useLegacyApi: false,
     }),
   );
   const history = createMemoryHistory({ initialEntries: ['/dashboard'] });
diff --git a/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx b/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx
index 4625fff6c89..478eba7ace4 100644
--- a/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx
+++ b/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx
@@ -66,7 +66,7 @@ describe('exploreUtils', () => {
         force: false,
         curUrl: 'http://superset.com',
       });
-      compareURI(URI(url!), URI('/explore_json/'));
+      compareURI(URI(url!), URI('/explore/'));
     });
     test('generates proper json forced url', () => {
       const url = getExploreUrl({
@@ -75,16 +75,7 @@ describe('exploreUtils', () => {
         force: true,
         curUrl: 'superset.com',
       });
-      compareURI(URI(url!), URI('/explore_json/').search({ force: 'true' }));
-    });
-    test('generates proper csv URL', () => {
-      const url = getExploreUrl({
-        formData,
-        endpointType: 'csv',
-        force: false,
-        curUrl: 'superset.com',
-      });
-      compareURI(URI(url!), URI('/explore_json/').search({ csv: 'true' }));
+      compareURI(URI(url!), URI('/explore/').search({ force: 'true' }));
     });
     test('generates proper standalone URL', () => {
       const url = getExploreUrl({
@@ -107,7 +98,7 @@ describe('exploreUtils', () => {
         force: false,
         curUrl: 'superset.com?foo=bar',
       });
-      compareURI(URI(url!), URI('/explore_json/').search({ foo: 'bar' }));
+      compareURI(URI(url!), URI('/explore/').search({ foo: 'bar' }));
     });
     test('generate proper save slice url', () => {
       const url = getExploreUrl({
@@ -116,7 +107,7 @@ describe('exploreUtils', () => {
         force: false,
         curUrl: 'superset.com?foo=bar',
       });
-      compareURI(URI(url!), URI('/explore_json/').search({ foo: 'bar' }));
+      compareURI(URI(url!), URI('/explore/').search({ foo: 'bar' }));
     });
   });
 
@@ -202,49 +193,33 @@ describe('exploreUtils', () => {
   // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
   describe('getQuerySettings', () => {
     beforeAll(() => {
-      getChartMetadataRegistry()
-        .registerValue('my_legacy_viz', {
-          useLegacyApi: true,
-        } as unknown as ChartMetadata)
-        .registerValue('my_v1_viz', {
-          useLegacyApi: false,
-        } as unknown as ChartMetadata);
+      getChartMetadataRegistry().registerValue('my_custom_parse_viz', {
+        parseMethod: 'json',
+      } as unknown as ChartMetadata);
     });
 
     afterAll(() => {
-      getChartMetadataRegistry().remove('my_legacy_viz').remove('my_v1_viz');
+      getChartMetadataRegistry().remove('my_custom_parse_viz');
     });
 
-    test('returns true for legacy viz', () => {
-      const [useLegacyApi, parseMethod] = getQuerySettings({
+    test('returns the parse method for a registered viz', () => {
+      const [parseMethod] = getQuerySettings({
         ...formData,
-        viz_type: 'my_legacy_viz',
+        viz_type: 'my_custom_parse_viz',
       });
-      expect(useLegacyApi).toBe(true);
-      expect(parseMethod).toBe('json-bigint');
-    });
-
-    test('returns false for v1 viz', () => {
-      const [useLegacyApi, parseMethod] = getQuerySettings({
-        ...formData,
-        viz_type: 'my_v1_viz',
-      });
-      expect(useLegacyApi).toBe(false);
-      expect(parseMethod).toBe('json-bigint');
+      expect(parseMethod).toBe('json');
     });
 
-    test('returns false for formData with unregistered viz_type', () => {
-      const [useLegacyApi, parseMethod] = getQuerySettings({
+    test('defaults the parse method for unregistered viz types', () => {
+      const [parseMethod] = getQuerySettings({
         ...formData,
         viz_type: 'undefined_viz',
       });
-      expect(useLegacyApi).toBe(false);
       expect(parseMethod).toBe('json-bigint');
     });
 
-    test('returns false for formData without viz_type', () => {
-      const [useLegacyApi, parseMethod] = getQuerySettings(formData);
-      expect(useLegacyApi).toBe(false);
+    test('defaults the parse method without a viz_type', () => {
+      const [parseMethod] = getQuerySettings(formData);
       expect(parseMethod).toBe('json-bigint');
     });
   });
diff --git a/superset-frontend/src/explore/exploreUtils/exportChart.test.ts b/superset-frontend/src/explore/exploreUtils/exportChart.test.ts
index 3311e92c761..d939b043f60 100644
--- a/superset-frontend/src/explore/exploreUtils/exportChart.test.ts
+++ b/superset-frontend/src/explore/exploreUtils/exportChart.test.ts
@@ -56,7 +56,6 @@ const createMockExportResponse = (headers: Headers = new Headers()) => ({
   blob: jest.fn().mockResolvedValue(mockBlob),
 });
 
-// Minimal formData that won't trigger legacy API (useLegacyApi = false)
 const baseFormData = {
   datasource: '1__table',
   viz_type: 'table',
@@ -179,138 +178,6 @@ test('exportChart passes xlsx exportType for Excel exports', async () => {
   );
 });
 
-test('exportChart legacy API (useLegacyApi=true) passes prefixed URL to onStartStreamingExport when app root is configured', async () => {
-  const appRoot = '/superset';
-  ensureAppRoot.mockImplementation((path: string) => `${appRoot}${path}`);
-
-  getChartMetadataRegistry.mockReturnValue({
-    get: jest.fn().mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }),
-  });
-
-  const onStartStreamingExport = jest.fn();
-  const legacyFormData = {
-    datasource: '1__table',
-    viz_type: 'legacy_viz',
-  };
-
-  await exportChart({
-    formData: legacyFormData,
-    resultFormat: 'csv',
-    onStartStreamingExport: onStartStreamingExport as unknown as null,
-  });
-
-  expect(onStartStreamingExport).toHaveBeenCalledTimes(1);
-  const callArgs = onStartStreamingExport.mock.calls[0][0];
-  // post `Superset.route_base = ""`, the blueprint path is
-  // `/explore_json/`. With appRoot=/superset, the prefixed URL is
-  // `/superset/explore_json/?csv=true` β€” single-prefix, not the legacy
-  // doubled `/superset/superset/explore_json/...` that this test used to
-  // pin (which was the bug, now fixed at source).
-  expect(callArgs.url).toBe('/superset/explore_json/?csv=true');
-  expect(callArgs.exportType).toBe('csv');
-});
-
-test('exportChart legacy API builds relative URL for CSV export without app root', async () => {
-  ensureAppRoot.mockImplementation((path: string) => path);
-
-  getChartMetadataRegistry.mockReturnValue({
-    get: jest.fn().mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }),
-  });
-
-  const onStartStreamingExport = jest.fn();
-  const legacyFormData = {
-    datasource: '1__table',
-    viz_type: 'world_map',
-  };
-
-  await exportChart({
-    formData: legacyFormData,
-    resultFormat: 'csv',
-    onStartStreamingExport: onStartStreamingExport as unknown as null,
-  });
-
-  expect(onStartStreamingExport).toHaveBeenCalledTimes(1);
-  const callArgs = onStartStreamingExport.mock.calls[0][0];
-  expect(callArgs.url).toBe('/explore_json/?csv=true');
-});
-
-test('exportChart legacy API builds relative URL for xlsx export', async () => {
-  ensureAppRoot.mockImplementation((path: string) => path);
-
-  getChartMetadataRegistry.mockReturnValue({
-    get: jest.fn().mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }),
-  });
-
-  const onStartStreamingExport = jest.fn();
-  const legacyFormData = {
-    datasource: '1__table',
-    viz_type: 'bubble',
-  };
-
-  await exportChart({
-    formData: legacyFormData,
-    resultFormat: 'xlsx',
-    resultType: 'results',
-    onStartStreamingExport: onStartStreamingExport as unknown as null,
-  });
-
-  expect(onStartStreamingExport).toHaveBeenCalledTimes(1);
-  const callArgs = onStartStreamingExport.mock.calls[0][0];
-  expect(callArgs.url).toBe('/explore_json/?xlsx=true');
-});
-
-test('exportChart legacy API calls postBlob with relative URL', async () => {
-  const { SupersetClient } = jest.requireMock('@superset-ui/core');
-  ensureAppRoot.mockImplementation((path: string) => path);
-  SupersetClient.postBlob.mockResolvedValue(createMockExportResponse());
-
-  getChartMetadataRegistry.mockReturnValue({
-    get: jest.fn().mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }),
-  });
-
-  const legacyFormData = {
-    datasource: '1__table',
-    viz_type: 'world_map',
-  };
-
-  await exportChart({
-    formData: legacyFormData,
-    resultFormat: 'csv',
-    resultType: 'full',
-  });
-
-  expect(SupersetClient.postBlob).toHaveBeenCalledTimes(1);
-  const [url] = SupersetClient.postBlob.mock.calls[0];
-  expect(url).toBe('/explore_json/?csv=true');
-  expect(url).not.toMatch(/^https?:\/\//);
-  expect(downloadBlob).toHaveBeenCalled();
-});
-
-test('exportChart legacy API includes force param when force=true', async () => {
-  ensureAppRoot.mockImplementation((path: string) => path);
-
-  getChartMetadataRegistry.mockReturnValue({
-    get: jest.fn().mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }),
-  });
-
-  const onStartStreamingExport = jest.fn();
-  const legacyFormData = {
-    datasource: '1__table',
-    viz_type: 'world_map',
-  };
-
-  await exportChart({
-    formData: legacyFormData,
-    resultFormat: 'csv',
-    force: true,
-    onStartStreamingExport: onStartStreamingExport as unknown as null,
-  });
-
-  expect(onStartStreamingExport).toHaveBeenCalledTimes(1);
-  const callArgs = onStartStreamingExport.mock.calls[0][0];
-  expect(callArgs.url).toBe('/explore_json/?force=true&csv=true');
-});
-
 test('exportChart successfully exports chart as CSV', async () => {
   const { SupersetClient } = jest.requireMock('@superset-ui/core');
   const mockResponse = createMockExportResponse();
diff --git a/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts b/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts
index 782d4412863..de1f7a38c70 100644
--- a/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts
+++ b/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts
@@ -36,44 +36,22 @@ test('Get ExploreUrl with default params', () => {
   expect(getExploreUrl(params)).toBe('http://localhost/explore/');
 });
 
-test('Get ExploreUrl with endpointType:full', () => {
+test('Get ExploreUrl for any endpoint type resolves to the explore page', () => {
   const params = createParams();
   expect(getExploreUrl({ ...params, endpointType: 'full' })).toBe(
-    'http://localhost/explore_json/',
+    'http://localhost/explore/',
   );
 });
 
-test('Get ExploreUrl with endpointType:full and method:GET', () => {
-  const params = createParams();
-  expect(
-    getExploreUrl({ ...params, endpointType: 'full', method: 'GET' }),
-  ).toBe('http://localhost/explore_json/');
-});
-
-test('Get relative ExploreUrl with endpointType:csv', () => {
-  const params = createParams();
-  expect(
-    getExploreUrl({ ...params, endpointType: 'csv', relative: true }),
-  ).toBe('/explore_json/?csv=true');
-});
-
-test('Get relative ExploreUrl with endpointType:xlsx', () => {
-  const params = createParams();
-  expect(
-    getExploreUrl({ ...params, endpointType: 'xlsx', relative: true }),
-  ).toBe('/explore_json/?xlsx=true');
-});
-
 test('Get relative ExploreUrl with force:true', () => {
   const params = createParams();
   expect(
     getExploreUrl({
       ...params,
-      endpointType: 'csv',
       force: true,
       relative: true,
     }),
-  ).toBe('/explore_json/?force=true&csv=true');
+  ).toBe('/explore/?force=true');
 });
 
 test('Get relative ExploreUrl with endpointType:base', () => {
diff --git a/superset-frontend/src/explore/exploreUtils/getLegacyEndpointType.test.ts b/superset-frontend/src/explore/exploreUtils/getLegacyEndpointType.test.ts
deleted file mode 100644
index eca145a680d..00000000000
--- a/superset-frontend/src/explore/exploreUtils/getLegacyEndpointType.test.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * 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 { getLegacyEndpointType } from '.';
-
-const createParams = () => ({
-  resultType: 'resultType',
-  resultFormat: 'resultFormat',
-});
-
-test('Should return resultType when resultFormat!=csv', () => {
-  expect(getLegacyEndpointType(createParams())).toBe('resultType');
-});
-
-test('Should return resultFormat when resultFormat:csv', () => {
-  expect(
-    getLegacyEndpointType({ ...createParams(), resultFormat: 'csv' }),
-  ).toBe('csv');
-});
-
-test('Should return resultFormat when resultFormat:xlsx', () => {
-  expect(
-    getLegacyEndpointType({ ...createParams(), resultFormat: 'xlsx' }),
-  ).toBe('xlsx');
-});
diff --git a/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts b/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts
index 7d3b004cd4d..c9789c8d9da 100644
--- a/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts
+++ b/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts
@@ -18,17 +18,9 @@
  */
 import { getURIDirectory } from '.';
 
-test('Cases in which the "explore_json" will be returned', () => {
-  // post `Superset.route_base = ""` collapse the legacy
-  // `/superset/explore_json/` endpoint is gone; `getURIDirectory` now
-  // returns the bare `/explore_json/` path (appRoot is applied at the
-  // outer `ensureAppRoot` layer when `includeAppRoot=true`).
-  ['full', 'json', 'csv', 'query', 'results', 'samples'].forEach(name => {
-    expect(getURIDirectory(name)).toBe('/explore_json/');
+test('always returns the explore directory', () => {
+  ['full', 'json', 'any-string'].forEach(name => {
+    expect(getURIDirectory(name)).toBe('/explore/');
   });
-});
-
-test('Cases in which the "explore" will be returned', () => {
-  expect(getURIDirectory('any-string')).toBe('/explore/');
   expect(getURIDirectory()).toBe('/explore/');
 });
diff --git a/superset-frontend/src/explore/exploreUtils/index.ts b/superset-frontend/src/explore/exploreUtils/index.ts
index 3e1cd406fbe..f4fc374c143 100644
--- a/superset-frontend/src/explore/exploreUtils/index.ts
+++ b/superset-frontend/src/explore/exploreUtils/index.ts
@@ -156,19 +156,7 @@ export function getURIDirectory(
   endpointType: EndpointType | string = 'base',
   includeAppRoot = true,
 ): string {
-  // Building the directory part of the URI
-  const uri = [
-    'full',
-    'json',
-    'csv',
-    'xlsx',
-    'query',
-    'results',
-    'samples',
-  ].includes(endpointType)
-    ? '/explore_json/'
-    : '/explore/';
-  return includeAppRoot ? ensureAppRoot(uri) : uri;
+  return includeAppRoot ? ensureAppRoot('/explore/') : '/explore/';
 }
 
 export function mountExploreUrl(
@@ -267,24 +255,9 @@ export function getExploreUrl({
   if (force) {
     search.force = 'true';
   }
-  if (endpointType === 'csv') {
-    search.csv = 'true';
-  }
-  if (endpointType === 'xlsx') {
-    search.xlsx = 'true';
-  }
   if (endpointType === URL_PARAMS.standalone.name) {
     search.standalone = '1';
   }
-  if (endpointType === 'query') {
-    search.query = 'true';
-  }
-  if (endpointType === 'results') {
-    search.results = 'true';
-  }
-  if (endpointType === 'samples') {
-    search.samples = 'true';
-  }
   const paramNames = Object.keys(requestParams);
   if (paramNames.length) {
     paramNames.forEach(name => {
@@ -298,14 +271,11 @@ export function getExploreUrl({
 
 export const getQuerySettings = (
   formData: Partial<QueryFormData>,
-): [boolean, string] => {
+): [string] => {
   const vizMetadata = formData.viz_type
     ? getChartMetadataRegistry().get(formData.viz_type)
     : undefined;
-  return [
-    vizMetadata?.useLegacyApi ?? false,
-    vizMetadata?.parseMethod ?? 'json-bigint',
-  ];
+  return [vizMetadata?.parseMethod ?? 'json-bigint'];
 };
 
 export const buildV1ChartDataPayload = async ({
@@ -346,15 +316,6 @@ export const buildV1ChartDataPayload = async ({
   );
 };
 
-export const getLegacyEndpointType = ({
-  resultType,
-  resultFormat,
-}: {
-  resultType: string;
-  resultFormat: string;
-}): string =>
-  resultFormat === 'csv' || resultFormat === 'xlsx' ? resultFormat : resultType;
-
 export const exportChart = async ({
   formData,
   resultFormat = 'json',
@@ -363,30 +324,14 @@ export const exportChart = async ({
   ownState = {},
   onStartStreamingExport = null,
 }: ExportChartParams): Promise<void> => {
-  let url: string | null;
-  let payload: QueryFormData | ReturnType<typeof buildQueryContext>;
-  const [useLegacyApi] = getQuerySettings(formData);
-  if (useLegacyApi) {
-    const endpointType = getLegacyEndpointType({ resultFormat, resultType });
-    url = getExploreUrl({
-      formData,
-      endpointType,
-      force,
-      allowDomainSharding: false,
-      relative: true,
-      includeAppRoot: false,
-    });
-    payload = formData;
-  } else {
-    url = '/api/v1/chart/data';
-    payload = await buildV1ChartDataPayload({
-      formData,
-      force,
-      resultFormat,
-      resultType,
-      ownState,
-    });
-  }
+  const url = '/api/v1/chart/data';
+  const payload = await buildV1ChartDataPayload({
+    formData,
+    force,
+    resultFormat,
+    resultType,
+    ownState,
+  });
 
   // Check if streaming export handler is provided (from dashboard Chart.jsx)
   if (onStartStreamingExport) {
diff --git a/superset-frontend/src/explore/exploreUtils/shouldUseLegacyApi.test.ts b/superset-frontend/src/explore/exploreUtils/shouldUseLegacyApi.test.ts
deleted file mode 100644
index caeeae68787..00000000000
--- a/superset-frontend/src/explore/exploreUtils/shouldUseLegacyApi.test.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * 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 { getChartMetadataRegistry } from '@superset-ui/core';
-import { getQuerySettings } from '.';
-
-jest.mock('@superset-ui/core', () => ({
-  ...jest.requireActual('@superset-ui/core'),
-  getChartMetadataRegistry: jest.fn(),
-}));
-
-const mockedGetChartMetadataRegistry = getChartMetadataRegistry as jest.Mock;
-
-test('Should return false', () => {
-  const get = jest.fn();
-  mockedGetChartMetadataRegistry.mockReturnValue({ get } as any);
-  expect(get).toHaveBeenCalledTimes(0);
-  const [useLegacyApi] = getQuerySettings({ viz_type: 'name_test' });
-  expect(useLegacyApi).toBe(false);
-  expect(get).toHaveBeenCalledTimes(1);
-  expect(get).toHaveBeenCalledWith('name_test');
-});
-
-test('Should return true', () => {
-  const get = jest.fn();
-  get.mockReturnValue({ useLegacyApi: true });
-  mockedGetChartMetadataRegistry.mockReturnValue({ get } as any);
-  expect(get).toHaveBeenCalledTimes(0);
-  const [useLegacyApi] = getQuerySettings({ viz_type: 'name_test' });
-  expect(useLegacyApi).toBe(true);
-  expect(get).toHaveBeenCalledTimes(1);
-  expect(get).toHaveBeenCalledWith('name_test');
-});
-
-test('Should return false when useLegacyApi:false', () => {
-  const get = jest.fn();
-  get.mockReturnValue({ useLegacyApi: false });
-  mockedGetChartMetadataRegistry.mockReturnValue({ get } as any);
-  expect(get).toHaveBeenCalledTimes(0);
-  const [useLegacyApi] = getQuerySettings({ viz_type: 'name_test' });
-  expect(useLegacyApi).toBe(false);
-  expect(get).toHaveBeenCalledTimes(1);
-  expect(get).toHaveBeenCalledWith('name_test');
-});
diff --git a/superset/async_events/async_query_manager.py b/superset/async_events/async_query_manager.py
index 4a8c840e413..0ed0292b2aa 100644
--- a/superset/async_events/async_query_manager.py
+++ b/superset/async_events/async_query_manager.py
@@ -121,7 +121,6 @@ class AsyncQueryManager:
         self._jwt_expiration_seconds: int = 0
         self._load_chart_data_into_cache_job: Any = None
         # pylint: disable=invalid-name
-        self._load_explore_json_into_cache_job: Any = None
 
     def init_app(self, app: Flask) -> None:
         cache_type = app.config.get("CACHE_CONFIG", {}).get("CACHE_TYPE")
@@ -162,13 +161,9 @@ class AsyncQueryManager:
             self.register_request_handlers(app)
 
         # pylint: disable=import-outside-toplevel
-        from superset.tasks.async_queries import (
-            load_chart_data_into_cache,
-            load_explore_json_into_cache,
-        )
+        from superset.tasks.async_queries import load_chart_data_into_cache
 
         self._load_chart_data_into_cache_job = load_chart_data_into_cache
-        self._load_explore_json_into_cache_job = load_explore_json_into_cache
 
     def register_request_handlers(self, app: Flask) -> None:
         @app.after_request
@@ -280,29 +275,6 @@ class AsyncQueryManager:
             channel_id, job_id, user_id, status=self.STATUS_PENDING
         )
 
-    # pylint: disable=too-many-arguments
-    def submit_explore_json_job(
-        self,
-        channel_id: str,
-        form_data: dict[str, Any],
-        response_type: str,
-        force: Optional[bool] = False,
-        user_id: Optional[int] = None,
-    ) -> dict[str, Any]:
-        # pylint: disable=import-outside-toplevel
-        from superset import security_manager
-
-        job_metadata = self.init_job(channel_id, user_id)
-        self._load_explore_json_into_cache_job.delay(
-            {**job_metadata, "guest_token": guest_user.guest_token}
-            if (guest_user := security_manager.get_current_guest_user_if_guest())
-            else job_metadata,
-            form_data,
-            response_type,
-            force,
-        )
-        return job_metadata
-
     def submit_chart_data_job(
         self,
         channel_id: str,
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 0098996a502..5fdf0adb745 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -1055,7 +1055,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
             Warms up the cache for the chart.
             Note for slices a force refresh occurs.
             In terms of the `extra_filters` these can be obtained from records in the JSON
-            encoded `logs.json` column associated with the `explore_json` action.
+            encoded `logs.json` column associated with the `explore` action.
           requestBody:
             description: >-
               Identifies the chart to warm up cache for, and any additional dashboard or
diff --git a/superset/commands/chart/warm_up_cache.py b/superset/commands/chart/warm_up_cache.py
index f36e1c16c71..11ff21683ad 100644
--- a/superset/commands/chart/warm_up_cache.py
+++ b/superset/commands/chart/warm_up_cache.py
@@ -18,8 +18,6 @@
 
 from typing import Any, cast, Optional, Union
 
-from flask import g
-
 from superset.commands.base import BaseCommand
 from superset.commands.chart.data.get_data_command import ChartDataCommand
 from superset.commands.chart.exceptions import (
@@ -33,8 +31,7 @@ from superset.extensions import db, security_manager
 from superset.models.slice import Slice
 from superset.utils import json
 from superset.utils.core import error_msg_from_exception, QueryObjectFilterClause
-from superset.views.utils import get_dashboard_extra_filters, get_form_data, get_viz
-from superset.viz import viz_types
+from superset.views.utils import get_dashboard_extra_filters
 
 
 class ChartWarmUpCacheCommand(BaseCommand):
@@ -58,27 +55,6 @@ class ChartWarmUpCacheCommand(BaseCommand):
 
         return get_dashboard_extra_filters(chart_id, self._dashboard_id)
 
-    def _warm_up_legacy_cache(
-        self, chart: Slice, form_data: dict[str, Any]
-    ) -> tuple[Any, Any]:
-        """Warm up cache for legacy visualizations."""
-        if not chart.datasource:
-            raise ChartInvalidError("Chart's datasource does not exist")
-
-        if self._dashboard_id:
-            form_data["extra_filters"] = self._get_dashboard_filters(chart.id)
-
-        g.form_data = form_data
-        payload = get_viz(
-            datasource_type=chart.datasource.type,
-            datasource_id=chart.datasource.id,
-            form_data=form_data,
-            force=True,
-        ).get_payload()
-        delattr(g, "form_data")
-
-        return payload["errors"] or None, payload["status"]
-
     def _warm_up_non_legacy_cache(self, chart: Slice) -> tuple[Any, Any]:
         """Warm up cache for non-legacy visualizations."""
         query_context = chart.get_query_context()
@@ -112,12 +88,7 @@ class ChartWarmUpCacheCommand(BaseCommand):
         chart = cast(Slice, self._chart_or_id)
 
         try:
-            form_data = get_form_data(chart.id, use_slice_data=True)[0]
-
-            if form_data.get("viz_type") in viz_types:
-                error, status = self._warm_up_legacy_cache(chart, form_data)
-            else:
-                error, status = self._warm_up_non_legacy_cache(chart)
+            error, status = self._warm_up_non_legacy_cache(chart)
         except Exception as ex:  # pylint: disable=broad-except
             error = error_msg_from_exception(ex)
             status = None
diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py
index bd7e8c9ccbe..6f344bd775f 100644
--- a/superset/common/query_context_processor.py
+++ b/superset/common/query_context_processor.py
@@ -53,8 +53,6 @@ from superset.utils.core import (
     is_adhoc_metric,
 )
 from superset.utils.pandas_postprocessing.utils import unescape_separator
-from superset.views.utils import get_viz
-from superset.viz import viz_types
 
 if TYPE_CHECKING:
     from superset.common.query_context import QueryContext
@@ -487,29 +485,6 @@ class QueryContextProcessor:
             )
 
         try:
-            if chart.viz_type in viz_types:
-                if not chart.datasource:
-                    raise QueryObjectValidationError(
-                        _(
-                            f"""The dataset for chart ID {chart.id} (referenced by
-                            annotation layer '{annotation_layer["name"]}') was
-                            not found. Please check that the dataset exists and
-                            is accessible."""
-                        )
-                    )
-
-                form_data = chart.form_data.copy()
-                form_data.update(annotation_layer.get("overrides", {}))
-
-                payload = get_viz(
-                    datasource_type=chart.datasource.type,
-                    datasource_id=chart.datasource.id,
-                    form_data=form_data,
-                    force=force,
-                ).get_payload()
-
-                return payload["data"]
-
             if not (query_context := chart.get_query_context()):
                 raise QueryObjectValidationError(
                     _(
diff --git a/superset/config.py b/superset/config.py
index f84493a44f2..03a62301039 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -349,7 +349,6 @@ WTF_CSRF_ENABLED = True
 WTF_CSRF_EXEMPT_LIST = [
     "superset.charts.data.api.data",
     "superset.dashboards.api.cache_dashboard_screenshot",
-    "superset.views.core.explore_json",
     "superset.views.core.log",
     "superset.views.datasource.views.samples",
     "flask_appbuilder.security.views.acs",
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index 4801e3afcc5..1cc41907069 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -1167,7 +1167,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
             Warms up the cache for the table.
             Note for slices a force refresh occurs.
             In terms of the `extra_filters` these can be obtained from records in the JSON
-            encoded `logs.json` column associated with the `explore_json` action.
+            encoded `logs.json` column associated with the `explore` action.
           requestBody:
             description: >-
               Identifies the database and table to warm up cache for, and any
diff --git a/superset/mcp_service/utils/permissions_utils.py b/superset/mcp_service/utils/permissions_utils.py
index 36977e8866e..38956fb2bec 100644
--- a/superset/mcp_service/utils/permissions_utils.py
+++ b/superset/mcp_service/utils/permissions_utils.py
@@ -55,7 +55,7 @@ SENSITIVE_FIELD_PERMISSIONS = {
     "sql": "can_sql_json",  # SQL Lab permissions
     "extra": "can_this_form_get",  # Advanced form permissions
     "database_id": "can_this_form_get",  # Database access permissions
-    "query_context": "can_explore_json",  # Explore permissions
+    "query_context": "can_explore",  # Explore permissions
     "cache_key": "can_warm_up_cache",  # Cache management permissions
     "css": "can_this_form_get",  # Dashboard styling permissions
 }
diff --git a/superset/middleware/legacy_prefix_redirect.py b/superset/middleware/legacy_prefix_redirect.py
index 61747ba57fa..91f38d9e0a3 100644
--- a/superset/middleware/legacy_prefix_redirect.py
+++ b/superset/middleware/legacy_prefix_redirect.py
@@ -21,7 +21,7 @@ After ``Superset.route_base = ""`` collapsed the historical ``/superset``
 prefix off every view, bookmarks/email links/external integrations to the
 legacy paths would 404. This middleware closes that gap for one release
 cycle (removed at EOL ``5.0.0``, matching the ``@deprecated`` gate on
-``Superset.explore`` and ``Superset.explore_json``).
+``Superset.explore``).
 
 Layering invariant
 ------------------
@@ -98,8 +98,6 @@ LEGACY_REDIRECT_MAP: dict[str, tuple[frozenset[str], str]] = {
     "/file-handler": (frozenset({"GET"}), "/file-handler"),
     "/log/": (frozenset({"POST"}), "/log/"),
     "/sqllab/history/": (frozenset({"GET"}), "/sqllab/history/"),
-    "/explore_json/": (frozenset({"GET", "POST"}), "/explore_json/"),
-    "/explore_json/data/": (frozenset({"GET"}), "/explore_json/data/"),
     # views/explore.py β€” ExploreView (route_base = "/explore") owns the
     # bare /explore/ rule; Superset.explore (deprecated) provides the
     # POST handler. ExplorePermalinkView.permalink (route_base = "")
diff --git a/superset/models/slice.py b/superset/models/slice.py
index e9d977b027b..9942bffb51e 100644
--- a/superset/models/slice.py
+++ b/superset/models/slice.py
@@ -48,7 +48,6 @@ from superset.tasks.thumbnails import cache_chart_thumbnail
 from superset.tasks.utils import get_current_user
 from superset.thumbnails.digest import get_chart_digest
 from superset.utils import core as utils, json
-from superset.viz import BaseViz, viz_types
 
 if TYPE_CHECKING:
     from superset.common.query_context import QueryContext
@@ -198,15 +197,6 @@ class Slice(  # pylint: disable=too-many-public-methods
         datasource = self.datasource
         return datasource.url if datasource else None
 
-    @property
-    def viz(self) -> BaseViz | None:
-        form_data = json.loads(self.params)
-        viz_class = viz_types.get(self.viz_type)
-        datasource = self.datasource
-        if viz_class and datasource:
-            return viz_class(datasource=datasource, form_data=form_data)
-        return None
-
     @property
     def description_markeddown(self) -> str:
         return utils.markdown(self.description or "")
@@ -217,8 +207,7 @@ class Slice(  # pylint: disable=too-many-public-methods
         data: dict[str, Any] = {}
         self.token = ""
         try:
-            viz = self.viz
-            data = viz.data if viz else self.form_data
+            data = self.form_data
             self.token = utils.get_form_data_token(data)
         except Exception as ex:  # pylint: disable=broad-except
             logger.exception(ex)
@@ -328,11 +317,6 @@ class Slice(  # pylint: disable=too-many-public-methods
         """Defines the url to access the slice"""
         return self.get_explore_url()
 
-    @property
-    def explore_json_url(self) -> str:
-        """Defines the url to access the slice"""
-        return self.get_explore_url("/explore_json")
-
     @property
     def edit_url(self) -> str:
         return f"/chart/edit/{self.id}"
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 352edf7e9c3..248ee92dbd6 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -114,7 +114,6 @@ if TYPE_CHECKING:
     from superset.models.slice import Slice
     from superset.models.sql_lab import Query
     from superset.semantic_layers.models import SemanticLayer, SemanticView
-    from superset.viz import BaseViz
 
 logger = logging.getLogger(__name__)
 
@@ -1079,7 +1078,6 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         ("can_read", "Chart"),
         ("can_dashboard", "Superset"),
         ("can_slice", "Superset"),
-        ("can_explore_json", "Superset"),
         ("can_dashboard_permalink", "Superset"),
         ("can_read", "DashboardPermalinkRestApi"),
         # Dashboard filter interactions
@@ -3451,7 +3449,6 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         query: Optional["Query | Explorable"] = None,
         query_context: Optional["QueryContext"] = None,
         table: Optional["Table"] = None,
-        viz: Optional["BaseViz"] = None,
         sql: Optional[str] = None,
         catalog: Optional[str] = None,
         schema: Optional[str] = None,
@@ -3466,7 +3463,6 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
         :param query: The SQL Lab query
         :param query_context: The query context
         :param table: The Superset table (requires database)
-        :param viz: The visualization
         :param sql: The SQL string (requires database)
         :param catalog: Optional catalog name
         :param schema: Optional schema name
@@ -3692,15 +3688,12 @@ class SupersetSecurityManager(  # pylint: disable=too-many-public-methods
                 )
             )
 
-        if datasource or query_context or viz:
+        if datasource or query_context:
             form_data = None
 
             if query_context:
                 datasource = query_context.datasource
                 form_data = query_context.form_data
-            elif viz:
-                datasource = viz.datasource
-                form_data = viz.form_data
 
             assert datasource
 
diff --git a/superset/tasks/async_queries.py b/superset/tasks/async_queries.py
index f9aa9efc2fe..ca20e676603 100644
--- a/superset/tasks/async_queries.py
+++ b/superset/tasks/async_queries.py
@@ -16,10 +16,9 @@
 # under the License.
 from __future__ import annotations
 
-import copy
 import dataclasses
 import logging
-from typing import Any, cast, TYPE_CHECKING
+from typing import Any, TYPE_CHECKING
 
 from celery.exceptions import SoftTimeLimitExceeded
 from flask import current_app, g
@@ -30,17 +29,13 @@ from superset.charts.schemas import ChartDataQueryContextSchema
 from superset.exceptions import (
     SupersetErrorException,
     SupersetErrorsException,
-    SupersetVizException,
 )
 from superset.extensions import (
     async_query_manager,
-    cache_manager,
     celery_app,
     security_manager,
 )
-from superset.utils.cache import generate_cache_key, set_and_log_cache
 from superset.utils.core import override_user
-from superset.views.utils import get_datasource_info, get_viz
 
 if TYPE_CHECKING:
     from superset.common.query_context import QueryContext
@@ -122,77 +117,3 @@ def load_chart_data_into_cache(
                 job_metadata, async_query_manager.STATUS_ERROR, errors=errors
             )
             raise
-
-
-@celery_app.task(name="load_explore_json_into_cache", soft_time_limit=query_timeout)
-def load_explore_json_into_cache(  # pylint: disable=too-many-locals
-    job_metadata: dict[str, Any],
-    form_data: dict[str, Any],
-    response_type: str | None = None,
-    force: bool = False,
-) -> None:
-    cache_key_prefix = "ejr-"  # ejr: explore_json request
-
-    with override_user(_load_user_from_job_metadata(job_metadata), force=False):
-        try:
-            set_form_data(form_data)
-            datasource_id, datasource_type = get_datasource_info(None, None, form_data)
-
-            # Perform a deep copy here so that below we can cache the original
-            # value of the form_data object. This is necessary since the viz
-            # objects modify the form_data object. If the modified version were
-            # to be cached here, it will lead to a cache miss when clients
-            # attempt to retrieve the value of the completed async query.
-            original_form_data = copy.deepcopy(form_data)
-
-            viz_obj = get_viz(
-                datasource_type=cast(str, datasource_type),
-                datasource_id=datasource_id,
-                form_data=form_data,
-                force=force,
-            )
-            # run query & cache results
-            payload = viz_obj.get_payload()
-            if viz_obj.has_error(payload):
-                raise SupersetVizException(errors=payload["errors"])
-
-            # Cache the original form_data value for async retrieval
-            cache_value = {
-                "form_data": original_form_data,
-                "response_type": response_type,
-            }
-            cache_key = generate_cache_key(cache_value, cache_key_prefix)
-            cache_instance = cache_manager.cache
-            cache_timeout = (
-                cache_instance.cache.default_timeout if cache_instance.cache else None
-            )
-            set_and_log_cache(
-                cache_instance, cache_key, cache_value, cache_timeout=cache_timeout
-            )
-            result_url = f"/explore_json/data/{cache_key}"
-            async_query_manager.update_job(
-                job_metadata,
-                async_query_manager.STATUS_DONE,
-                result_url=result_url,
-            )
-        except SoftTimeLimitExceeded as ex:
-            logger.warning(
-                "A timeout occurred while loading explore json, error: %s", ex
-            )
-            raise
-        except Exception as ex:
-            if isinstance(ex, SupersetVizException):
-                errors = ex.errors
-            # Extract SIP-40 style errors when available
-            elif isinstance(ex, SupersetErrorException):
-                errors = [dataclasses.asdict(ex.error)]  # type: ignore
-            elif isinstance(ex, SupersetErrorsException):
-                errors = [dataclasses.asdict(error) for error in ex.errors]  # type: ignore
-            else:
-                error = ex.message if hasattr(ex, "message") else str(ex)
-                errors = [error]  # type: ignore
-
-            async_query_manager.update_job(
-                job_metadata, async_query_manager.STATUS_ERROR, errors=errors
-            )
-            raise
diff --git a/superset/views/core.py b/superset/views/core.py
index 84932ae55ac..30b7fc593d1 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -39,7 +39,6 @@ from flask_appbuilder import expose
 from flask_appbuilder.security.decorators import (
     has_access,
     has_access_api,
-    permission_name,
 )
 from flask_babel import gettext as __, lazy_gettext as _
 from sqlalchemy.exc import SQLAlchemyError
@@ -51,7 +50,6 @@ from superset import (
     is_feature_enabled,
     security_manager,
 )
-from superset.async_events.async_query_manager import AsyncQueryTokenException
 from superset.commands.chart.exceptions import ChartNotFoundError
 from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand
 from superset.commands.dashboard.exceptions import DashboardAccessDeniedError
@@ -60,19 +58,15 @@ from superset.commands.dataset.exceptions import DatasetNotFoundError
 from superset.commands.explore.form_data.get import GetFormDataCommand
 from superset.commands.explore.form_data.parameters import CommandParameters
 from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
-from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType
 from superset.connectors.sqla.models import BaseDatasource, SqlaTable
 from superset.daos.chart import ChartDAO
 from superset.daos.datasource import DatasourceDAO
 from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError
 from superset.exceptions import (
-    CacheLoadError,
-    SupersetErrorException,
     SupersetException,
     SupersetSecurityException,
 )
 from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError
-from superset.extensions import async_query_manager, cache_manager
 from superset.models.core import Database
 from superset.models.dashboard import Dashboard
 from superset.models.slice import Slice
@@ -83,10 +77,8 @@ from superset.superset_typing import (
 )
 from superset.tasks.utils import get_current_user
 from superset.utils import core as utils, json
-from superset.utils.cache import etag_cache
 from superset.utils.core import (
     DatasourceType,
-    GenericDataType,
     get_user_id,
     ReservedUrlParameters,
 )
@@ -94,28 +86,19 @@ from superset.views.base import (
     api,
     BaseSupersetView,
     common_bootstrap_payload,
-    CsvResponse,
-    data_payload_response,
     deprecated,
-    generate_download_headers,
     json_error_response,
     json_success,
-    XlsxResponse,
 )
 from superset.views.error_handling import handle_api_exception
 from superset.views.utils import (
     bootstrap_user_data,
-    check_datasource_perms,
-    check_explore_cache_perms,
-    check_resource_permissions,
     get_datasource_info,
     get_explore_redirect_url,
     get_form_data,
-    get_viz,
     redirect_to_login,
     sanitize_datasource_data,
 )
-from superset.viz import BaseViz
 
 logger = logging.getLogger(__name__)
 
@@ -159,245 +142,6 @@ class Superset(BaseSupersetView):
             endpoint_params[ReservedUrlParameters.STANDALONE.value] = "true"
         return redirect(url_for("ExploreView.root", **endpoint_params))
 
-    def get_query_string_response(self, viz_obj: BaseViz) -> FlaskResponse:
-        query = None
-        try:
-            if query_obj := viz_obj.query_obj():
-                query = viz_obj.datasource.get_query_str(query_obj)
-        except Exception as ex:  # pylint: disable=broad-except
-            err_msg = utils.error_msg_from_exception(ex)
-            logger.exception(err_msg)
-            return json_error_response(err_msg)
-
-        if not query:
-            query = __("Query cannot be loaded.")
-
-        return self.json_response(
-            {"query": query, "language": viz_obj.datasource.query_language}
-        )
-
-    def get_raw_results(self, viz_obj: BaseViz) -> FlaskResponse:
-        payload = viz_obj.get_df_payload()
-        if viz_obj.has_error(payload):
-            return json_error_response(payload=payload, status=400)
-        response = {
-            "data": payload["df"].to_dict("records")
-            if payload["df"] is not None
-            else [],
-            "colnames": payload.get("colnames"),
-            "coltypes": payload.get("coltypes"),
-            "rowcount": payload.get("rowcount"),
-            "sql_rowcount": payload.get("sql_rowcount"),
-        }
-        return self.json_response(response)
-
-    def get_samples(self, viz_obj: BaseViz) -> FlaskResponse:
-        return self.json_response(viz_obj.get_samples())
-
-    @staticmethod
-    def send_data_payload_response(viz_obj: BaseViz, payload: Any) -> FlaskResponse:
-        return data_payload_response(*viz_obj.payload_json_and_has_error(payload))
-
-    def generate_json(
-        self, viz_obj: BaseViz, response_type: str | None = None
-    ) -> FlaskResponse:
-        if response_type == ChartDataResultFormat.CSV:
-            return CsvResponse(
-                viz_obj.get_csv(), headers=generate_download_headers("csv")
-            )
-
-        if response_type == ChartDataResultFormat.XLSX:
-            return self._generate_xlsx(viz_obj)
-
-        if response_type == ChartDataResultType.QUERY:
-            return self.get_query_string_response(viz_obj)
-
-        if response_type == ChartDataResultType.RESULTS:
-            return self.get_raw_results(viz_obj)
-
-        if response_type == ChartDataResultType.SAMPLES:
-            return self.get_samples(viz_obj)
-
-        payload = viz_obj.get_payload()
-        return self.send_data_payload_response(viz_obj, payload)
-
-    @staticmethod
-    def _generate_xlsx(viz_obj: BaseViz) -> FlaskResponse:
-        import pandas as pd
-
-        from superset.utils.excel import apply_column_types, df_to_excel
-
-        payload = viz_obj.get_df_payload()
-        df = payload.get("df")
-        if df is None:
-            df = pd.DataFrame()
-            coltypes: list[GenericDataType] = []
-        else:
-            coltypes = payload.get("coltypes") or []
-            if coltypes:
-                df = apply_column_types(df, coltypes)
-        xlsx_data = df_to_excel(df, index=False)
-        return XlsxResponse(xlsx_data, headers=generate_download_headers("xlsx"))
-
-    @event_logger.log_this
-    @api
-    @has_access_api
-    @handle_api_exception
-    @permission_name("explore_json")
-    @expose("/explore_json/data/<cache_key>", methods=("GET",))
-    @check_resource_permissions(check_explore_cache_perms)
-    @deprecated(eol_version="5.0.0")
-    def explore_json_data(self, cache_key: str) -> FlaskResponse:
-        """Serves cached result data for async explore_json calls
-
-        `self.generate_json` receives this input and returns different
-        payloads based on the request args in the first block
-
-        TODO: form_data should not be loaded twice from cache
-          (also loaded in `check_explore_cache_perms`)
-        """
-        try:
-            cached = cache_manager.cache.get(cache_key)
-            if not cached:
-                raise CacheLoadError("Cached data not found")
-
-            form_data = cached.get("form_data")
-            response_type = cached.get("response_type")
-            # Set form_data in Flask Global as it is used as a fallback
-            # for async queries with jinja context
-            g.form_data = form_data
-            datasource_id, datasource_type = get_datasource_info(None, None, form_data)
-
-            viz_obj = get_viz(
-                datasource_type=cast(str, datasource_type),
-                datasource_id=datasource_id,
-                form_data=form_data,
-                force_cached=True,
-            )
-
-            return self.generate_json(viz_obj, response_type)
-        except SupersetErrorException:
-            # Let structured Superset errors (e.g. OAuth2RedirectError) propagate
-            # so the global Flask error handler serializes them.
-            raise
-        except SupersetException as ex:
-            return json_error_response(utils.error_msg_from_exception(ex), 400)
-
-    @api
-    @has_access_api
-    @handle_api_exception
-    @event_logger.log_this
-    @expose(
-        "/explore_json/<datasource_type>/<int:datasource_id>/",
-        methods=(
-            "GET",
-            "POST",
-        ),
-    )
-    @expose(
-        "/explore_json/",
-        methods=(
-            "GET",
-            "POST",
-        ),
-    )
-    @etag_cache()
-    @check_resource_permissions(check_datasource_perms)
-    @deprecated(eol_version="5.0.0", new_target="/api/v1/chart/data")
-    def explore_json(  # noqa: C901
-        self, datasource_type: str | None = None, datasource_id: int | None = None
-    ) -> FlaskResponse:
-        """Serves all request that GET or POST form_data
-
-        This endpoint evolved to be the entry point of many different
-        requests that GETs or POSTs a form_data.
-
-        `self.generate_json` receives this input and returns different
-        payloads based on the request args in the first block
-
-        TODO: break into one endpoint for each return shape"""
-
-        response_type = ChartDataResultFormat.JSON.value
-        responses: list[ChartDataResultFormat | ChartDataResultType] = list(
-            ChartDataResultFormat
-        )
-        responses.extend(list(ChartDataResultType))
-        for response_option in responses:
-            if request.args.get(response_option) == "true":
-                response_type = response_option
-                break
-
-        # Verify user has permission to export data files
-        if response_type in (
-            ChartDataResultFormat.CSV,
-            ChartDataResultFormat.XLSX,
-        ):
-            if is_feature_enabled("GRANULAR_EXPORT_CONTROLS"):
-                can_export = security_manager.can_access("can_export_data", "Superset")
-            else:
-                can_export = security_manager.can_access("can_csv", "Superset")
-            if not can_export:
-                return json_error_response(
-                    _("You don't have the rights to export data"),
-                    status=403,
-                )
-
-        form_data = get_form_data()[0]
-        try:
-            datasource_id, datasource_type = get_datasource_info(
-                datasource_id, datasource_type, form_data
-            )
-            force = request.args.get("force") == "true"
-
-            # TODO: support CSV, SQL query and other non-JSON types
-            if (
-                is_feature_enabled("GLOBAL_ASYNC_QUERIES")
-                and response_type == ChartDataResultFormat.JSON
-            ):
-                # First, look for the chart query results in the cache.
-                with contextlib.suppress(CacheLoadError):
-                    viz_obj = get_viz(
-                        datasource_type=cast(str, datasource_type),
-                        datasource_id=datasource_id,
-                        form_data=form_data,
-                        force_cached=True,
-                        force=force,
-                    )
-                    payload = viz_obj.get_payload()
-                    # If the chart query has already been cached, return it immediately.
-                    if payload is not None:
-                        return self.send_data_payload_response(viz_obj, payload)
-                # Otherwise, kick off a background job to run the chart query.
-                # Clients will either poll or be notified of query completion,
-                # at which point they will call the /explore_json/data/<cache_key>
-                # endpoint to retrieve the results.
-                try:
-                    async_channel_id = (
-                        async_query_manager.parse_channel_id_from_request(request)
-                    )
-                    job_metadata = async_query_manager.submit_explore_json_job(
-                        async_channel_id, form_data, response_type, force, get_user_id()
-                    )
-                except AsyncQueryTokenException:
-                    return json_error_response("Not authorized", 401)
-
-                return json_success(json.dumps(job_metadata), status=202)
-
-            viz_obj = get_viz(
-                datasource_type=cast(str, datasource_type),
-                datasource_id=datasource_id,
-                form_data=form_data,
-                force=force,
-            )
-
-            return self.generate_json(viz_obj, response_type)
-        except SupersetErrorException:
-            # Let structured Superset errors (e.g. OAuth2RedirectError) propagate
-            # so the global Flask error handler serializes them.
-            raise
-        except SupersetException as ex:
-            return json_error_response(utils.error_msg_from_exception(ex), 400)
-
     @has_access
     @event_logger.log_this
     @expose(
@@ -711,7 +455,7 @@ class Superset(BaseSupersetView):
         Note for slices a force refresh occurs.
 
         In terms of the `extra_filters` these can be obtained from records in the JSON
-        encoded `logs.json` column associated with the `explore_json` action.
+        encoded `logs.json` column associated with the `explore` action.
         """
         slice_id = request.args.get("slice_id")
         dashboard_id = request.args.get("dashboard_id")
diff --git a/superset/views/utils.py b/superset/views/utils.py
index 3bc6ddd5c37..deb05a6bd92 100644
--- a/superset/views/utils.py
+++ b/superset/views/utils.py
@@ -34,19 +34,14 @@ from flask import (
 from flask_appbuilder.security.sqla import models as ab_models
 from flask_appbuilder.security.sqla.models import User
 from flask_babel import _
-from sqlalchemy.exc import NoResultFound
 
-from superset import appbuilder, dataframe, db, result_set, viz
+from superset import appbuilder, dataframe, db, result_set
 from superset.common.db_query_status import QueryStatus
-from superset.daos.datasource import DatasourceDAO
-from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
 from superset.exceptions import (
-    CacheLoadError,
     SerializationError,
     SupersetException,
-    SupersetSecurityException,
 )
-from superset.extensions import cache_manager, feature_flag_manager, security_manager
+from superset.extensions import feature_flag_manager, security_manager
 from superset.legacy import update_time_range
 from superset.models.core import Database
 from superset.models.dashboard import Dashboard
@@ -60,7 +55,6 @@ from superset.superset_typing import (
 from superset.utils import json
 from superset.utils.core import DatasourceType
 from superset.utils.decorators import stats_timing
-from superset.viz import BaseViz
 
 logger = logging.getLogger(__name__)
 stats_logger = app.config["STATS_LOGGER"]
@@ -181,24 +175,6 @@ def get_permissions(
     return roles_permissions, transformed_permissions
 
 
-def get_viz(
-    form_data: FormData,
-    datasource_type: str,
-    datasource_id: int,
-    force: bool = False,
-    force_cached: bool = False,
-) -> BaseViz:
-    viz_type = form_data.get("viz_type", "table")
-    datasource = DatasourceDAO.get_datasource(
-        DatasourceType(datasource_type),
-        datasource_id,
-    )
-    viz_obj = viz.viz_types[viz_type](
-        datasource, form_data=form_data, force=force, force_cached=force_cached
-    )
-    return viz_obj
-
-
 def loads_request_json(request_json_data: str) -> dict[Any, Any]:
     """Parse a JSON request payload, coercing non-objects to ``{}``.
 
@@ -636,81 +612,6 @@ def check_resource_permissions(
     return decorator
 
 
-def check_explore_cache_perms(_self: Any, cache_key: str) -> None:
-    """
-    Loads async explore_json request data from cache and performs access check
-
-    :param _self: the Superset view instance
-    :param cache_key: the cache key passed into /explore_json/data/
-    :raises SupersetSecurityException: If the user cannot access the resource
-    """
-    cached = cache_manager.cache.get(cache_key)
-    if not cached:
-        raise CacheLoadError("Cached data not found")
-
-    check_datasource_perms(_self, form_data=cached["form_data"])
-
-
-def check_datasource_perms(
-    _self: Any,
-    datasource_type: Optional[str] = None,
-    datasource_id: Optional[int] = None,
-    **kwargs: Any,
-) -> None:
-    """
-    Check if user can access a cached response from explore_json.
-
-    This function takes `self` since it must have the same signature as the
-    the decorated method.
-
-    :param datasource_type: The datasource type
-    :param datasource_id: The datasource ID
-    :raises SupersetSecurityException: If the user cannot access the resource
-    """
-
-    form_data = kwargs["form_data"] if "form_data" in kwargs else get_form_data()[0]
-
-    try:
-        datasource_id, datasource_type = get_datasource_info(
-            datasource_id, datasource_type, form_data
-        )
-    except SupersetException as ex:
-        raise SupersetSecurityException(
-            SupersetError(
-                error_type=SupersetErrorType.FAILED_FETCHING_DATASOURCE_INFO_ERROR,
-                level=ErrorLevel.ERROR,
-                message=str(ex),
-            )
-        ) from ex
-
-    if datasource_type is None:
-        raise SupersetSecurityException(
-            SupersetError(
-                error_type=SupersetErrorType.UNKNOWN_DATASOURCE_TYPE_ERROR,
-                level=ErrorLevel.ERROR,
-                message=_("Could not determine datasource type"),
-            )
-        )
-
-    try:
-        viz_obj = get_viz(
-            datasource_type=datasource_type,
-            datasource_id=datasource_id,
-            form_data=form_data,
-            force=False,
-        )
-    except NoResultFound as ex:
-        raise SupersetSecurityException(
-            SupersetError(
-                error_type=SupersetErrorType.UNKNOWN_DATASOURCE_TYPE_ERROR,
-                level=ErrorLevel.ERROR,
-                message=_("Could not find viz object"),
-            )
-        ) from ex
-
-    viz_obj.raise_for_access()
-
-
 def _deserialize_results_payload(
     payload: Union[bytes, str], query: Query, use_msgpack: Optional[bool] = False
 ) -> dict[str, Any]:
diff --git a/superset/viz.py b/superset/viz.py
deleted file mode 100644
index 1536af58da4..00000000000
--- a/superset/viz.py
+++ /dev/null
@@ -1,2742 +0,0 @@
-# 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.
-# pylint: disable=too-many-lines
-"""This module contains the 'Viz' objects
-
-These objects represent the backend of all the visualizations that
-Superset can render.
-"""
-
-from __future__ import annotations
-
-import copy
-import dataclasses
-import logging
-import math
-from collections import OrderedDict
-from datetime import datetime, timedelta
-from itertools import product
-from typing import Any, cast, Optional, TYPE_CHECKING
-
-import numpy as np
-import pandas as pd
-import polyline
-import pygeohash
-from dateutil import relativedelta as rdelta
-from deprecation import deprecated
-from flask import current_app, request
-from flask_babel import lazy_gettext as _
-from geopy.point import Point
-from pandas.tseries.frequencies import to_offset
-
-from superset.common.db_query_status import QueryStatus
-from superset.constants import CACHE_DISABLED_TIMEOUT
-from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
-from superset.exceptions import (
-    CacheLoadError,
-    NullValueException,
-    QueryObjectValidationError,
-    SpatialException,
-    SupersetErrorException,
-    SupersetSecurityException,
-)
-from superset.extensions import cache_manager, security_manager
-from superset.models.helpers import QueryResult
-from superset.sql.parse import sanitize_clause
-from superset.superset_typing import (
-    Column,
-    Metric,
-    QueryObjectDict,
-    VizData,
-    VizPayload,
-)
-from superset.utils import core as utils, csv, json
-from superset.utils.cache import set_and_log_cache
-from superset.utils.cache_keys import add_impersonation_cache_key_if_needed
-from superset.utils.core import (
-    apply_max_row_limit,
-    DateColumn,
-    DTTM_ALIAS,
-    ExtraFiltersReasonType,
-    get_column_name,
-    get_column_names,
-    get_column_names_from_columns,
-    JS_MAX_INTEGER,
-    merge_extra_filters,
-    simple_filter_to_adhoc,
-)
-from superset.utils.date_parser import get_since_until, parse_past_timedelta
-from superset.utils.hashing import hash_from_str
-from superset.utils.pandas_postprocessing.utils import (
-    escape_separator,
-    FLAT_COLUMN_SEPARATOR,
-)
-
-if TYPE_CHECKING:
-    from superset.connectors.sqla.models import BaseDatasource
-
-logger = logging.getLogger(__name__)
-
-METRIC_KEYS = [
-    "metric",
-    "metrics",
-    "percent_metrics",
-    "metric_2",
-    "secondary_metric",
-    "x",
-    "y",
-    "size",
-]
-
-# Allowlist of resampler aggregation methods that may be invoked dynamically via
-# ``getattr`` on a pandas ``Resampler``. Restricting the set of callable names
-# keeps the dynamic dispatch limited to known-safe aggregations.
-ALLOWED_RESAMPLE_METHODS = frozenset(
-    {
-        "asfreq",
-        "bfill",
-        "count",
-        "ffill",
-        "first",
-        "last",
-        "max",
-        "mean",
-        "median",
-        "min",
-        "std",
-        "sum",
-        "var",
-    }
-)
-
-
-class BaseViz:  # pylint: disable=too-many-public-methods
-    """All visualizations derive this base class"""
-
-    viz_type: str | None = None
-    verbose_name = "Base Viz"
-    credits = ""
-    is_timeseries = False
-    cache_type = "df"
-    enforce_numerical_metrics = True
-
-    @deprecated(deprecated_in="3.0")
-    def __init__(
-        self,
-        datasource: BaseDatasource,
-        form_data: dict[str, Any],
-        force: bool = False,
-        force_cached: bool = False,
-    ) -> None:
-        if not datasource:
-            raise QueryObjectValidationError(_("Viz is missing a datasource"))
-
-        self.datasource = datasource
-        self.request = request
-        self.viz_type = form_data.get("viz_type")
-        self.form_data = form_data
-
-        self.query = ""
-        self.token = utils.get_form_data_token(form_data)
-
-        self.groupby: list[Column] = self.form_data.get("groupby") or []
-        self.time_shift = timedelta()
-
-        self.status: str | None = None
-        self.error_msg = ""
-        self.results: QueryResult | None = None
-        self.applied_filter_columns: list[Column] = []
-        self.rejected_filter_columns: list[Column] = []
-        self.errors: list[dict[str, Any]] = []
-        self.force = force
-        self._force_cached = force_cached
-        self.from_dttm: datetime | None = None
-        self.to_dttm: datetime | None = None
-        self._extra_chart_data: list[tuple[str, pd.DataFrame]] = []
-
-        self.process_metrics()
-
-        self.applied_filters: list[dict[str, str]] = []
-        self.rejected_filters: list[dict[str, str]] = []
-
-    @property
-    @deprecated(deprecated_in="3.0")
-    def force_cached(self) -> bool:
-        return self._force_cached
-
-    @deprecated(deprecated_in="3.0")
-    def process_metrics(self) -> None:
-        # metrics in Viz is order sensitive, so metric_dict should be
-        # OrderedDict
-        self.metric_dict = OrderedDict()
-        for mkey in METRIC_KEYS:
-            val = self.form_data.get(mkey)
-            if val:
-                if not isinstance(val, list):
-                    val = [val]
-                for o in val:
-                    label = utils.get_metric_name(o)
-                    self.metric_dict[label] = o
-
-        # Cast to list needed to return serializable object in py3
-        self.all_metrics = list(self.metric_dict.values())
-        self.metric_labels = list(self.metric_dict.keys())
-
-    @staticmethod
-    @deprecated(deprecated_in="3.0")
-    def handle_js_int_overflow(
-        data: dict[str, list[dict[str, Any]]],
-    ) -> dict[str, list[dict[str, Any]]]:
-        for record in data.get("records", {}):
-            for k, v in list(record.items()):
-                if isinstance(v, int):
-                    # if an int is too big for Java Script to handle
-                    # convert it to a string
-                    if abs(v) > JS_MAX_INTEGER:
-                        record[k] = str(v)
-        return data
-
-    @deprecated(deprecated_in="3.0")
-    def run_extra_queries(self) -> None:
-        """Lifecycle method to use when more than one query is needed
-
-        In rare-ish cases, a visualization may need to execute multiple
-        queries. That is the case for FilterBox or for time comparison
-        in Line chart for instance.
-
-        In those cases, we need to make sure these queries run before the
-        main `get_payload` method gets called, so that the overall caching
-        metadata can be right. The way it works here is that if any of
-        the previous `get_df_payload` calls hit the cache, the main
-        payload's metadata will reflect that.
-
-        The multi-query support may need more work to become a first class
-        use case in the framework, and for the UI to reflect the subtleties
-        (show that only some of the queries were served from cache for
-        instance). In the meantime, since multi-query is rare, we treat
-        it with a bit of a hack. Note that the hack became necessary
-        when moving from caching the visualization's data itself, to caching
-        the underlying query(ies).
-        """
-
-    @deprecated(deprecated_in="3.0")
-    def apply_rolling(self, df: pd.DataFrame) -> pd.DataFrame:
-        rolling_type = self.form_data.get("rolling_type")
-        rolling_periods = int(self.form_data.get("rolling_periods") or 0)
-        min_periods = int(self.form_data.get("min_periods") or 0)
-
-        if rolling_type in ("mean", "std", "sum") and rolling_periods:
-            kwargs = {"window": rolling_periods, "min_periods": min_periods}
-            if rolling_type == "mean":
-                df = df.rolling(**kwargs).mean()
-            elif rolling_type == "std":
-                df = df.rolling(**kwargs).std()
-            elif rolling_type == "sum":
-                df = df.rolling(**kwargs).sum()
-        elif rolling_type == "cumsum":
-            df = df.cumsum()
-        if min_periods:
-            df = df[min_periods:]
-        if df.empty:
-            raise QueryObjectValidationError(
-                _(
-                    "Applied rolling window did not return any data. Please make sure "
-                    "the source query satisfies the minimum periods defined in the "
-                    "rolling window."
-                )
-            )
-        return df
-
-    @deprecated(deprecated_in="3.0")
-    def get_samples(self) -> dict[str, Any]:
-        query_obj = self.query_obj()
-        query_obj.update(
-            {
-                "is_timeseries": False,
-                "groupby": [],
-                "metrics": [],
-                "orderby": [],
-                "row_limit": current_app.config["SAMPLES_ROW_LIMIT"],
-                "columns": [o.column_name for o in self.datasource.columns],
-                "from_dttm": None,
-                "to_dttm": None,
-            }
-        )
-        payload = self.get_df_payload(query_obj)  # leverage caching logic
-        return {
-            "data": payload["df"].to_dict(orient="records"),
-            "colnames": payload.get("colnames"),
-            "coltypes": payload.get("coltypes"),
-            "rowcount": payload.get("rowcount"),
-            "sql_rowcount": payload.get("sql_rowcount"),
-        }
-
-    @deprecated(deprecated_in="3.0")
-    def get_df(self, query_obj: QueryObjectDict | None = None) -> pd.DataFrame:
-        """Returns a pandas dataframe based on the query object"""
-        if not query_obj:
-            query_obj = self.query_obj()
-        if not query_obj:
-            return pd.DataFrame()
-
-        self.error_msg = ""
-
-        timestamp_format = None
-        if self.datasource.type == "table":
-            granularity_col = self.datasource.get_column(query_obj["granularity"])
-            if granularity_col:
-                timestamp_format = granularity_col.python_date_format
-
-        # The datasource here can be different backend but the interface is common
-        self.results = self.datasource.query(query_obj)
-        self.applied_filter_columns = self.results.applied_filter_columns or []
-        self.rejected_filter_columns = self.results.rejected_filter_columns or []
-        self.query = self.results.query
-        self.status = self.results.status
-        self.errors = self.results.errors
-
-        df = self.results.df
-        # Transform the timestamp we received from database to pandas supported
-        # datetime format. If no python_date_format is specified, the pattern will
-        # be considered as the default ISO date format
-        # If the datetime format is unix, the parse will use the corresponding
-        # parsing logic.
-        if not df.empty:
-            utils.normalize_dttm_col(
-                df=df,
-                dttm_cols=tuple(  # noqa: C409
-                    [
-                        DateColumn.get_legacy_time_column(
-                            timestamp_format=timestamp_format,
-                            offset=self.datasource.offset,
-                            time_shift=self.form_data.get("time_shift"),
-                        )
-                    ]
-                ),
-            )
-
-            if self.enforce_numerical_metrics:
-                self.df_metrics_to_num(df)
-
-            df.replace([np.inf, -np.inf], np.nan, inplace=True)
-        return df
-
-    @deprecated(deprecated_in="3.0")
-    def df_metrics_to_num(self, df: pd.DataFrame) -> None:
-        """Converting metrics to numeric when pandas.read_sql cannot"""
-        metrics = self.metric_labels
-        for col, dtype in df.dtypes.items():
-            if dtype.type == np.object_ and col in metrics:
-                df[col] = pd.to_numeric(df[col], errors="coerce")
-
-    @deprecated(deprecated_in="3.0")
-    def process_query_filters(self) -> None:
-        utils.convert_legacy_filters_into_adhoc(self.form_data)
-        merge_extra_filters(self.form_data)
-        engine = self.datasource.database.db_engine_spec.engine
-        utils.split_adhoc_filters_into_base_filters(self.form_data, engine)
-
-    @staticmethod
-    @deprecated(deprecated_in="3.0")
-    def dedup_columns(*columns_args: list[Column] | None) -> list[Column]:
-        # dedup groupby and columns while preserving order
-        labels: list[str] = []
-        deduped_columns: list[Column] = []
-        for columns in columns_args:
-            for column in columns or []:
-                label = get_column_name(column)
-                if label not in labels:
-                    deduped_columns.append(column)
-        return deduped_columns
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:  # pylint: disable=too-many-locals
-        """Building a query object"""
-        self.process_query_filters()
-
-        metrics = self.all_metrics or []
-
-        groupby = self.dedup_columns(self.groupby, self.form_data.get("columns"))
-
-        is_timeseries = self.is_timeseries
-
-        if DTTM_ALIAS in (groupby_labels := get_column_names(groupby)):
-            del groupby[groupby_labels.index(DTTM_ALIAS)]
-            is_timeseries = True
-
-        granularity = self.form_data.get("granularity_sqla")
-        limit = int(self.form_data.get("limit") or 0)
-        timeseries_limit_metric = self.form_data.get("timeseries_limit_metric")
-
-        # apply row limit to query
-        row_limit = int(
-            self.form_data.get("row_limit") or current_app.config["ROW_LIMIT"]
-        )
-        row_limit = apply_max_row_limit(row_limit)
-
-        # default order direction
-        order_desc = self.form_data.get("order_desc", True)
-
-        try:
-            since, until = get_since_until(
-                relative_start=current_app.config["DEFAULT_RELATIVE_START_TIME"],
-                relative_end=current_app.config["DEFAULT_RELATIVE_END_TIME"],
-                time_range=self.form_data.get("time_range"),
-                since=self.form_data.get("since"),
-                until=self.form_data.get("until"),
-            )
-        except ValueError as ex:
-            raise QueryObjectValidationError(str(ex)) from ex
-
-        time_shift = self.form_data.get("time_shift", "")
-        self.time_shift = parse_past_timedelta(time_shift)
-        from_dttm = None if since is None else (since - self.time_shift)
-        to_dttm = None if until is None else (until - self.time_shift)
-        if from_dttm and to_dttm and from_dttm > to_dttm:
-            raise QueryObjectValidationError(
-                _("From date cannot be larger than to date")
-            )
-
-        self.from_dttm = from_dttm
-        self.to_dttm = to_dttm
-
-        # validate sql filters
-        for param in ("where", "having"):
-            clause = self.form_data.get(param)
-            if clause:
-                engine = self.datasource.database.db_engine_spec.engine
-                sanitized_clause = sanitize_clause(clause, engine)
-                if sanitized_clause != clause:
-                    self.form_data[param] = sanitized_clause
-
-        # extras are used to query elements specific to a datasource type
-        # for instance the extra where clause that applies only to Tables
-        extras = {
-            "having": self.form_data.get("having", ""),
-            "time_grain_sqla": self.form_data.get("time_grain_sqla"),
-            "where": self.form_data.get("where", ""),
-        }
-
-        return {
-            "granularity": granularity,
-            "from_dttm": from_dttm,
-            "to_dttm": to_dttm,
-            "is_timeseries": is_timeseries,
-            "groupby": groupby,
-            "metrics": metrics,
-            "row_limit": row_limit,
-            "filter": self.form_data.get("filters", []),
-            "series_limit": limit,
-            "extras": extras,
-            "series_limit_metric": timeseries_limit_metric,
-            "order_desc": order_desc,
-        }
-
-    @property
-    @deprecated(deprecated_in="3.0")
-    def cache_timeout(self) -> int:
-        if self.form_data.get("cache_timeout") is not None:
-            return int(self.form_data["cache_timeout"])
-        if self.datasource.cache_timeout is not None:
-            return self.datasource.cache_timeout
-        if (
-            hasattr(self.datasource, "database")
-            and self.datasource.database.cache_timeout
-        ) is not None:
-            return self.datasource.database.cache_timeout
-        if (
-            current_app.config["DATA_CACHE_CONFIG"].get("CACHE_DEFAULT_TIMEOUT")
-            is not None
-        ):
-            return current_app.config["DATA_CACHE_CONFIG"]["CACHE_DEFAULT_TIMEOUT"]
-        return current_app.config["CACHE_DEFAULT_TIMEOUT"]
-
-    @deprecated(deprecated_in="3.0")
-    def get_json(self) -> str:
-        return json.dumps(
-            self.get_payload(), default=json.json_int_dttm_ser, ignore_nan=True
-        )
-
-    @deprecated(deprecated_in="3.0")
-    def cache_key(self, query_obj: QueryObjectDict, **extra: Any) -> str:
-        """
-        The cache key is made out of the key/values in `query_obj`, plus any
-        other key/values in `extra`.
-
-        We remove datetime bounds that are hard values, and replace them with
-        the use-provided inputs to bounds, which may be time-relative (as in
-        "5 days ago" or "now").
-
-        The `extra` arguments are currently used by time shift queries, since
-        different time shifts will differ only in the `from_dttm`, `to_dttm`,
-        `inner_from_dttm`, and `inner_to_dttm` values which are stripped.
-        """
-        # Cast to dict[str, Any] to allow mutable operations (update, del)
-        # since TypedDict doesn't support these operations in the same way
-        cache_dict: dict[str, Any] = copy.copy(cast(dict[str, Any], query_obj))
-        cache_dict.update(extra)
-
-        for k in ["from_dttm", "to_dttm", "inner_from_dttm", "inner_to_dttm"]:
-            if k in cache_dict:
-                del cache_dict[k]
-
-        cache_dict["time_range"] = self.form_data.get("time_range")
-        cache_dict["datasource"] = self.datasource.uid
-        cache_dict["extra_cache_keys"] = self.datasource.get_extra_cache_keys(query_obj)
-        cache_dict["rls"] = security_manager.get_rls_cache_key(self.datasource)
-        cache_dict["changed_on"] = self.datasource.changed_on
-
-        # Add an impersonation key to cache if impersonation is enabled on the db
-        # or if the CACHE_QUERY_BY_USER flag is on or per_user_caching is enabled on
-        #  the database
-        try:
-            add_impersonation_cache_key_if_needed(self.datasource.database, cache_dict)
-        except AttributeError:
-            # datasource or database do not exist
-            pass
-
-        json_data = self.json_dumps(cache_dict, sort_keys=True)
-        return hash_from_str(json_data)
-
-    @deprecated(deprecated_in="3.0")
-    def get_payload(self, query_obj: QueryObjectDict | None = None) -> VizPayload:
-        """Returns a payload of metadata and data"""
-
-        try:
-            self.run_extra_queries()
-        except SupersetSecurityException as ex:
-            error = dataclasses.asdict(ex.error)
-            self.errors.append(error)
-            self.status = QueryStatus.FAILED
-
-        payload = self.get_df_payload(query_obj)
-
-        # if payload does not have a df, we are raising an error here.
-        df = cast(Optional[pd.DataFrame], payload["df"])
-
-        if self.status != QueryStatus.FAILED:
-            payload["data"] = self.get_data(df)
-        if "df" in payload:
-            del payload["df"]
-
-        applied_filter_columns = self.applied_filter_columns or []
-        rejected_filter_columns = self.rejected_filter_columns or []
-        applied_time_extras = self.form_data.get("applied_time_extras", {})
-        applied_time_columns, rejected_time_columns = utils.get_time_filter_status(
-            self.datasource, applied_time_extras
-        )
-        payload["applied_filters"] = [
-            {"column": get_column_name(col)} for col in applied_filter_columns
-        ] + applied_time_columns
-        payload["rejected_filters"] = [
-            {
-                "reason": ExtraFiltersReasonType.COL_NOT_IN_DATASOURCE,
-                "column": get_column_name(col),
-            }
-            for col in rejected_filter_columns
-        ] + rejected_time_columns
-        if df is not None:
-            payload["colnames"] = list(df.columns)
-
-        return payload
-
-    @deprecated(deprecated_in="3.0")
-    def get_df_payload(  # pylint: disable=too-many-statements  # noqa: C901
-        self, query_obj: QueryObjectDict | None = None, **kwargs: Any
-    ) -> dict[str, Any]:
-        """Handles caching around the df payload retrieval"""
-        if not query_obj:
-            query_obj = self.query_obj()
-        cache_key = self.cache_key(query_obj, **kwargs) if query_obj else None
-        cache_value = None
-        logger.info("Cache key: %s", cache_key)
-        is_loaded = False
-        stacktrace = None
-        df = None
-        cache_timeout = self.cache_timeout
-        force = self.force or cache_timeout == CACHE_DISABLED_TIMEOUT
-        if cache_key and cache_manager.data_cache and not force:
-            cache_value = cache_manager.data_cache.get(cache_key)
-            if cache_value:
-                current_app.config["STATS_LOGGER"].incr("loading_from_cache")
-                try:
-                    df = cache_value["df"]
-                    self.query = cache_value["query"]
-                    self.applied_filter_columns = cache_value.get(
-                        "applied_filter_columns", []
-                    )
-                    self.rejected_filter_columns = cache_value.get(
-                        "rejected_filter_columns", []
-                    )
-                    self.status = QueryStatus.SUCCESS
-                    is_loaded = True
-                    current_app.config["STATS_LOGGER"].incr("loaded_from_cache")
-                except Exception as ex:  # pylint: disable=broad-except
-                    logger.exception(ex)
-                    logger.error(
-                        "Error reading cache: %s",
-                        utils.error_msg_from_exception(ex),
-                        exc_info=True,
-                    )
-                logger.info("Serving from cache")
-
-        if query_obj and not is_loaded:
-            if self.force_cached:
-                logger.warning(
-                    "force_cached (viz.py): value not found for cache key %s",
-                    cache_key,
-                )
-                raise CacheLoadError(_("Cached value not found"))
-            try:
-                invalid_columns = [
-                    col
-                    for col in get_column_names_from_columns(
-                        query_obj.get("columns") or []
-                    )
-                    + get_column_names_from_columns(query_obj.get("groupby") or [])
-                    + utils.get_column_names_from_metrics(
-                        cast(list[Metric], query_obj.get("metrics") or [])
-                    )
-                    if col not in self.datasource.column_names
-                ]
-                if invalid_columns:
-                    raise QueryObjectValidationError(
-                        _(
-                            "Columns missing in datasource: %(invalid_columns)s",
-                            invalid_columns=invalid_columns,
-                        )
-                    )
-                df = self.get_df(query_obj)
-                if self.status != QueryStatus.FAILED:
-                    current_app.config["STATS_LOGGER"].incr("loaded_from_source")
-                    if not self.force:
-                        current_app.config["STATS_LOGGER"].incr(
-                            "loaded_from_source_without_force"
-                        )
-                    is_loaded = True
-            except QueryObjectValidationError as ex:
-                error = dataclasses.asdict(
-                    SupersetError(
-                        message=str(ex),
-                        level=ErrorLevel.ERROR,
-                        error_type=SupersetErrorType.VIZ_GET_DF_ERROR,
-                    )
-                )
-                self.errors.append(error)
-                self.status = QueryStatus.FAILED
-            except SupersetErrorException:
-                # Let structured Superset errors (e.g. OAuth2RedirectError) propagate
-                # so the global Flask error handler serializes them.
-                raise
-            except Exception as ex:  # pylint: disable=broad-except
-                logger.exception(ex)
-
-                error = dataclasses.asdict(
-                    SupersetError(
-                        message=str(ex),
-                        level=ErrorLevel.ERROR,
-                        error_type=SupersetErrorType.VIZ_GET_DF_ERROR,
-                    )
-                )
-                self.errors.append(error)
-                self.status = QueryStatus.FAILED
-                # Only expose the raw stacktrace when explicitly enabled, mirroring
-                # the gating used elsewhere (e.g. superset.views.base.get_error_msg).
-                # ``get_stacktrace()`` itself returns ``None`` unless SHOW_STACKTRACE
-                # is set, so gating purely on that config keeps the two consistent.
-                if current_app.config.get("SHOW_STACKTRACE"):
-                    stacktrace = utils.get_stacktrace()
-
-            if is_loaded and cache_key and self.status != QueryStatus.FAILED:
-                set_and_log_cache(
-                    cache_instance=cache_manager.data_cache,
-                    cache_key=cache_key,
-                    cache_value={"df": df, "query": self.query},
-                    cache_timeout=cache_timeout,
-                    datasource_uid=self.datasource.uid,
-                )
-        return {
-            "cache_key": cache_key,
-            "cached_dttm": cache_value["dttm"] if cache_value is not None else None,
-            "cache_timeout": cache_timeout,
-            "df": df,
-            "errors": self.errors,
-            "form_data": self.form_data,
-            "is_cached": cache_value is not None,
-            "query": self.query,
-            "from_dttm": self.from_dttm,
-            "to_dttm": self.to_dttm,
-            "status": self.status,
-            "stacktrace": stacktrace,
-            "rowcount": len(df.index) if df is not None else 0,
-            "colnames": list(df.columns) if df is not None else None,
-            "coltypes": (
-                utils.extract_dataframe_dtypes(df, self.datasource)
-                if df is not None
-                else None
-            ),
-        }
-
-    @staticmethod
-    @deprecated(deprecated_in="3.0")
-    def json_dumps(query_obj: Any, sort_keys: bool = False) -> str:
-        return json.dumps(
-            query_obj,
-            default=json.json_int_dttm_ser,
-            ignore_nan=True,
-            sort_keys=sort_keys,
-        )
-
-    @staticmethod
-    @deprecated(deprecated_in="3.0")
-    def has_error(payload: VizPayload) -> bool:
-        return (
-            payload.get("status") == QueryStatus.FAILED
-            or payload.get("error") is not None
-            or bool(payload.get("errors"))
-        )
-
-    @deprecated(deprecated_in="3.0")
-    def payload_json_and_has_error(self, payload: VizPayload) -> tuple[str, bool]:
-        return self.json_dumps(payload), self.has_error(payload)
-
-    @property
-    @deprecated(deprecated_in="3.0")
-    def data(self) -> dict[str, Any]:
-        """This is the data object serialized to the js layer"""
-        content = {
-            "form_data": self.form_data,
-            "token": self.token,
-            "viz_name": self.viz_type,
-            "filter_select_enabled": self.datasource.filter_select_enabled,
-        }
-        return content
-
-    @deprecated(deprecated_in="3.0")
-    def get_csv(self) -> str | None:
-        df = self.get_df_payload()["df"]  # leverage caching logic
-        include_index = not isinstance(df.index, pd.RangeIndex)
-        return csv.df_to_escaped_csv(
-            df, index=include_index, **current_app.config["CSV_EXPORT"]
-        )
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        return df.to_dict(orient="records")
-
-    @property
-    @deprecated(deprecated_in="3.0")
-    def json_data(self) -> str:
-        return json.dumps(self.data)
-
-    @deprecated(deprecated_in="3.0")
-    def raise_for_access(self) -> None:
-        """
-        Raise an exception if the user cannot access the resource.
-
-        :raises SupersetSecurityException: If the user cannot access the resource
-        """
-
-        security_manager.raise_for_access(viz=self)
-
-
-class TimeTableViz(BaseViz):
-    """A data table with rich time-series related columns"""
-
-    viz_type = "time_table"
-    verbose_name = _("Time Table View")
-    credits = 'a <a href="https://github.com/airbnb/superset">Superset</a> original'
-    is_timeseries = True
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-
-        if not self.form_data.get("metrics"):
-            raise QueryObjectValidationError(_("Pick at least one metric"))
-
-        if self.form_data.get("groupby") and len(self.form_data["metrics"]) > 1:
-            raise QueryObjectValidationError(
-                _("When using 'Group By' you are limited to use a single metric")
-            )
-
-        sort_by = utils.get_first_metric_name(query_obj["metrics"])
-        is_asc = not query_obj.get("order_desc")
-        query_obj["orderby"] = [(sort_by, is_asc)]
-
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        if df.empty:
-            return None
-
-        columns = None
-        values: list[str] | str = self.metric_labels
-        if self.form_data.get("groupby"):
-            values = self.metric_labels[0]
-            columns = get_column_names(self.form_data.get("groupby"))
-        pt = df.pivot_table(index=DTTM_ALIAS, columns=columns, values=values)
-        pt.index = pt.index.map(str)
-        pt = pt.sort_index()
-        if isinstance(pt.columns, pd.MultiIndex):
-            pt.columns = [
-                FLAT_COLUMN_SEPARATOR.join(escape_separator(str(s)) for s in col)
-                for col in pt.columns
-            ]
-        return {
-            "records": pt.to_dict(orient="index"),
-            "columns": list(pt.columns),
-            "is_group_by": bool(self.form_data.get("groupby")),
-        }
-
-
-class CalHeatmapViz(BaseViz):
-    """Calendar heatmap."""
-
-    viz_type = "cal_heatmap"
-    verbose_name = _("Calendar Heatmap")
-    credits = "<a href=https://github.com/wa0x6e/cal-heatmap>cal-heatmap</a>"
-    is_timeseries = True
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:  # pylint: disable=too-many-locals  # noqa: C901
-        if df.empty:
-            return None
-
-        form_data = self.form_data
-        data = {}
-        records = df.to_dict("records")
-        for metric in self.metric_labels:
-            values = {}
-            for query_obj in records:
-                v = query_obj[DTTM_ALIAS]
-                if hasattr(v, "value"):
-                    v = v.value
-                values[str(v / 10**9)] = query_obj.get(metric)
-            data[metric] = values
-
-        try:
-            start, end = get_since_until(
-                relative_start=current_app.config["DEFAULT_RELATIVE_START_TIME"],
-                relative_end=current_app.config["DEFAULT_RELATIVE_END_TIME"],
-                time_range=form_data.get("time_range"),
-                since=form_data.get("since"),
-                until=form_data.get("until"),
-            )
-        except ValueError as ex:
-            raise QueryObjectValidationError(str(ex)) from ex
-        if not start or not end:
-            raise QueryObjectValidationError(
-                "Please provide both time bounds (Since and Until)"
-            )
-        domain = form_data.get("domain_granularity")
-        diff_delta = rdelta.relativedelta(end, start)
-        diff_secs = (end - start).total_seconds()
-
-        if domain == "year":
-            range_ = end.year - start.year + 1
-        elif domain == "month":
-            range_ = diff_delta.years * 12 + diff_delta.months + 1
-        elif domain == "week":
-            range_ = diff_delta.years * 53 + diff_delta.weeks + 1
-        elif domain == "day":
-            range_ = diff_secs // (24 * 60 * 60) + 1  # type: ignore
-        else:
-            range_ = diff_secs // (60 * 60) + 1  # type: ignore
-
-        return {
-            "data": data,
-            "start": start,
-            "domain": domain,
-            "subdomain": form_data.get("subdomain_granularity"),
-            "range": range_,
-        }
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-        query_obj["metrics"] = self.form_data.get("metrics")
-        mapping = {
-            "min": "PT1M",
-            "hour": "PT1H",
-            "day": "P1D",
-            "week": "P1W",
-            "month": "P1M",
-            "year": "P1Y",
-        }
-        query_obj["extras"]["time_grain_sqla"] = mapping[
-            self.form_data.get("subdomain_granularity", "min")
-        ]
-        return query_obj
-
-
-class NVD3Viz(BaseViz):
-    """Base class for all nvd3 vizs"""
-
-    credits = '<a href="http://nvd3.org/">NVD3.org</a>'
-    viz_type: str | None = None
-    verbose_name = "Base NVD3 Viz"
-    is_timeseries = False
-
-
-class BulletViz(NVD3Viz):
-    """Based on the NVD3 bullet chart"""
-
-    viz_type = "bullet"
-    verbose_name = _("Bullet Chart")
-    is_timeseries = False
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        form_data = self.form_data
-        query_obj = super().query_obj()
-        self.metric = form_data[  # pylint: disable=attribute-defined-outside-init
-            "metric"
-        ]
-
-        query_obj["metrics"] = [self.metric]
-        if not self.metric:
-            raise QueryObjectValidationError(_("Pick a metric to display"))
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        if df.empty:
-            return None
-        df["metric"] = df[[utils.get_metric_name(self.metric)]]
-        values = df["metric"].values
-        return {
-            "measures": values.tolist(),
-        }
-
-
-class NVD3TimeSeriesViz(NVD3Viz):
-    """A rich line chart component with tons of options"""
-
-    viz_type = "line"
-    verbose_name = _("Time Series - Line Chart")
-    sort_series = False
-    is_timeseries = True
-    pivot_fill_value: int | None = None
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-        sort_by = self.form_data.get(
-            "timeseries_limit_metric"
-        ) or utils.get_first_metric_name(query_obj.get("metrics") or [])
-        is_asc = not self.form_data.get("order_desc")
-        if sort_by:
-            sort_by_label = utils.get_metric_name(sort_by)
-            if sort_by_label not in utils.get_metric_names(query_obj["metrics"]):
-                query_obj["metrics"].append(sort_by)
-            query_obj["orderby"] = [(sort_by, is_asc)]
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def to_series(  # pylint: disable=too-many-branches  # noqa: C901
-        self, df: pd.DataFrame, classed: str = "", title_suffix: str = ""
-    ) -> list[dict[str, Any]]:
-        cols = []
-        for col in df.columns:
-            if col == "":
-                cols.append("N/A")
-            elif col is None:
-                cols.append("NULL")
-            else:
-                cols.append(col)
-        df.columns = cols
-        series = df.to_dict("series")
-
-        chart_data = []
-        for name in df.T.index.tolist():
-            ys = series[name]
-            if df[name].dtype.kind not in "biufc":
-                continue
-            series_title: list[str] | str | tuple[str, ...]
-            if isinstance(name, list):
-                series_title = [str(title) for title in name]
-            elif isinstance(name, tuple):
-                series_title = tuple(str(title) for title in name)
-            else:
-                series_title = str(name)
-            if (
-                isinstance(series_title, (list, tuple))
-                and len(series_title) > 1
-                and len(self.metric_labels) == 1
-            ):
-                # Removing metric from series name if only one metric
-                series_title = series_title[1:]
-            if title_suffix:
-                if isinstance(series_title, str):
-                    series_title = (series_title, title_suffix)
-                elif isinstance(series_title, list):
-                    series_title = series_title + [title_suffix]
-                elif isinstance(series_title, tuple):
-                    series_title = series_title + (title_suffix,)
-
-            values = []
-            non_nan_cnt = 0
-            for ds in df.index:
-                if ds in ys:
-                    data = {"x": ds, "y": ys[ds]}
-                    if not np.isnan(ys[ds]):
-                        non_nan_cnt += 1
-                else:
-                    data = {}
-                values.append(data)
-
-            if non_nan_cnt == 0:
-                continue
-
-            data = {"key": series_title, "values": values}
-            if classed:
-                data["classed"] = classed
-            chart_data.append(data)
-        return chart_data
-
-    @deprecated(deprecated_in="3.0")
-    def process_data(self, df: pd.DataFrame, aggregate: bool = False) -> VizData:
-        if df.empty:
-            return df
-
-        if aggregate:
-            df = df.pivot_table(
-                index=DTTM_ALIAS,
-                columns=get_column_names(self.form_data.get("groupby")),
-                values=self.metric_labels,
-                fill_value=0,
-                aggfunc=sum,
-            )
-        else:
-            df = df.pivot_table(
-                index=DTTM_ALIAS,
-                columns=get_column_names(self.form_data.get("groupby")),
-                values=self.metric_labels,
-                fill_value=self.pivot_fill_value,
-            )
-
-        rule = self.form_data.get("resample_rule")
-        method = self.form_data.get("resample_method")
-
-        if rule and method:
-            # ``method`` comes straight from ``form_data`` and may be a
-            # non-string (e.g. a list) for malformed requests; guard the
-            # membership test so unsupported input returns a controlled
-            # validation error instead of an unhashable-type ``TypeError``.
-            if not isinstance(method, str) or method not in ALLOWED_RESAMPLE_METHODS:
-                raise QueryObjectValidationError(
-                    _(
-                        "Resample method '%(method)s' is not supported.",
-                        method=method,
-                    )
-                )
-            df = getattr(df.resample(rule), method)()
-
-        if self.sort_series:
-            dfs = df.sum()
-            dfs.sort_values(ascending=False, inplace=True)
-            df = df[dfs.index]
-
-        df = self.apply_rolling(df)
-        if self.form_data.get("contribution"):
-            dft = df.T
-            df = (dft / dft.sum()).T
-
-        return df
-
-    @deprecated(deprecated_in="3.0")
-    def run_extra_queries(self) -> None:
-        time_compare = self.form_data.get("time_compare") or []
-        # backwards compatibility
-        if not isinstance(time_compare, list):
-            time_compare = [time_compare]
-
-        max_time_compare = current_app.config["VIZ_TIME_COMPARE_MAX"]
-        if len(time_compare) > max_time_compare:
-            raise QueryObjectValidationError(
-                _(
-                    "Too many time comparisons requested. The maximum allowed is "
-                    "%(max)s, but %(count)s were requested.",
-                    max=max_time_compare,
-                    count=len(time_compare),
-                )
-            )
-
-        for option in time_compare:
-            query_object = self.query_obj()
-            try:
-                delta = parse_past_timedelta(option)
-            except ValueError as ex:
-                raise QueryObjectValidationError(str(ex)) from ex
-            query_object["inner_from_dttm"] = query_object["from_dttm"]
-            query_object["inner_to_dttm"] = query_object["to_dttm"]
-
-            if not query_object["from_dttm"] or not query_object["to_dttm"]:
-                raise QueryObjectValidationError(
-                    _(
-                        "An enclosed time range (both start and end) must be specified "
-                        "when using a Time Comparison."
-                    )
-                )
-            query_object["from_dttm"] -= delta
-            query_object["to_dttm"] -= delta
-
-            df2 = self.get_df_payload(query_object, time_compare=option).get("df")
-            if df2 is not None and DTTM_ALIAS in df2:
-                dttm_series = df2[DTTM_ALIAS] + delta
-                df2 = df2.drop(DTTM_ALIAS, axis=1)
-                df2 = pd.concat([dttm_series, df2], axis=1)
-                label = f"{option} offset"
-                df2 = self.process_data(df2)
-                self._extra_chart_data.append((label, df2))
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        comparison_type = self.form_data.get("comparison_type") or "values"
-        df = self.process_data(df)
-        if comparison_type == "values":
-            # Filter out series with all NaN
-            chart_data = self.to_series(df.dropna(axis=1, how="all"))
-
-            for i, (label, df2) in enumerate(self._extra_chart_data):
-                chart_data.extend(
-                    self.to_series(df2, classed=f"time-shift-{i}", title_suffix=label)
-                )
-        else:
-            chart_data = []
-            for i, (label, df2) in enumerate(self._extra_chart_data):
-                # reindex df2 into the df2 index
-                combined_index = df.index.union(df2.index)
-                df2 = (
-                    df2.reindex(combined_index)
-                    .interpolate(method="time")
-                    .reindex(df.index)
-                )
-
-                if comparison_type == "absolute":
-                    diff = df - df2
-                elif comparison_type == "percentage":
-                    diff = (df - df2) / df2
-                elif comparison_type == "ratio":
-                    diff = df / df2
-                else:
-                    raise QueryObjectValidationError(
-                        f"Invalid `comparison_type`: {comparison_type}"
-                    )
-
-                # remove leading/trailing NaNs from the time shift difference
-                diff = diff[diff.first_valid_index() : diff.last_valid_index()]
-
-                chart_data.extend(
-                    self.to_series(diff, classed=f"time-shift-{i}", title_suffix=label)
-                )
-
-        if not self.sort_series:
-            chart_data = sorted(chart_data, key=lambda x: tuple(x["key"]))
-        return chart_data
-
-
-class NVD3TimePivotViz(NVD3TimeSeriesViz):
-    """Time Series - Periodicity Pivot"""
-
-    viz_type = "time_pivot"
-    sort_series = True
-    verbose_name = _("Time Series - Period Pivot")
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-        query_obj["metrics"] = [self.form_data.get("metric")]
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        if df.empty:
-            return None
-
-        df = self.process_data(df)
-        freq = to_offset(self.form_data.get("freq"))
-        try:
-            freq = type(freq)(freq.n, normalize=True, **freq.kwds)
-        except ValueError:
-            freq = type(freq)(freq.n, **freq.kwds)
-        df.index.name = None
-        df[DTTM_ALIAS] = df.index.map(freq.rollback)
-        df["ranked"] = df[DTTM_ALIAS].rank(method="dense", ascending=False) - 1
-        df.ranked = df.ranked.map(int)
-        df["series"] = "-" + df.ranked.map(str)
-        df["series"] = df["series"].str.replace("-0", "current")
-        rank_lookup = {
-            row["series"]: row["ranked"] for row in df.to_dict(orient="records")
-        }
-        max_ts = df[DTTM_ALIAS].max()
-        max_rank = df["ranked"].max()
-        df[DTTM_ALIAS] = df.index + (max_ts - df[DTTM_ALIAS])
-        df = df.pivot_table(
-            index=DTTM_ALIAS,
-            columns="series",
-            values=utils.get_metric_name(self.form_data["metric"]),
-        )
-        chart_data = self.to_series(df)
-        for series in chart_data:
-            series["rank"] = rank_lookup[series["key"]]
-            series["perc"] = 1 - (series["rank"] / (max_rank + 1))
-        return chart_data
-
-
-class ChordViz(BaseViz):
-    """A Chord diagram"""
-
-    viz_type = "chord"
-    verbose_name = _("Directed Force Layout")
-    credits = '<a href="https://github.com/d3/d3-chord">Bostock</a>'
-    is_timeseries = False
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-        query_obj["groupby"] = [
-            self.form_data.get("groupby"),
-            self.form_data.get("columns"),
-        ]
-        query_obj["metrics"] = [self.form_data.get("metric")]
-        if self.form_data.get("sort_by_metric", False):
-            query_obj["orderby"] = [(query_obj["metrics"][0], False)]
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        if df.empty:
-            return None
-
-        df.columns = ["source", "target", "value"]
-
-        # Preparing a symmetrical matrix like d3.chords calls for
-        nodes = list(set(df["source"]) | set(df["target"]))
-        matrix = {}
-        for source, target in product(nodes, nodes):
-            matrix[(source, target)] = 0
-        for source, target, value in df.to_records(index=False):
-            matrix[(source, target)] = value
-        return {
-            "nodes": list(nodes),
-            "matrix": [[matrix[(n1, n2)] for n1 in nodes] for n2 in nodes],
-        }
-
-
-class CountryMapViz(BaseViz):
-    """A country centric"""
-
-    viz_type = "country_map"
-    verbose_name = _("Country Map")
-    is_timeseries = False
-    credits = "From bl.ocks.org By john-guerra"
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-        metric = self.form_data.get("metric")
-        entity = self.form_data.get("entity")
-        if not self.form_data.get("select_country"):
-            raise QueryObjectValidationError("Must specify a country")
-        if not metric:
-            raise QueryObjectValidationError("Must specify a metric")
-        if not entity:
-            raise QueryObjectValidationError("Must provide ISO codes")
-        query_obj["metrics"] = [metric]
-        query_obj["groupby"] = [entity]
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        if df.empty:
-            return None
-        cols = get_column_names([self.form_data.get("entity")])  # type: ignore
-        metric = self.metric_labels[0]
-        cols += [metric]
-        ndf = df[cols]
-        df = ndf
-        df.columns = ["country_id", "metric"]
-        return df.to_dict(orient="records")
-
-
-class WorldMapViz(BaseViz):
-    """A country centric world map"""
-
-    viz_type = "world_map"
-    verbose_name = _("World Map")
-    is_timeseries = False
-    credits = 'datamaps on <a href="https://www.npmjs.com/package/datamaps">npm</a>'
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-        query_obj["groupby"] = [self.form_data["entity"]]
-        if self.form_data.get("sort_by_metric", False):
-            query_obj["orderby"] = [(query_obj["metrics"][0], False)]
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        if df.empty:
-            return None
-
-        # pylint: disable=import-outside-toplevel
-        from superset.examples import countries
-
-        cols = get_column_names([self.form_data.get("entity")])  # type: ignore
-        metric = utils.get_metric_name(self.form_data["metric"])
-        secondary_metric = (
-            utils.get_metric_name(self.form_data["secondary_metric"])
-            if self.form_data.get("secondary_metric")
-            else None
-        )
-        columns = ["country", "m1", "m2"]
-        if metric == secondary_metric:
-            ndf = df[cols]
-            ndf["m1"] = df[metric]
-            ndf["m2"] = ndf["m1"]
-        else:
-            if secondary_metric:
-                cols += [metric, secondary_metric]
-            else:
-                cols += [metric]
-                columns = ["country", "m1"]
-            ndf = df[cols]
-        df = ndf
-        df.columns = columns
-        data = df.to_dict(orient="records")
-        for row in data:
-            country = None
-            if isinstance(row["country"], str):
-                if "country_fieldtype" in self.form_data:
-                    country = countries.get(
-                        self.form_data["country_fieldtype"], row["country"]
-                    )
-            if country:
-                row["code"] = country[self.form_data["country_fieldtype"]]
-                row["country"] = country["cca3"]
-                row["latitude"] = country["lat"]
-                row["longitude"] = country["lng"]
-                row["name"] = country["name"]
-            else:
-                row["country"] = "XXX"
-        return data
-
-
-class ParallelCoordinatesViz(BaseViz):
-    """Interactive parallel coordinate implementation
-
-    Uses this amazing javascript library
-    https://github.com/syntagmatic/parallel-coordinates
-    """
-
-    viz_type = "para"
-    verbose_name = _("Parallel Coordinates")
-    credits = (
-        '<a href="https://syntagmatic.github.io/parallel-coordinates/">'
-        "Syntagmatic's library</a>"
-    )
-    is_timeseries = False
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-        query_obj["groupby"] = [self.form_data.get("series")]
-        if sort_by := self.form_data.get("timeseries_limit_metric"):
-            sort_by_label = utils.get_metric_name(sort_by)
-            if sort_by_label not in utils.get_metric_names(query_obj["metrics"]):
-                query_obj["metrics"].append(sort_by)
-            if self.form_data.get("order_desc"):
-                query_obj["orderby"] = [
-                    (sort_by, not self.form_data.get("order_desc", True))
-                ]
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        return df.to_dict(orient="records")
-
-
-class HorizonViz(NVD3TimeSeriesViz):
-    """Horizon chart
-
-    https://www.npmjs.com/package/d3-horizon-chart
-    """
-
-    viz_type = "horizon"
-    verbose_name = _("Horizon Charts")
-    credits = (
-        '<a href="https://www.npmjs.com/package/d3-horizon-chart">d3-horizon-chart</a>'
-    )
-
-
-class DeckGLMultiLayer(BaseViz):
-    """Pile on multiple DeckGL layers"""
-
-    viz_type = "deck_multi"
-    verbose_name = _("Deck.gl - Multiple Layers")
-
-    is_timeseries = False
-    credits = '<a href="https://uber.github.io/deck.gl/">deck.gl</a>'
-
-    @staticmethod
-    def _merge_filter_metadata(
-        *filter_groups: list[dict[str, Any]] | None,
-    ) -> list[dict[str, Any]]:
-        """Merge multiple filter metadata lists, de-duplicating identical entries.
-
-        Used to combine the applied/rejected filter metadata reported by each
-        child layer into a single list for the multi-layer chart payload.
-        """
-        merged_filters: list[dict[str, Any]] = []
-        seen_filters: set[str] = set()
-
-        for filters in filter_groups:
-            for filter_metadata in filters or []:
-                if not isinstance(filter_metadata, dict):
-                    continue
-
-                cache_key = json.dumps(filter_metadata, sort_keys=True)
-                if cache_key in seen_filters:
-                    continue
-
-                merged_filters.append(filter_metadata)
-                seen_filters.add(cache_key)
-
-        return merged_filters
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        return {}
-
-    def _filter_items_by_scope(
-        self,
-        items: list[Any],
-        layer_index: int,
-        layer_filter_scope: dict[str, list[int]],
-    ) -> list[Any]:
-        """Filter items based on layer filter scope."""
-        filtered_items = []
-        for filter_item in items:
-            filter_id = getattr(filter_item, "filterId", None)
-            if filter_id:
-                filter_scope = layer_filter_scope.get(filter_id, [])
-                if filter_scope is None:
-                    filter_scope = []
-                if not filter_scope or layer_index in filter_scope:
-                    filtered_items.append(filter_item)
-            else:
-                filtered_items.append(filter_item)
-        return filtered_items
-
-    def _process_extra_form_data_filters(
-        self,
-        layer_index: int,
-        layer_filter_scope: dict[str, list[int]],
-        filter_data_mapping: dict[str, list[Any]],
-        extra_form_data: dict[str, Any],
-    ) -> dict[str, Any]:
-        """Process extra_form_data filters with layer-specific filtering."""
-        if not extra_form_data or not filter_data_mapping:
-            return extra_form_data
-
-        filtered_extra_form_data_filters = []
-        for filter_id, filter_scope in layer_filter_scope.items():
-            if filter_scope is None:
-                filter_scope = []
-
-            if not filter_scope or layer_index in filter_scope:
-                filters_from_this_filter = filter_data_mapping.get(filter_id, [])
-                filtered_extra_form_data_filters.extend(filters_from_this_filter)
-
-        return {
-            **extra_form_data,
-            "filters": filtered_extra_form_data_filters,
-        }
-
-    def _apply_layer_filtering(
-        self, form_data: dict[str, Any], layer_index: int
-    ) -> dict[str, Any]:
-        """Apply layer-specific filtering to form data."""
-        layer_filter_scope = self.form_data.get("layer_filter_scope", {})
-        filter_data_mapping = self.form_data.get("filter_data_mapping", {})
-
-        if not layer_filter_scope:
-            form_data["extra_filters"] = self.form_data.get("extra_filters", [])
-            form_data["adhoc_filters"] = self.form_data.get("adhoc_filters")
-            form_data["extra_form_data"] = self.form_data.get("extra_form_data")
-            return form_data
-
-        filtered_extra_filters = self._filter_items_by_scope(
-            self.form_data.get("extra_filters", []), layer_index, layer_filter_scope
-        )
-        filtered_adhoc_filters = self._filter_items_by_scope(
-            self.form_data.get("adhoc_filters", []), layer_index, layer_filter_scope
-        )
-
-        extra_form_data = self.form_data.get("extra_form_data", {})
-        filtered_extra_form_data = self._process_extra_form_data_filters(
-            layer_index, layer_filter_scope, filter_data_mapping, extra_form_data
-        )
-
-        form_data["extra_filters"] = filtered_extra_filters
-        form_data["adhoc_filters"] = filtered_adhoc_filters
-        form_data["extra_form_data"] = filtered_extra_form_data
-        return form_data
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        # Late imports to avoid circular import issues
-        # pylint: disable=import-outside-toplevel
-        from superset import db
-        from superset.models.slice import Slice
-
-        slice_ids = self.form_data.get("deck_slices") or []
-        max_slices = current_app.config["DECK_MULTI_MAX_SLICES"]
-        if len(slice_ids) > max_slices:
-            raise QueryObjectValidationError(
-                _(
-                    "Too many sub-slices requested. The maximum allowed is "
-                    "%(max)s, but %(count)s were requested.",
-                    max=max_slices,
-                    count=len(slice_ids),
-                )
-            )
-        slices = db.session.query(Slice).filter(Slice.id.in_(slice_ids)).all()
-
-        features: dict[str, list[Any]] = {}
-        self.applied_filters = []
-        self.rejected_filters = []
-
-        for layer_index, slc in enumerate(slices):
-            form_data = slc.form_data
-            form_data = self._apply_layer_filtering(form_data, layer_index)
-
-            viz_type_name = form_data.get("viz_type")
-            viz_class = viz_types.get(viz_type_name)
-            if not viz_class:
-                continue  # skip unknown viz types
-
-            viz_instance = viz_class(datasource=slc.datasource, form_data=form_data)
-            payload = viz_instance.get_payload()
-            if payload:
-                self.applied_filters = self._merge_filter_metadata(
-                    self.applied_filters,
-                    payload.get("applied_filters"),
-                )
-                self.rejected_filters = self._merge_filter_metadata(
-                    self.rejected_filters,
-                    payload.get("rejected_filters"),
-                )
-
-            if (
-                payload
-                and "data" in payload
-                and payload["data"] is not None
-                and "features" in payload["data"]
-            ):
-                if viz_type_name not in features:
-                    features[viz_type_name] = []
-                features[viz_type_name].extend(payload["data"]["features"])
-
-        return {
-            "features": features,
-            "mapboxApiKey": current_app.config["MAPBOX_API_KEY"],
-            "slices": [slc.data for slc in slices if slc.data is not None],
-        }
-
-    @deprecated(deprecated_in="3.0")
-    def get_payload(self, query_obj: QueryObjectDict | None = None) -> VizPayload:
-        """Extend the base payload with merged child-layer filter metadata.
-
-        The applied/rejected filter metadata collected from each sub-slice in
-        ``get_data`` is merged into the base payload so dashboard filter badges
-        reflect the filters applied across all layers.
-        """
-        payload = super().get_payload(query_obj)
-        payload["applied_filters"] = self._merge_filter_metadata(
-            payload.get("applied_filters"),
-            self.applied_filters,
-        )
-        payload["rejected_filters"] = self._merge_filter_metadata(
-            payload.get("rejected_filters"),
-            self.rejected_filters,
-        )
-        return payload
-
-
-class BaseDeckGLViz(BaseViz):
-    """Base class for deck.gl visualizations"""
-
-    is_timeseries = False
-    credits = '<a href="https://uber.github.io/deck.gl/">deck.gl</a>'
-    spatial_control_keys: list[str] = []
-
-    def _extract_tooltip_columns(self) -> list[str]:
-        """Extract column names from tooltip_contents configuration."""
-        tooltip_columns = []
-        if tooltip_contents := self.form_data.get("tooltip_contents", []):
-            for item in tooltip_contents:
-                if isinstance(item, str):
-                    tooltip_columns.append(item)
-                elif isinstance(item, dict) and item.get("item_type") == "column":
-                    column_name = item.get("column_name")
-                    if column_name:
-                        tooltip_columns.append(column_name)
-        return tooltip_columns
-
-    def _add_tooltip_columns_to_query(
-        self, query_obj: QueryObjectDict, tooltip_columns: list[str]
-    ) -> None:
-        """Add tooltip columns to the appropriate query field."""
-        if not tooltip_columns:
-            return
-
-        if query_obj.get("metrics"):
-            existing_groupby = set(query_obj.get("groupby", []))
-            for col in tooltip_columns:
-                if col not in existing_groupby:
-                    query_obj.setdefault("groupby", []).append(col)
-        else:
-            existing_columns = set(query_obj.get("columns", []))
-            for col in tooltip_columns:
-                if col not in existing_columns:
-                    query_obj.setdefault("columns", []).append(col)
-
-    def _integrate_tooltip_columns(self, query_obj: QueryObjectDict) -> QueryObjectDict:
-        """Helper method to integrate tooltip columns into query object."""
-        tooltip_columns = self._extract_tooltip_columns()
-        self._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-        return query_obj
-
-    def _add_tooltip_properties(
-        self, properties: dict[str, Any], data: dict[str, Any]
-    ) -> dict[str, Any]:
-        """Helper method to add tooltip fields to properties dictionary."""
-        tooltip_columns = self._extract_tooltip_columns()
-        for col in tooltip_columns:
-            if col in data:
-                properties[col] = data[col]
-        return properties
-
-    def _get_base_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        """Get base properties common to most deck.gl layers."""
-        return {
-            "position": data.get("spatial"),
-            "weight": (data.get(self.metric_label) if self.metric_label else None) or 1,
-        }
-
-    def _setup_metric_label(self) -> None:
-        """Setup metric label for the visualization."""
-        self.metric_label: str | None = (  # pylint: disable=attribute-defined-outside-init
-            utils.get_metric_name(self.metric) if self.metric else None
-        )
-
-    def __init__(
-        self, datasource: BaseDatasource, form_data: dict[str, Any], **kwargs: Any
-    ) -> None:
-        # Apply layer-specific filtering for deck multi layer charts in edit mode
-        if self._should_apply_layer_filtering(form_data):
-            form_data = self._apply_multilayer_filtering(form_data)
-        super().__init__(datasource, form_data, **kwargs)
-
-    def _should_apply_layer_filtering(self, form_data: dict[str, Any]) -> bool:
-        """Check if this is a deck layer that's part of a multilayer setup."""
-        return (
-            "slice_id" in form_data
-            and "adhoc_filters" in form_data
-            and self._has_layer_scoped_filters(form_data)
-        )
-
-    def _has_layer_scoped_filters(self, form_data: dict[str, Any]) -> bool:
-        """Check if any filter has layerFilterScope (indicates multilayer context)."""
-        for filter_item in form_data.get("adhoc_filters", []):
-            if (
-                isinstance(filter_item, dict)
-                and filter_item.get("layerFilterScope") is not None
-            ):
-                return True
-        return False
-
-    def _apply_multilayer_filtering(self, form_data: dict[str, Any]) -> dict[str, Any]:
-        """
-        Filter adhoc_filters based on layer scope for this specific layer.
-
-        In deck multi-layer charts, each individual layer should only receive:
-        1. Global filters (filters without layerFilterScope)
-        2. Filters specifically scoped to this layer
-
-        This prevents over-filtering when multiple layer-scoped filters are present.
-        """
-        slice_id = form_data.get("slice_id")
-        deck_slices = self._get_deck_slices_from_filters(form_data)
-
-        if not deck_slices or slice_id not in deck_slices:
-            return form_data
-
-        layer_index = deck_slices.index(slice_id)
-        filtered_adhoc_filters = []
-
-        for filter_item in form_data.get("adhoc_filters", []):
-            layer_scope = self._get_filter_layer_scope(filter_item)
-
-            # Include global filters (no layer scope) or filters scoped to this layer
-            if layer_scope is None or layer_index in layer_scope:
-                filtered_adhoc_filters.append(filter_item)
-
-        modified_form_data = form_data.copy()
-        modified_form_data["adhoc_filters"] = filtered_adhoc_filters
-        return modified_form_data
-
-    def _get_deck_slices_from_filters(
-        self, form_data: dict[str, Any]
-    ) -> list[int] | None:
-        """Extract deck_slices from any filter that contains it."""
-        for filter_item in form_data.get("adhoc_filters", []):
-            if isinstance(filter_item, dict) and "deck_slices" in filter_item:
-                return filter_item["deck_slices"]
-        return None
-
-    def _get_filter_layer_scope(self, filter_item: Any) -> list[int] | None:
-        """Extract layerFilterScope from a filter item."""
-        if isinstance(filter_item, dict):
-            return filter_item.get("layerFilterScope")
-        return getattr(filter_item, "layerFilterScope", None)
-
-    @deprecated(deprecated_in="3.0")
-    def get_metrics(self) -> list[str]:
-        # pylint: disable=attribute-defined-outside-init
-        self.metric = self.form_data.get("size")
-        return [self.metric] if self.metric else []
-
-    @deprecated(deprecated_in="3.0")
-    def process_spatial_query_obj(self, key: str, group_by: list[str]) -> None:
-        group_by.extend(self.get_spatial_columns(key))
-
-    @deprecated(deprecated_in="3.0")
-    def get_spatial_columns(self, key: str) -> list[str]:
-        spatial = self.form_data.get(key)
-        if spatial is None:
-            raise ValueError(_("Bad spatial key"))
-
-        if spatial.get("type") == "latlong":
-            return [spatial.get("lonCol"), spatial.get("latCol")]
-
-        if spatial.get("type") == "delimited":
-            return [spatial.get("lonlatCol")]
-
-        if spatial.get("type") == "geohash":
-            return [spatial.get("geohashCol")]
-        return []
-
-    @staticmethod
-    @deprecated(deprecated_in="3.0")
-    def parse_coordinates(latlong: Any) -> tuple[float, float] | None:
-        if not latlong:
-            return None
-        try:
-            point = Point(latlong)
-            return (point.latitude, point.longitude)
-        except Exception as ex:
-            raise SpatialException(
-                _("Invalid spatial point encountered: %(latlong)s", latlong=latlong)
-            ) from ex
-
-    @staticmethod
-    @deprecated(deprecated_in="3.0")
-    def reverse_geohash_decode(geohash_code: str) -> tuple[str, str]:
-        lat, lng = pygeohash.decode(geohash_code)
-        return (lng, lat)
-
-    @staticmethod
-    @deprecated(deprecated_in="3.0")
-    def reverse_latlong(df: pd.DataFrame, key: str) -> None:
-        df[key] = [tuple(reversed(o)) for o in df[key] if isinstance(o, (list, tuple))]
-
-    @deprecated(deprecated_in="3.0")
-    def process_spatial_data_obj(self, key: str, df: pd.DataFrame) -> pd.DataFrame:
-        spatial = self.form_data.get(key)
-        if spatial is None:
-            raise ValueError(_("Bad spatial key"))
-
-        if spatial.get("type") == "latlong":
-            df[key] = list(
-                zip(
-                    pd.to_numeric(df[spatial.get("lonCol")], errors="coerce"),
-                    pd.to_numeric(df[spatial.get("latCol")], errors="coerce"),
-                    strict=False,
-                )
-            )
-        elif spatial.get("type") == "delimited":
-            lon_lat_col = spatial.get("lonlatCol")
-            df[key] = df[lon_lat_col].apply(self.parse_coordinates)
-            del df[lon_lat_col]
-        elif spatial.get("type") == "geohash":
-            df[key] = df[spatial.get("geohashCol")].map(self.reverse_geohash_decode)
-            del df[spatial.get("geohashCol")]
-
-        if spatial.get("reverseCheckbox"):
-            self.reverse_latlong(df, key)
-
-        if df.get(key) is None:
-            raise NullValueException(
-                _(
-                    "Encountered invalid NULL spatial entry, \
-                                       please consider filtering those out"
-                )
-            )
-        return df
-
-    @deprecated(deprecated_in="3.0")
-    def add_null_filters(self) -> None:
-        spatial_columns = set()
-        for key in self.spatial_control_keys:
-            for column in self.get_spatial_columns(key):
-                spatial_columns.add(column)
-
-        if self.form_data.get("adhoc_filters") is None:
-            self.form_data["adhoc_filters"] = []
-
-        if line_column := self.form_data.get("line_column"):
-            spatial_columns.add(line_column)
-
-        for column in sorted(spatial_columns):
-            filter_ = simple_filter_to_adhoc(
-                {"col": column, "op": "IS NOT NULL", "val": ""}
-            )
-            self.form_data["adhoc_filters"].append(filter_)
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        # add NULL filters
-        if self.form_data.get("filter_nulls", True):
-            self.add_null_filters()
-
-        query_obj = super().query_obj()
-        group_by: list[str] = []
-
-        for key in self.spatial_control_keys:
-            self.process_spatial_query_obj(key, group_by)
-
-        if self.form_data.get("dimension"):
-            group_by += [self.form_data["dimension"]]
-
-        if self.form_data.get("js_columns"):
-            group_by += self.form_data.get("js_columns") or []
-        # Ensure this value is sorted so that it does not
-        # cause the cache key generation (which hashes the
-        # query object) to generate different keys for values
-        # that should be considered the same.
-        group_by = sorted(set(group_by))
-        if metrics := self.get_metrics():
-            query_obj["groupby"] = group_by
-            query_obj["metrics"] = metrics
-            query_obj["columns"] = []
-            first_metric = query_obj["metrics"][0]
-            query_obj["orderby"] = [
-                (first_metric, not self.form_data.get("order_desc", True))
-            ]
-        else:
-            query_obj["columns"] = group_by
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_js_columns(self, data: dict[str, Any]) -> dict[str, Any]:
-        cols = self.form_data.get("js_columns") or []
-        return {col: data.get(col) for col in cols}
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        if df.empty:
-            return None
-
-        # Processing spatial info
-        for key in self.spatial_control_keys:
-            df = self.process_spatial_data_obj(key, df)
-
-        features = []
-        for data in df.to_dict(orient="records"):
-            feature = self.get_properties(data)
-            extra_props = self.get_js_columns(data)
-            if extra_props:
-                feature["extraProps"] = extra_props
-            features.append(feature)
-
-        return {
-            "features": features,
-            "mapboxApiKey": current_app.config["MAPBOX_API_KEY"],
-            "metricLabels": self.metric_labels,
-        }
-
-    @deprecated(deprecated_in="3.0")
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        raise NotImplementedError()
-
-
-class DeckScatterViz(BaseDeckGLViz):
-    """deck.gl's ScatterLayer"""
-
-    viz_type = "deck_scatter"
-    verbose_name = _("Deck.gl - Scatter plot")
-    spatial_control_keys = ["spatial"]
-    is_timeseries = True
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        # pylint: disable=attribute-defined-outside-init
-        self.is_timeseries = bool(self.form_data.get("time_grain_sqla"))
-        self.point_radius_fixed = self.form_data.get("point_radius_fixed") or {
-            "type": "fix",
-            "value": 500,
-        }
-
-        query_obj = super().query_obj()
-        return self._integrate_tooltip_columns(query_obj)
-
-    @deprecated(deprecated_in="3.0")
-    def get_metrics(self) -> list[str]:
-        # pylint: disable=attribute-defined-outside-init
-        self.metric = None
-        if self.point_radius_fixed.get("type") == "metric":
-            self.metric = self.point_radius_fixed["value"]
-            return [self.metric]
-        return []
-
-    @deprecated(deprecated_in="3.0")
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        properties = {
-            "metric": data.get(self.metric_label) if self.metric_label else None,
-            "radius": (
-                self.fixed_value
-                if self.fixed_value
-                else data.get(self.metric_label)
-                if self.metric_label
-                else None
-            ),
-            "cat_color": data.get(self.dim) if self.dim else None,
-            "position": data.get("spatial"),
-            DTTM_ALIAS: data.get(DTTM_ALIAS),
-        }
-
-        return self._add_tooltip_properties(properties, data)
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        self._setup_metric_label()
-        # pylint: disable=attribute-defined-outside-init
-        self.point_radius_fixed = self.form_data.get("point_radius_fixed")
-        self.fixed_value = None
-        self.dim = self.form_data.get("dimension")
-        if self.point_radius_fixed and self.point_radius_fixed.get("type") != "metric":
-            self.fixed_value = self.point_radius_fixed.get("value")
-        return super().get_data(df)
-
-
-class DeckScreengrid(BaseDeckGLViz):
-    """deck.gl's ScreenGridLayer"""
-
-    viz_type = "deck_screengrid"
-    verbose_name = _("Deck.gl - Screen Grid")
-    spatial_control_keys = ["spatial"]
-    is_timeseries = True
-
-    def _extract_tooltip_columns(self) -> list[str]:
-        """Extract column names from tooltip_contents configuration."""
-        tooltip_columns = []
-        if tooltip_contents := self.form_data.get("tooltip_contents", []):
-            for item in tooltip_contents:
-                if isinstance(item, str):
-                    tooltip_columns.append(item)
-                elif isinstance(item, dict) and item.get("item_type") == "column":
-                    column_name = item.get("column_name")
-                    if column_name:
-                        tooltip_columns.append(column_name)
-        return tooltip_columns
-
-    def _add_tooltip_columns_to_query(
-        self, query_obj: QueryObjectDict, tooltip_columns: list[str]
-    ) -> None:
-        """Add tooltip columns to the appropriate query field."""
-        if not tooltip_columns:
-            return
-
-        if query_obj.get("metrics"):
-            # Add to groupby when metrics exist
-            existing_groupby = set(query_obj.get("groupby", []))
-            for col in tooltip_columns:
-                if col not in existing_groupby:
-                    query_obj.setdefault("groupby", []).append(col)
-        else:
-            # Add to columns when no metrics
-            existing_columns = set(query_obj.get("columns", []))
-            for col in tooltip_columns:
-                if col not in existing_columns:
-                    query_obj.setdefault("columns", []).append(col)
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        self.is_timeseries = bool(self.form_data.get("time_grain_sqla"))
-        tooltip_columns = self._extract_tooltip_columns()
-        query_obj = super().query_obj()
-        self._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        properties = {
-            "position": data.get("spatial"),
-            "weight": (data.get(self.metric_label) if self.metric_label else None) or 1,
-            "__timestamp": data.get(DTTM_ALIAS) or data.get("__time"),
-        }
-
-        # For Screengrid, the frontend handles the cell-to-points mapping
-        # The backend just provides the aggregated data and tooltip fields
-
-        # For Screengrid, automatically collect all values for tooltip fields
-        # When user selects LON as tooltip field, {{ LON }} will show all LON values
-        tooltip_columns = self._extract_tooltip_columns()
-        for col in tooltip_columns:
-            if col in data:
-                # For Screengrid, each cell contains multiple points, provide as-is
-                # The frontend will handle displaying multiple values appropriately
-                properties[col] = data[col]
-
-        return properties
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        self.metric_label = (  # pylint: disable=attribute-defined-outside-init
-            utils.get_metric_name(self.metric) if self.metric else None
-        )
-        return super().get_data(df)
-
-
-class DeckGrid(BaseDeckGLViz):
-    """deck.gl's GridLayer"""
-
-    viz_type = "deck_grid"
-    verbose_name = _("Deck.gl - 3D Grid")
-    spatial_control_keys = ["spatial"]
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        tooltip_columns = self._extract_tooltip_columns()
-        query_obj = super().query_obj()
-        self._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        properties = {
-            "position": data.get("spatial"),
-            "weight": (data.get(self.metric_label) if self.metric_label else None) or 1,
-        }
-        # Add tooltip fields to properties
-        tooltip_columns = self._extract_tooltip_columns()
-        for col in tooltip_columns:
-            if col in data:
-                properties[col] = data[col]
-        return properties
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        self.metric_label = (  # pylint: disable=attribute-defined-outside-init
-            utils.get_metric_name(self.metric) if self.metric else None
-        )
-        return super().get_data(df)
-
-
-@deprecated(deprecated_in="3.0")
-def geohash_to_json(geohash_code: str) -> list[list[float]]:
-    # Get the center and the error margins
-    lat, lon, lat_err, lon_err = pygeohash.decode_exactly(geohash_code)
-    # Calculate the Bounding Box
-    bbox = {
-        "n": lat + lat_err,
-        "s": lat - lat_err,
-        "e": lon + lon_err,
-        "w": lon - lon_err,
-    }
-    return [
-        [bbox["w"], bbox["n"]],
-        [bbox["e"], bbox["n"]],
-        [bbox["e"], bbox["s"]],
-        [bbox["w"], bbox["s"]],
-        [bbox["w"], bbox["n"]],
-    ]
-
-
-class DeckPathViz(BaseDeckGLViz):
-    """deck.gl's PathLayer"""
-
-    viz_type = "deck_path"
-    verbose_name = _("Deck.gl - Paths")
-    deck_viz_key = "path"
-    is_timeseries = True
-    deser_map = {
-        "json": json.loads,
-        "polyline": polyline.decode,
-        "geohash": geohash_to_json,
-    }
-
-    def _extract_tooltip_columns(self) -> list[str]:
-        tooltip_columns = []
-        if tooltip_contents := self.form_data.get("tooltip_contents", []):
-            for item in tooltip_contents:
-                if isinstance(item, str):
-                    tooltip_columns.append(item)
-                elif isinstance(item, dict) and item.get("item_type") == "column":
-                    column_name = item.get("column_name")
-                    if column_name:
-                        tooltip_columns.append(column_name)
-        return tooltip_columns
-
-    def _add_tooltip_columns_to_query(
-        self, query_obj: QueryObjectDict, tooltip_columns: list[str]
-    ) -> None:
-        if not tooltip_columns:
-            return
-        if query_obj.get("metrics"):
-            existing_groupby = set(query_obj.get("groupby", []))
-            for col in tooltip_columns:
-                if col not in existing_groupby:
-                    query_obj.setdefault("groupby", []).append(col)
-        else:
-            existing_columns = set(query_obj.get("columns", []))
-            for col in tooltip_columns:
-                if col not in existing_columns:
-                    query_obj.setdefault("columns", []).append(col)
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        # pylint: disable=attribute-defined-outside-init
-        self.is_timeseries = bool(self.form_data.get("time_grain_sqla"))
-        tooltip_columns = self._extract_tooltip_columns()
-        query_obj = super().query_obj()
-        self.metric = self.form_data.get("metric")
-        line_col = self.form_data.get("line_column")
-        if query_obj["metrics"]:
-            self.has_metrics = True
-            query_obj["groupby"].append(line_col)
-        else:
-            self.has_metrics = False
-            query_obj["columns"].append(line_col)
-        self._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        line_type = self.form_data["line_type"]
-        deser = self.deser_map[line_type]
-        line_column = self.form_data["line_column"]
-        path = deser(data[line_column])
-        if self.form_data.get("reverse_long_lat"):
-            path = [(o[1], o[0]) for o in path]
-        data[self.deck_viz_key] = path
-        data["__timestamp"] = data.get(DTTM_ALIAS) or data.get("__time")
-        return data
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        self.metric_label = (  # pylint: disable=attribute-defined-outside-init
-            utils.get_metric_name(self.metric) if self.metric else None
-        )
-        return super().get_data(df)
-
-
-class DeckPolygon(DeckPathViz):
-    """deck.gl's Polygon Layer"""
-
-    viz_type = "deck_polygon"
-    deck_viz_key = "polygon"
-    verbose_name = _("Deck.gl - Polygon")
-
-    def _extract_tooltip_columns(self) -> list[str]:
-        tooltip_columns = []
-        if tooltip_contents := self.form_data.get("tooltip_contents", []):
-            for item in tooltip_contents:
-                if isinstance(item, str):
-                    tooltip_columns.append(item)
-                elif isinstance(item, dict) and item.get("item_type") == "column":
-                    column_name = item.get("column_name")
-                    if column_name:
-                        tooltip_columns.append(column_name)
-        return tooltip_columns
-
-    def _add_tooltip_columns_to_query(
-        self, query_obj: QueryObjectDict, tooltip_columns: list[str]
-    ) -> None:
-        if not tooltip_columns:
-            return
-        if query_obj.get("metrics"):
-            existing_groupby = set(query_obj.get("groupby", []))
-            for col in tooltip_columns:
-                if col not in existing_groupby:
-                    query_obj.setdefault("groupby", []).append(col)
-        else:
-            existing_columns = set(query_obj.get("columns", []))
-            for col in tooltip_columns:
-                if col not in existing_columns:
-                    query_obj.setdefault("columns", []).append(col)
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        self.elevation = self.form_data.get("point_radius_fixed") or {
-            "type": "fix",
-            "value": 500,
-        }
-        tooltip_columns = self._extract_tooltip_columns()
-        query_obj = super().query_obj()
-        self._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_metrics(self) -> list[str]:
-        metrics = [self.form_data.get("metric")]
-        if self.elevation.get("type") == "metric":
-            metrics.append(self.elevation.get("value"))
-        return [metric for metric in metrics if metric]
-
-    @deprecated(deprecated_in="3.0")
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        super().get_properties(data)
-        elevation = self.form_data["point_radius_fixed"]["value"]
-        type_ = self.form_data["point_radius_fixed"]["type"]
-        data["elevation"] = (
-            data.get(utils.get_metric_name(elevation))
-            if type_ == "metric"
-            else elevation
-        )
-        return data
-
-
-class DeckHex(BaseDeckGLViz):
-    """deck.gl's DeckLayer"""
-
-    viz_type = "deck_hex"
-    verbose_name = _("Deck.gl - 3D HEX")
-    spatial_control_keys = ["spatial"]
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        tooltip_columns = self._extract_tooltip_columns()
-        query_obj = super().query_obj()
-        self._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        properties = {
-            "position": data.get("spatial"),
-            "weight": (data.get(self.metric_label) if self.metric_label else None) or 1,
-        }
-        # Add tooltip fields to properties
-        tooltip_columns = self._extract_tooltip_columns()
-        for col in tooltip_columns:
-            if col in data:
-                properties[col] = data[col]
-        return properties
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        self.metric_label = (  # pylint: disable=attribute-defined-outside-init
-            utils.get_metric_name(self.metric) if self.metric else None
-        )
-        return super().get_data(df)
-
-
-class DeckHeatmap(BaseDeckGLViz):
-    """deck.gl's HeatmapLayer"""
-
-    viz_type = "deck_heatmap"
-    verbose_name = _("Deck.gl - Heatmap")
-    spatial_control_keys = ["spatial"]
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        tooltip_columns = self._extract_tooltip_columns()
-        query_obj = super().query_obj()
-        self._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        properties = {
-            "position": data.get("spatial"),
-            "weight": (data.get(self.metric_label) if self.metric_label else None) or 1,
-        }
-        tooltip_columns = self._extract_tooltip_columns()
-        for col in tooltip_columns:
-            if col in data:
-                properties[col] = data[col]
-
-        return properties
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        self.metric_label = (  # pylint: disable=attribute-defined-outside-init
-            utils.get_metric_name(self.metric) if self.metric else None
-        )
-        return super().get_data(df)
-
-
-class DeckContour(BaseDeckGLViz):
-    """deck.gl's ContourLayer"""
-
-    viz_type = "deck_contour"
-    verbose_name = _("Deck.gl - Contour")
-    spatial_control_keys = ["spatial"]
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-
-        # Extract and add tooltip columns
-        tooltip_columns = self._extract_tooltip_columns()
-        self._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-
-        return query_obj
-
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        properties = {
-            "position": data.get("spatial"),
-            "weight": (data.get(self.metric_label) if self.metric_label else None) or 1,
-            # Include all original data for tooltip access
-            "_originalData": data,
-        }
-
-        # Add tooltip data at top level too for backward compatibility
-        tooltip_columns = self._extract_tooltip_columns()
-        for col in tooltip_columns:
-            if col in data:
-                properties[col] = data[col]
-
-        return properties
-
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        self.metric_label = (  # pylint: disable=attribute-defined-outside-init
-            utils.get_metric_name(self.metric) if self.metric else None
-        )
-        return super().get_data(df)
-
-
-class DeckGeoJson(BaseDeckGLViz):
-    """deck.gl's GeoJSONLayer"""
-
-    viz_type = "deck_geojson"
-    verbose_name = _("Deck.gl - GeoJSON")
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-        query_obj["columns"] += [self.form_data.get("geojson")]
-        query_obj["metrics"] = []
-        query_obj["groupby"] = []
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        geojson = data[get_column_name(self.form_data["geojson"])]
-        return json.loads(geojson)
-
-
-class DeckArc(BaseDeckGLViz):
-    """deck.gl's Arc Layer"""
-
-    viz_type = "deck_arc"
-    verbose_name = _("Deck.gl - Arc")
-    spatial_control_keys = ["start_spatial", "end_spatial"]
-    is_timeseries = True
-
-    def _extract_tooltip_columns(self) -> list[str]:
-        tooltip_columns = []
-        if tooltip_contents := self.form_data.get("tooltip_contents", []):
-            for item in tooltip_contents:
-                if isinstance(item, str):
-                    tooltip_columns.append(item)
-                elif isinstance(item, dict) and item.get("item_type") == "column":
-                    column_name = item.get("column_name")
-                    if column_name:
-                        tooltip_columns.append(column_name)
-        return tooltip_columns
-
-    def _add_tooltip_columns_to_query(
-        self, query_obj: QueryObjectDict, tooltip_columns: list[str]
-    ) -> None:
-        if not tooltip_columns:
-            return
-        if query_obj.get("metrics"):
-            existing_groupby = set(query_obj.get("groupby", []))
-            for col in tooltip_columns:
-                if col not in existing_groupby:
-                    query_obj.setdefault("groupby", []).append(col)
-        else:
-            existing_columns = set(query_obj.get("columns", []))
-            for col in tooltip_columns:
-                if col not in existing_columns:
-                    query_obj.setdefault("columns", []).append(col)
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        self.is_timeseries = bool(self.form_data.get("time_grain_sqla"))
-        tooltip_columns = self._extract_tooltip_columns()
-        query_obj = super().query_obj()
-        self._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_properties(self, data: dict[str, Any]) -> dict[str, Any]:
-        dim = self.form_data.get("dimension")
-        properties = {
-            "sourcePosition": data.get("start_spatial"),
-            "targetPosition": data.get("end_spatial"),
-            "cat_color": data.get(dim) if dim else None,
-            DTTM_ALIAS: data.get(DTTM_ALIAS),
-        }
-        # Add tooltip fields to properties
-        tooltip_columns = self._extract_tooltip_columns()
-        for col in tooltip_columns:
-            if col in data:
-                properties[col] = data[col]
-        return properties
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        if df.empty:
-            return None
-        return {
-            "features": super().get_data(df)["features"],
-            "mapboxApiKey": current_app.config["MAPBOX_API_KEY"],
-        }
-
-
-class PairedTTestViz(BaseViz):
-    """A table displaying paired t-test values"""
-
-    viz_type = "paired_ttest"
-    verbose_name = _("Time Series - Paired t-test")
-    sort_series = False
-    is_timeseries = True
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-        if sort_by := self.form_data.get("timeseries_limit_metric"):
-            sort_by_label = utils.get_metric_name(sort_by)
-            if sort_by_label not in utils.get_metric_names(query_obj["metrics"]):
-                query_obj["metrics"].append(sort_by)
-            if self.form_data.get("order_desc"):
-                query_obj["orderby"] = [
-                    (sort_by, not self.form_data.get("order_desc", True))
-                ]
-        return query_obj
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        """
-        Transform received data frame into an object of the form:
-        {
-            'metric1': [
-                {
-                    groups: ('groupA', ... ),
-                    values: [ {x, y}, ... ],
-                }, ...
-            ], ...
-        }
-        """
-
-        if df.empty:
-            return None
-
-        groups = get_column_names(self.form_data.get("groupby"))
-        metrics = self.metric_labels
-        df = df.pivot_table(index=DTTM_ALIAS, columns=groups, values=metrics)
-        cols = []
-        # Be rid of falsy keys
-        for col in df.columns:
-            if col == "":
-                cols.append("N/A")
-            elif col is None:
-                cols.append("NULL")
-            else:
-                cols.append(col)
-        df.columns = cols
-        data: dict[str, list[dict[str, Any]]] = {}
-        series = df.to_dict("series")
-        for name_set in df.columns:
-            # If no groups are defined, nameSet will be the metric name
-            has_group = not isinstance(name_set, str)
-            data_ = {
-                "group": name_set[1:] if has_group else "All",
-                "values": [
-                    {
-                        "x": t,
-                        "y": series[name_set][t] if t in series[name_set] else None,
-                    }
-                    for t in df.index
-                ],
-            }
-            key = name_set[0] if has_group else name_set
-            if key in data:
-                data[key].append(data_)
-            else:
-                data[key] = [data_]
-        return data
-
-
-class RoseViz(NVD3TimeSeriesViz):
-    viz_type = "rose"
-    verbose_name = _("Time Series - Nightingale Rose Chart")
-    sort_series = False
-    is_timeseries = True
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        if df.empty:
-            return None
-
-        data = super().get_data(df)
-        result: dict[str, list[dict[str, str]]] = {}
-        for datum in data:
-            key = datum["key"]
-            for val in datum["values"]:
-                timestamp = val["x"].value
-                if not result.get(timestamp):
-                    result[timestamp] = []
-                value = 0 if math.isnan(val["y"]) else val["y"]
-                result[timestamp].append(
-                    {
-                        "key": key,
-                        "value": value,  # type: ignore
-                        "name": ", ".join(key) if isinstance(key, list) else key,
-                        "time": val["x"],
-                    }
-                )
-        return result
-
-
-class PartitionViz(NVD3TimeSeriesViz):
-    """
-    A hierarchical data visualization with support for time series.
-    """
-
-    viz_type = "partition"
-    verbose_name = _("Partition Diagram")
-
-    @deprecated(deprecated_in="3.0")
-    def query_obj(self) -> QueryObjectDict:
-        query_obj = super().query_obj()
-        time_op = self.form_data.get("time_series_option", "not_time")
-        # Return time series data if the user specifies so
-        query_obj["is_timeseries"] = time_op != "not_time"
-        return query_obj
-
-    @staticmethod
-    @deprecated(deprecated_in="3.0")
-    def levels_for(
-        time_op: str, groups: list[str], df: pd.DataFrame
-    ) -> dict[int, pd.Series]:
-        """
-        Compute the partition at each `level` from the dataframe.
-        """
-        levels = {}
-        for i in range(0, len(groups) + 1):
-            agg_df = df.groupby(groups[:i]) if i else df
-            levels[i] = (
-                agg_df.mean(numeric_only=True)
-                if time_op == "agg_mean"
-                else agg_df.sum(numeric_only=True)
-            )
-        return levels
-
-    @staticmethod
-    @deprecated(deprecated_in="3.0")
-    def levels_for_diff(
-        time_op: str, groups: list[str], df: pd.DataFrame
-    ) -> dict[int, pd.DataFrame]:
-        # Obtain a unique list of the time grains
-        times = list(set(df[DTTM_ALIAS]))
-        times.sort()
-        until = times[len(times) - 1]
-        since = times[0]
-        # Function describing how to calculate the difference
-        func = {
-            "point_diff": [pd.Series.sub, lambda a, b, fill_value: a - b],
-            "point_factor": [pd.Series.div, lambda a, b, fill_value: a / float(b)],
-            "point_percent": [
-                lambda a, b, fill_value=0: a.div(b, fill_value=fill_value) - 1,
-                lambda a, b, fill_value: a / float(b) - 1,
-            ],
-        }[time_op]
-        agg_df = df.groupby(DTTM_ALIAS).sum(numeric_only=True)
-        levels = {
-            0: pd.Series(
-                {
-                    m: func[1](agg_df[m][until], agg_df[m][since], 0)
-                    for m in agg_df.columns
-                }
-            )
-        }
-        for i in range(1, len(groups) + 1):
-            agg_df = df.groupby([DTTM_ALIAS] + groups[:i]).sum(numeric_only=True)
-            levels[i] = pd.DataFrame(
-                {
-                    m: func[0](agg_df[m][until], agg_df[m][since], fill_value=0)
-                    for m in agg_df.columns
-                }
-            )
-        return levels
-
-    @deprecated(deprecated_in="3.0")
-    def levels_for_time(
-        self, groups: list[str], df: pd.DataFrame
-    ) -> dict[int, VizData]:
-        procs = {}
-        for i in range(0, len(groups) + 1):
-            self.form_data["groupby"] = groups[:i]
-            df_drop = df.drop(groups[i:], axis=1)
-            procs[i] = self.process_data(df_drop, aggregate=True)
-        self.form_data["groupby"] = groups
-        return procs
-
-    @deprecated(deprecated_in="3.0")
-    def nest_values(
-        self,
-        levels: dict[int, pd.DataFrame],
-        level: int = 0,
-        metric: str | None = None,
-        dims: list[str] | None = None,
-    ) -> list[dict[str, Any]]:
-        """
-        Nest values at each level on the back-end with
-        access and setting, instead of summing from the bottom.
-        """
-        if dims is None:
-            dims = []
-        if not level:
-            return [
-                {
-                    "name": m,
-                    "val": levels[0][m],
-                    "children": self.nest_values(levels, 1, m),
-                }
-                for m in levels[0].index
-            ]
-        if level == 1:
-            metric_level = levels[1][metric]
-            return [
-                {
-                    "name": i,
-                    "val": metric_level[i],
-                    "children": self.nest_values(levels, 2, metric, [i]),
-                }
-                for i in metric_level.index
-            ]
-        if level >= len(levels):
-            return []
-
-        dim_level = levels[level][metric]
-        for d in dims:
-            if d not in dim_level:
-                return []
-            dim_level = dim_level[d]
-
-        return [
-            {
-                "name": [*dims, i],
-                "val": dim_level[i],
-                "children": self.nest_values(levels, level + 1, metric, dims + [i]),
-            }
-            for i in dim_level.index
-        ]
-
-    @deprecated(deprecated_in="3.0")
-    def nest_procs(
-        self,
-        procs: dict[int, pd.DataFrame],
-        level: int = -1,
-        dims: tuple[str, ...] | None = None,
-        time: Any = None,
-    ) -> list[dict[str, Any]]:
-        if dims is None:
-            dims = ()
-        if level == -1:
-            return [
-                {"name": m, "children": self.nest_procs(procs, 0, (m,))}
-                for m in procs[0].columns
-            ]
-        if not level:
-            return [
-                {
-                    "name": t,
-                    "val": procs[0][dims[0]][t],
-                    "children": self.nest_procs(procs, 1, dims, t),
-                }
-                for t in procs[0].index
-            ]
-        if level >= len(procs):
-            return []
-        return [
-            {
-                "name": i,
-                "val": procs[level][dims][i][time],
-                "children": self.nest_procs(procs, level + 1, dims + (i,), time),
-            }
-            for i in procs[level][dims].columns
-        ]
-
-    @deprecated(deprecated_in="3.0")
-    def get_data(self, df: pd.DataFrame) -> VizData:
-        if df.empty:
-            return None
-        groups = get_column_names(self.form_data.get("groupby"))
-        time_op = self.form_data.get("time_series_option", "not_time")
-        if not groups:
-            raise ValueError(_("Please choose at least one groupby"))
-        if time_op == "not_time":
-            levels = self.levels_for("agg_sum", groups, df)
-        elif time_op in ["agg_sum", "agg_mean"]:
-            levels = self.levels_for(time_op, groups, df)
-        elif time_op in ["point_diff", "point_factor", "point_percent"]:
-            levels = self.levels_for_diff(time_op, groups, df)
-        elif time_op == "adv_anal":
-            procs = self.levels_for_time(groups, df)
-            return self.nest_procs(procs)
-        else:
-            levels = self.levels_for("agg_sum", [DTTM_ALIAS] + groups, df)
-        return self.nest_values(levels)
-
-
-def _get_subclasses(cls: type[BaseViz]) -> set[type[BaseViz]]:
-    return set(cls.__subclasses__()).union(
-        [sc for c in cls.__subclasses__() for sc in _get_subclasses(c)]
-    )
-
-
-@deprecated(deprecated_in="3.0")
-def get_subclasses(cls: type[BaseViz]) -> set[type[BaseViz]]:
-    return _get_subclasses(cls)
-
-
-viz_types = {
-    o.viz_type: o
-    for o in _get_subclasses(BaseViz)
-    if o.viz_type not in current_app.config["VIZ_TYPE_DENYLIST"]
-}
diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py
index 3390da5b266..6464f3c1db1 100644
--- a/tests/integration_tests/core_tests.py
+++ b/tests/integration_tests/core_tests.py
@@ -43,7 +43,7 @@ from superset.common.db_query_status import QueryStatus
 from superset.connectors.sqla.models import SqlaTable
 from superset.db_engine_specs.base import BaseEngineSpec
 from superset.db_engine_specs.mssql import MssqlEngineSpec
-from superset.exceptions import OAuth2RedirectError, SupersetException
+from superset.exceptions import SupersetException
 from superset.extensions import cache_manager
 from superset.models import core as models
 from superset.models.dashboard import Dashboard
@@ -120,29 +120,6 @@ class TestCore(SupersetTestCase):
         resp = self.client.get("/slice/-1/")
         assert resp.status_code == 404
 
-    @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
-    def test_viz_cache_key(self):
-        self.login(ADMIN_USERNAME)
-        slc = self.get_slice("% Rural")
-
-        viz = slc.viz
-        qobj = viz.query_obj()
-        cache_key = viz.cache_key(qobj)
-
-        qobj["groupby"] = []
-        cache_key_with_groupby = viz.cache_key(qobj)
-        assert cache_key != cache_key_with_groupby
-
-        assert viz.cache_key(qobj) != viz.cache_key(qobj, time_compare="12 weeks")
-
-        assert viz.cache_key(qobj, time_compare="28 days") != viz.cache_key(
-            qobj, time_compare="12 weeks"
-        )
-
-        qobj["inner_from_dttm"] = datetime.datetime(1901, 1, 1)
-
-        assert cache_key_with_groupby == viz.cache_key(qobj)
-
     def test_admin_only_menu_views(self):
         def assert_admin_view_menus_in(role_name, assert_func):
             role = security_manager.find_role(role_name)
@@ -447,84 +424,6 @@ class TestCore(SupersetTestCase):
         assert "comment 2" in rendered_query
         assert "FROM tbl" in rendered_query
 
-    def test_slice_payload_no_datasource(self):
-        form_data = {
-            "viz_type": "dist_bar",
-        }
-        self.login(ADMIN_USERNAME)
-        rv = self.client.post(
-            "/explore_json/",
-            data={"form_data": json.dumps(form_data)},
-        )
-        data = json.loads(rv.data.decode("utf-8"))
-
-        assert (
-            data["errors"][0]["message"]
-            == "The dataset associated with this chart no longer exists"
-        )
-
-    def test_explore_json_data_invalid_cache_key(self):
-        self.login(ADMIN_USERNAME)
-        cache_key = "invalid-cache-key"
-        rv = self.client.get(f"/explore_json/data/{cache_key}")
-        data = json.loads(rv.data.decode("utf-8"))
-
-        assert rv.status_code == 404
-        assert data["error"] == "Cached data not found"
-
-    @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
-    @mock.patch("superset.viz.BaseViz.get_df")
-    def test_explore_json_propagates_oauth2_redirect_error(
-        self, mock_get_df: mock.Mock
-    ) -> None:
-        """
-        SupersetErrorException exceptions bubble up properly.
-        """
-        mock_get_df.side_effect = OAuth2RedirectError(
-            url="https://accounts.example.com/o/oauth2/v2/auth?...",
-            tab_id="tab-123",
-            redirect_uri="https://superset.example.com/oauth2/redirect",
-        )
-
-        self.login(ADMIN_USERNAME)
-        slc = self.get_slice("% Rural")
-        rv = self.client.post(
-            f"/explore_json/{slc.datasource_type}/{slc.datasource_id}/",
-            data={"form_data": json.dumps(slc.form_data)},
-        )
-        data = json.loads(rv.data.decode("utf-8"))
-
-        assert "errors" in data, data
-        assert data["errors"][0]["error_type"] == "OAUTH2_REDIRECT"
-        assert data["errors"][0]["extra"] == {
-            "url": "https://accounts.example.com/o/oauth2/v2/auth?...",
-            "tab_id": "tab-123",
-            "redirect_uri": "https://superset.example.com/oauth2/redirect",
-        }
-
-    @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
-    @mock.patch("superset.viz.BaseViz.get_df")
-    def test_explore_json_generic_exception_still_returns_viz_get_df_error(
-        self, mock_get_df: mock.Mock
-    ) -> None:
-        """
-        Non-Superset exceptions raised by ``get_df`` are reported as the
-        generic ``VIZ_GET_DF_ERROR``.
-        """
-        mock_get_df.side_effect = RuntimeError("boom")
-
-        self.login(ADMIN_USERNAME)
-        slc = self.get_slice("% Rural")
-        rv = self.client.post(
-            f"/explore_json/{slc.datasource_type}/{slc.datasource_id}/",
-            data={"form_data": json.dumps(slc.form_data)},
-        )
-        data = json.loads(rv.data.decode("utf-8"))
-
-        assert "errors" in data, data
-        assert data["errors"][0]["error_type"] == "VIZ_GET_DF_ERROR"
-        assert data["errors"][0]["message"] == "boom"
-
     def test_results_default_deserialization(self):
         use_new_deserialization = False
         data = [("a", 4, 4.0, "2019-08-18T16:39:16.660000")]
diff --git a/tests/integration_tests/security_tests.py b/tests/integration_tests/security_tests.py
index c089bf4cf25..1b8f8698ee5 100644
--- a/tests/integration_tests/security_tests.py
+++ b/tests/integration_tests/security_tests.py
@@ -31,7 +31,7 @@ from flask import current_app, g
 from flask_appbuilder.security.sqla.models import Role
 from superset.daos.datasource import DatasourceDAO  # noqa: F401
 from superset.models.dashboard import Dashboard
-from superset import appbuilder, db, security_manager, viz
+from superset import appbuilder, db, security_manager
 from superset.connectors.sqla.models import SqlaTable
 from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
 from superset.exceptions import SupersetSecurityException
@@ -1356,8 +1356,6 @@ class TestRolePermission(SupersetTestCase):
         assert ("can_explore", "Superset") in perm_set
         assert ("can_share_chart", "Superset") in perm_set
         assert ("can_share_dashboard", "Superset") in perm_set
-        assert ("can_explore_json", "Superset") in perm_set
-        assert ("can_explore_json", "Superset") in perm_set
         assert ("can_userinfo", "UserDBModelView") in perm_set
         assert ("can_view_chart_as_table", "Dashboard") in perm_set
         assert ("can_view_query", "Dashboard") in perm_set
@@ -2062,24 +2060,6 @@ class TestSecurityManager(SupersetTestCase):
         with self.assertRaises(SupersetSecurityException):  # noqa: PT027
             security_manager.raise_for_access(database=database, table=table)
 
-    @patch("superset.security.SupersetSecurityManager.is_owner")
-    @patch("superset.security.SupersetSecurityManager.can_access")
-    @patch("superset.security.SupersetSecurityManager.can_access_schema")
-    def test_raise_for_access_viz(
-        self, mock_can_access_schema, mock_can_access, mock_is_owner
-    ):
-        test_viz = viz.TimeTableViz(self.get_datasource_mock(), form_data={})
-
-        mock_can_access_schema.return_value = True
-        security_manager.raise_for_access(viz=test_viz)
-
-        mock_can_access.return_value = False
-        mock_can_access_schema.return_value = False
-        mock_is_owner.return_value = False
-        with override_user(security_manager.find_user("gamma")):
-            with self.assertRaises(SupersetSecurityException):  # noqa: PT027
-                security_manager.raise_for_access(viz=test_viz)
-
     @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
     @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
     @with_feature_flags(DASHBOARD_RBAC=True)
diff --git a/tests/integration_tests/tasks/async_queries_tests.py b/tests/integration_tests/tasks/async_queries_tests.py
index 728076011bd..16d618f0ece 100644
--- a/tests/integration_tests/tasks/async_queries_tests.py
+++ b/tests/integration_tests/tasks/async_queries_tests.py
@@ -29,7 +29,6 @@ from superset.async_events.cache_backend import (
 )
 from superset.commands.chart.data.get_data_command import ChartDataCommand
 from superset.commands.chart.exceptions import ChartDataQueryFailedError
-from superset.exceptions import SupersetException
 from superset.extensions import async_query_manager, security_manager
 from tests.integration_tests.base_tests import SupersetTestCase
 from tests.integration_tests.fixtures.birth_names_dashboard import (
@@ -154,120 +153,3 @@ class TestAsyncQueries(SupersetTestCase):
                 set_form_data.side_effect = SoftTimeLimitExceeded()
                 load_chart_data_into_cache(job_metadata, form_data)
             set_form_data.assert_called_once_with(form_data, "error", errors=errors)
-
-    @parameterized.expand(
-        [
-            ("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)),
-            ("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)),
-        ]
-    )
-    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    @pytest.mark.skip(reason="This test will be changed to use the api/v1/data")
-    @mock.patch.object(async_query_manager, "update_job")
-    def test_load_explore_json_into_cache(
-        self, cache_type, cache_backend, mock_update_job
-    ):
-        from superset.tasks.async_queries import load_explore_json_into_cache
-
-        app._got_first_request = False
-
-        async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend)
-        async_query_manager.init_app(app)
-
-        table = self.get_table(name="birth_names")
-        user = security_manager.find_user("gamma")
-        form_data = {
-            "datasource": f"{table.id}__table",
-            "viz_type": "dist_bar",
-            "granularity_sqla": "ds",
-            "time_range": "No filter",
-            "metrics": ["count"],
-            "adhoc_filters": [],
-            "groupby": ["gender"],
-            "row_limit": 100,
-        }
-        job_metadata = {
-            "channel_id": str(uuid4()),
-            "job_id": str(uuid4()),
-            "user_id": user.id,
-            "status": "pending",
-            "errors": [],
-        }
-
-        load_explore_json_into_cache(job_metadata, form_data)
-
-        mock_update_job.assert_called_once_with(
-            job_metadata, "done", result_url=mock.ANY
-        )
-
-    @parameterized.expand(
-        [
-            ("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)),
-            ("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)),
-        ]
-    )
-    @mock.patch.object(async_query_manager, "update_job")
-    @mock.patch("superset.tasks.async_queries.set_form_data")
-    def test_load_explore_json_into_cache_error(
-        self, cache_type, cache_backend, mock_set_form_data, mock_update_job
-    ):
-        from superset.tasks.async_queries import load_explore_json_into_cache
-
-        app._got_first_request = False
-
-        async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend)
-        async_query_manager.init_app(app)
-
-        user = security_manager.find_user("gamma")
-        form_data = {}
-        job_metadata = {
-            "channel_id": str(uuid4()),
-            "job_id": str(uuid4()),
-            "user_id": user.id,
-            "status": "pending",
-            "errors": [],
-        }
-
-        with pytest.raises(SupersetException):
-            load_explore_json_into_cache(job_metadata, form_data)
-
-        mock_set_form_data.assert_called_once_with(form_data)
-        errors = ["The dataset associated with this chart no longer exists"]
-        mock_update_job.assert_called_once_with(job_metadata, "error", errors=errors)
-
-    @parameterized.expand(
-        [
-            ("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)),
-            ("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)),
-        ]
-    )
-    @mock.patch.object(ChartDataCommand, "run")
-    @mock.patch.object(async_query_manager, "update_job")
-    def test_soft_timeout_load_explore_json_into_cache(
-        self, cache_type, cache_backend, mock_update_job, mock_run_command
-    ):
-        from superset.tasks.async_queries import load_explore_json_into_cache
-
-        app._got_first_request = False
-
-        async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend)
-        async_query_manager.init_app(app)
-
-        user = security_manager.find_user("gamma")
-        form_data = {}
-        job_metadata = {
-            "channel_id": str(uuid4()),
-            "job_id": str(uuid4()),
-            "user_id": user.id,
-            "status": "pending",
-            "errors": [],
-        }
-        errors = ["A timeout occurred while loading explore JSON data"]
-
-        with pytest.raises(SoftTimeLimitExceeded):  # noqa: PT012
-            with mock.patch(
-                "superset.tasks.async_queries.set_form_data"
-            ) as set_form_data:
-                set_form_data.side_effect = SoftTimeLimitExceeded()
-                load_explore_json_into_cache(job_metadata, form_data)
-            set_form_data.assert_called_once_with(form_data, "error", errors=errors)
diff --git a/tests/integration_tests/utils_tests.py b/tests/integration_tests/utils_tests.py
index fc083cd7e3c..a27062f7585 100644
--- a/tests/integration_tests/utils_tests.py
+++ b/tests/integration_tests/utils_tests.py
@@ -37,7 +37,7 @@ import tests.integration_tests.test_app  # noqa: F401
 from superset import db, security_manager
 from superset.constants import NO_TIME_RANGE
 from superset.exceptions import CertificateException, SupersetException  # noqa: F401
-from superset.models.core import Database, Log
+from superset.models.core import Database
 from superset.models.dashboard import Dashboard  # noqa: F401
 from superset.models.slice import Slice  # noqa: F401
 from superset.utils.core import (
@@ -63,7 +63,6 @@ from superset.utils import schema
 from superset.utils.hashing import hash_from_str
 from superset.views.utils import build_extra_filters, get_form_data  # noqa: F401
 from tests.integration_tests.base_tests import SupersetTestCase
-from tests.integration_tests.constants import ADMIN_USERNAME
 from tests.integration_tests.fixtures.world_bank_dashboard import (
     load_world_bank_dashboard_with_slices,  # noqa: F401
     load_world_bank_data,  # noqa: F401
@@ -354,36 +353,6 @@ class TestUtils(SupersetTestCase):
             assert form_data == {}
             assert slc is None
 
-    @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
-    def test_log_this(self) -> None:
-        # TODO: Add additional scenarios.
-        self.login(ADMIN_USERNAME)
-        slc = self.get_slice("% Rural")
-        dashboard_id = 1
-
-        assert slc.viz is not None
-        resp = self.get_json_resp(  # noqa: F841
-            f"/explore_json/{slc.datasource_type}/{slc.datasource_id}/"
-            + f'?form_data={{"slice_id": {slc.id}}}&dashboard_id={dashboard_id}',
-            {"form_data": json.dumps(slc.viz.form_data)},
-        )
-
-        record = (
-            db.session.query(Log)
-            .filter_by(action="explore_json", slice_id=slc.id)
-            .order_by(Log.dttm.desc())
-            .first()
-        )
-
-        assert record.dashboard_id == dashboard_id
-        assert json.loads(record.json)["dashboard_id"] == str(dashboard_id)
-        assert json.loads(record.json)["form_data"]["slice_id"] == slc.id
-
-        assert (
-            json.loads(record.json)["form_data"]["viz_type"]
-            == slc.viz.form_data["viz_type"]
-        )
-
     def test_schema_validate_json(self):
         valid = '{"a": 5, "b": [1, 5, ["g", "h"]]}'
         assert schema.validate_json(valid) is None
diff --git a/tests/integration_tests/viz_tests.py b/tests/integration_tests/viz_tests.py
deleted file mode 100644
index ad6d9b16a5a..00000000000
--- a/tests/integration_tests/viz_tests.py
+++ /dev/null
@@ -1,2453 +0,0 @@
-# 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.
-# isort:skip_file
-from datetime import datetime
-import logging
-from unittest.mock import Mock, patch
-
-import numpy as np
-import pandas as pd
-import pytest
-
-import tests.integration_tests.test_app  # noqa: F401
-import superset.viz as viz
-from flask import current_app
-from superset.exceptions import QueryObjectValidationError, SpatialException
-from superset.utils.core import DTTM_ALIAS, ExtraFiltersReasonType
-from superset.utils.pandas_postprocessing.utils import FLAT_COLUMN_SEPARATOR
-from tests.conftest import with_config
-
-from .base_tests import SupersetTestCase
-from .utils import load_fixture
-
-logger = logging.getLogger(__name__)
-
-
-class TestBaseViz(SupersetTestCase):
-    def test_constructor_exception_no_datasource(self):
-        form_data = {}
-        datasource = None
-        with self.assertRaises(Exception):  # noqa: B017, PT027
-            viz.BaseViz(datasource, form_data)
-
-    def test_process_metrics(self):
-        # test TimeTableViz metrics in correct order
-        form_data = {
-            "url_params": {},
-            "row_limit": 500,
-            "metric": "sum__SP_POP_TOTL",
-            "entity": "country_code",
-            "secondary_metric": "sum__SP_POP_TOTL",
-            "granularity_sqla": "year",
-            "page_length": 0,
-            "all_columns": [],
-            "viz_type": "time_table",
-            "since": "2014-01-01",
-            "until": "2014-01-02",
-            "metrics": ["sum__SP_POP_TOTL", "SUM(SE_PRM_NENR_MA)", "SUM(SP_URB_TOTL)"],
-            "country_fieldtype": "cca3",
-            "percent_metrics": ["count"],
-            "slice_id": 74,
-            "time_grain_sqla": None,
-            "order_by_cols": [],
-            "groupby": ["country_name"],
-            "compare_lag": "10",
-            "limit": "25",
-            "datasource": "2__table",
-            "table_timestamp_format": "%Y-%m-%d %H:%M:%S",
-            "markup_type": "markdown",
-            "where": "",
-            "compare_suffix": "o10Y",
-        }
-        datasource = Mock()
-        datasource.type = "table"
-        test_viz = viz.BaseViz(datasource, form_data)
-        expect_metric_labels = [
-            "sum__SP_POP_TOTL",
-            "SUM(SE_PRM_NENR_MA)",
-            "SUM(SP_URB_TOTL)",
-            "count",
-        ]
-        assert test_viz.metric_labels == expect_metric_labels
-        assert test_viz.all_metrics == expect_metric_labels
-
-    def test_get_df_returns_empty_df(self):
-        form_data = {"dummy": 123}
-        query_obj = {"granularity": "day"}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseViz(datasource, form_data)
-        result = test_viz.get_df(query_obj)
-        assert isinstance(result, pd.DataFrame)
-        assert result.empty
-
-    def test_get_df_handles_dttm_col(self):
-        form_data = {"dummy": 123}
-        query_obj = {"granularity": "day"}
-        results = Mock()
-        results.query = Mock()
-        results.status = Mock()
-        results.error_message = Mock()
-        datasource = Mock()
-        datasource.type = "table"
-        datasource.query = Mock(return_value=results)
-        mock_dttm_col = Mock()
-        datasource.get_column = Mock(return_value=mock_dttm_col)
-
-        test_viz = viz.BaseViz(datasource, form_data)
-        test_viz.df_metrics_to_num = Mock()
-        test_viz.get_fillna_for_columns = Mock(return_value=0)
-
-        results.df = pd.DataFrame(data={DTTM_ALIAS: ["1960-01-01 05:00:00"]})
-        datasource.offset = 0
-        mock_dttm_col = Mock()
-        datasource.get_column = Mock(return_value=mock_dttm_col)
-        mock_dttm_col.python_date_format = "epoch_ms"
-        result = test_viz.get_df(query_obj)
-        import logging  # noqa: F401
-
-        logger.info(result)
-        pd.testing.assert_series_equal(
-            result[DTTM_ALIAS], pd.Series([datetime(1960, 1, 1, 5, 0)], name=DTTM_ALIAS)
-        )
-
-        mock_dttm_col.python_date_format = None
-        result = test_viz.get_df(query_obj)
-        pd.testing.assert_series_equal(
-            result[DTTM_ALIAS], pd.Series([datetime(1960, 1, 1, 5, 0)], name=DTTM_ALIAS)
-        )
-
-        datasource.offset = 1
-        result = test_viz.get_df(query_obj)
-        pd.testing.assert_series_equal(
-            result[DTTM_ALIAS], pd.Series([datetime(1960, 1, 1, 6, 0)], name=DTTM_ALIAS)
-        )
-
-        datasource.offset = 0
-        results.df = pd.DataFrame(data={DTTM_ALIAS: ["1960-01-01"]})
-        mock_dttm_col.python_date_format = "%Y-%m-%d"
-        result = test_viz.get_df(query_obj)
-        pd.testing.assert_series_equal(
-            result[DTTM_ALIAS], pd.Series([datetime(1960, 1, 1, 0, 0)], name=DTTM_ALIAS)
-        )
-
-    def test_cache_timeout(self):
-        datasource = self.get_datasource_mock()
-        datasource.cache_timeout = 0
-        test_viz = viz.BaseViz(datasource, form_data={})
-        assert 0 == test_viz.cache_timeout
-
-        datasource.cache_timeout = 156
-        test_viz = viz.BaseViz(datasource, form_data={})
-        assert 156 == test_viz.cache_timeout
-
-        datasource.cache_timeout = None
-        datasource.database.cache_timeout = 0
-        assert 0 == test_viz.cache_timeout
-
-        datasource.database.cache_timeout = 1666
-        assert 1666 == test_viz.cache_timeout
-
-        datasource.database.cache_timeout = None
-        test_viz = viz.BaseViz(datasource, form_data={})
-        assert (
-            current_app.config["DATA_CACHE_CONFIG"]["CACHE_DEFAULT_TIMEOUT"]
-            == test_viz.cache_timeout
-        )
-
-        data_cache_timeout = current_app.config["DATA_CACHE_CONFIG"][
-            "CACHE_DEFAULT_TIMEOUT"
-        ]
-        current_app.config["DATA_CACHE_CONFIG"]["CACHE_DEFAULT_TIMEOUT"] = None
-        datasource.database.cache_timeout = None
-        test_viz = viz.BaseViz(datasource, form_data={})
-        assert current_app.config["CACHE_DEFAULT_TIMEOUT"] == test_viz.cache_timeout
-        # restore DATA_CACHE_CONFIG timeout
-        current_app.config["DATA_CACHE_CONFIG"]["CACHE_DEFAULT_TIMEOUT"] = (
-            data_cache_timeout
-        )
-
-
-class TestPairedTTest(SupersetTestCase):
-    def test_get_data_transforms_dataframe(self):
-        form_data = {
-            "groupby": ["groupA", "groupB", "groupC"],
-            "metrics": ["metric1", "metric2", "metric3"],
-        }
-        datasource = self.get_datasource_mock()
-        # Test data
-        raw = {}
-        raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300]
-        raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"]
-        raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"]
-        raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"]
-        raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-        raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90]
-        raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900]
-        df = pd.DataFrame(raw)
-        pairedTTestViz = viz.viz_types["paired_ttest"](datasource, form_data)  # noqa: N806
-        data = pairedTTestViz.get_data(df)
-        # Check method correctly transforms data
-        expected = {
-            "metric1": [
-                {
-                    "values": [
-                        {"x": 100, "y": 1},
-                        {"x": 200, "y": 2},
-                        {"x": 300, "y": 3},
-                    ],
-                    "group": ("a1", "a2", "a3"),
-                },
-                {
-                    "values": [
-                        {"x": 100, "y": 4},
-                        {"x": 200, "y": 5},
-                        {"x": 300, "y": 6},
-                    ],
-                    "group": ("b1", "b2", "b3"),
-                },
-                {
-                    "values": [
-                        {"x": 100, "y": 7},
-                        {"x": 200, "y": 8},
-                        {"x": 300, "y": 9},
-                    ],
-                    "group": ("c1", "c2", "c3"),
-                },
-            ],
-            "metric2": [
-                {
-                    "values": [
-                        {"x": 100, "y": 10},
-                        {"x": 200, "y": 20},
-                        {"x": 300, "y": 30},
-                    ],
-                    "group": ("a1", "a2", "a3"),
-                },
-                {
-                    "values": [
-                        {"x": 100, "y": 40},
-                        {"x": 200, "y": 50},
-                        {"x": 300, "y": 60},
-                    ],
-                    "group": ("b1", "b2", "b3"),
-                },
-                {
-                    "values": [
-                        {"x": 100, "y": 70},
-                        {"x": 200, "y": 80},
-                        {"x": 300, "y": 90},
-                    ],
-                    "group": ("c1", "c2", "c3"),
-                },
-            ],
-            "metric3": [
-                {
-                    "values": [
-                        {"x": 100, "y": 100},
-                        {"x": 200, "y": 200},
-                        {"x": 300, "y": 300},
-                    ],
-                    "group": ("a1", "a2", "a3"),
-                },
-                {
-                    "values": [
-                        {"x": 100, "y": 400},
-                        {"x": 200, "y": 500},
-                        {"x": 300, "y": 600},
-                    ],
-                    "group": ("b1", "b2", "b3"),
-                },
-                {
-                    "values": [
-                        {"x": 100, "y": 700},
-                        {"x": 200, "y": 800},
-                        {"x": 300, "y": 900},
-                    ],
-                    "group": ("c1", "c2", "c3"),
-                },
-            ],
-        }
-        assert data == expected
-
-    def test_get_data_empty_null_keys(self):
-        form_data = {"groupby": [], "metrics": [""]}
-        datasource = self.get_datasource_mock()
-        # Test data
-        raw = {}
-        raw[DTTM_ALIAS] = [100, 200, 300]
-        raw[""] = [1, 2, 3]
-        raw[None] = [10, 20, 30]
-
-        df = pd.DataFrame(raw)
-        pairedTTestViz = viz.viz_types["paired_ttest"](datasource, form_data)  # noqa: N806
-        data = pairedTTestViz.get_data(df)
-        # Check method correctly transforms data
-        expected = {
-            "N/A": [
-                {
-                    "values": [
-                        {"x": 100, "y": 1},
-                        {"x": 200, "y": 2},
-                        {"x": 300, "y": 3},
-                    ],
-                    "group": "All",
-                }
-            ],
-        }
-        assert data == expected
-
-        form_data = {"groupby": [], "metrics": [None]}
-        with self.assertRaises(ValueError):  # noqa: PT027
-            viz.viz_types["paired_ttest"](datasource, form_data)
-
-
-class TestPartitionViz(SupersetTestCase):
-    @patch("superset.viz.BaseViz.query_obj")
-    def test_query_obj_time_series_option(self, super_query_obj):
-        datasource = self.get_datasource_mock()
-        form_data = {}
-        test_viz = viz.PartitionViz(datasource, form_data)
-        super_query_obj.return_value = {}
-        query_obj = test_viz.query_obj()
-        assert not query_obj["is_timeseries"]
-        test_viz.form_data["time_series_option"] = "agg_sum"
-        query_obj = test_viz.query_obj()
-        assert query_obj["is_timeseries"]
-
-    def test_levels_for_computes_levels(self):
-        raw = {}
-        raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300]
-        raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"]
-        raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"]
-        raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"]
-        raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-        raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90]
-        raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900]
-        df = pd.DataFrame(raw)
-        groups = ["groupA", "groupB", "groupC"]
-        time_op = "agg_sum"
-        test_viz = viz.PartitionViz(Mock(), {})
-        levels = test_viz.levels_for(time_op, groups, df)
-        assert 4 == len(levels)
-        expected = {DTTM_ALIAS: 1800, "metric1": 45, "metric2": 450, "metric3": 4500}
-        assert expected == levels[0].to_dict()
-        expected = {
-            DTTM_ALIAS: {"a1": 600, "b1": 600, "c1": 600},
-            "metric1": {"a1": 6, "b1": 15, "c1": 24},
-            "metric2": {"a1": 60, "b1": 150, "c1": 240},
-            "metric3": {"a1": 600, "b1": 1500, "c1": 2400},
-        }
-        assert expected == levels[1].to_dict()
-        assert ["groupA", "groupB"] == levels[2].index.names
-        assert ["groupA", "groupB", "groupC"] == levels[3].index.names
-        time_op = "agg_mean"
-        levels = test_viz.levels_for(time_op, groups, df)
-        assert 4 == len(levels)
-        expected = {
-            DTTM_ALIAS: 200.0,
-            "metric1": 5.0,
-            "metric2": 50.0,
-            "metric3": 500.0,
-        }
-        assert expected == levels[0].to_dict()
-        expected = {
-            DTTM_ALIAS: {"a1": 200, "c1": 200, "b1": 200},
-            "metric1": {"a1": 2, "b1": 5, "c1": 8},
-            "metric2": {"a1": 20, "b1": 50, "c1": 80},
-            "metric3": {"a1": 200, "b1": 500, "c1": 800},
-        }
-        assert expected == levels[1].to_dict()
-        assert ["groupA", "groupB"] == levels[2].index.names
-        assert ["groupA", "groupB", "groupC"] == levels[3].index.names
-
-    def test_levels_for_diff_computes_difference(self):
-        raw = {}
-        raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300]
-        raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"]
-        raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"]
-        raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"]
-        raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-        raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90]
-        raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900]
-        df = pd.DataFrame(raw)
-        groups = ["groupA", "groupB", "groupC"]
-        test_viz = viz.PartitionViz(Mock(), {})
-        time_op = "point_diff"
-        levels = test_viz.levels_for_diff(time_op, groups, df)
-        expected = {"metric1": 6, "metric2": 60, "metric3": 600}
-        assert expected == levels[0].to_dict()
-        expected = {
-            "metric1": {"a1": 2, "b1": 2, "c1": 2},
-            "metric2": {"a1": 20, "b1": 20, "c1": 20},
-            "metric3": {"a1": 200, "b1": 200, "c1": 200},
-        }
-        assert expected == levels[1].to_dict()
-        assert 4 == len(levels)
-        assert ["groupA", "groupB", "groupC"] == levels[3].index.names
-
-    def test_levels_for_time_calls_process_data_and_drops_cols(self):
-        raw = {}
-        raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300]
-        raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"]
-        raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"]
-        raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"]
-        raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-        raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90]
-        raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900]
-        df = pd.DataFrame(raw)
-        groups = ["groupA", "groupB", "groupC"]
-        test_viz = viz.PartitionViz(Mock(), {"groupby": groups})
-
-        def return_args(df_drop, aggregate):
-            return df_drop
-
-        test_viz.process_data = Mock(side_effect=return_args)
-        levels = test_viz.levels_for_time(groups, df)
-        assert 4 == len(levels)
-        cols = [DTTM_ALIAS, "metric1", "metric2", "metric3"]
-        assert sorted(cols) == sorted(levels[0].columns.tolist())
-        cols += ["groupA"]
-        assert sorted(cols) == sorted(levels[1].columns.tolist())
-        cols += ["groupB"]
-        assert sorted(cols) == sorted(levels[2].columns.tolist())
-        cols += ["groupC"]
-        assert sorted(cols) == sorted(levels[3].columns.tolist())
-        assert 4 == len(test_viz.process_data.mock_calls)
-
-    def test_nest_values_returns_hierarchy(self):
-        raw = {}
-        raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"]
-        raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"]
-        raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"]
-        raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-        raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90]
-        raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900]
-        df = pd.DataFrame(raw)
-        test_viz = viz.PartitionViz(Mock(), {})
-        groups = ["groupA", "groupB", "groupC"]
-        levels = test_viz.levels_for("agg_sum", groups, df)
-        nest = test_viz.nest_values(levels)
-        assert 3 == len(nest)
-        for i in range(0, 3):
-            assert "metric" + str(i + 1) == nest[i]["name"]
-        assert 3 == len(nest[0]["children"])
-        assert 1 == len(nest[0]["children"][0]["children"])
-        assert 1 == len(nest[0]["children"][0]["children"][0]["children"])
-
-    def test_nest_values_returns_hierarchy_when_more_dimensions(self):
-        raw = {}
-        raw["category"] = ["a", "a", "a"]
-        raw["subcategory"] = ["a.2", "a.1", "a.2"]
-        raw["sub_subcategory"] = ["a.2.1", "a.1.1", "a.2.2"]
-        raw["metric1"] = [5, 10, 15]
-        raw["metric2"] = [50, 100, 150]
-        raw["metric3"] = [500, 1000, 1500]
-        df = pd.DataFrame(raw)
-        test_viz = viz.PartitionViz(Mock(), {})
-        groups = ["category", "subcategory", "sub_subcategory"]
-        levels = test_viz.levels_for("agg_sum", groups, df)
-        nest = test_viz.nest_values(levels)
-        assert 3 == len(nest)
-        for i in range(0, 3):
-            assert "metric" + str(i + 1) == nest[i]["name"]
-        assert 1 == len(nest[0]["children"])
-        assert 2 == len(nest[0]["children"][0]["children"])
-        assert 1 == len(nest[0]["children"][0]["children"][0]["children"])
-        assert 2 == len(nest[0]["children"][0]["children"][1]["children"])
-
-    def test_nest_procs_returns_hierarchy(self):
-        raw = {}
-        raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300]
-        raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"]
-        raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"]
-        raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"]
-        raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-        raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90]
-        raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900]
-        df = pd.DataFrame(raw)
-        test_viz = viz.PartitionViz(Mock(), {})
-        groups = ["groupA", "groupB", "groupC"]
-        metrics = ["metric1", "metric2", "metric3"]
-        procs = {}
-        for i in range(0, 4):
-            df_drop = df.drop(groups[i:], axis=1)
-            pivot = df_drop.pivot_table(
-                index=DTTM_ALIAS, columns=groups[:i], values=metrics
-            )
-            procs[i] = pivot
-        nest = test_viz.nest_procs(procs)
-        assert 3 == len(nest)
-        for i in range(0, 3):
-            assert "metric" + str(i + 1) == nest[i]["name"]
-            assert None is nest[i].get("val")
-        assert 3 == len(nest[0]["children"])
-        assert 3 == len(nest[0]["children"][0]["children"])
-        assert 1 == len(nest[0]["children"][0]["children"][0]["children"])
-        assert 1 == len(
-            nest[0]["children"][0]["children"][0]["children"][0]["children"]
-        )
-
-    def test_get_data_calls_correct_method(self):
-        raw = {}
-        raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300]
-        raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"]
-        raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"]
-        raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"]
-        raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-        raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90]
-        raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900]
-        df = pd.DataFrame(raw)
-        test_viz = viz.PartitionViz(Mock(), {})
-        with self.assertRaises(ValueError):  # noqa: PT027
-            test_viz.get_data(df)
-        test_viz.levels_for = Mock(return_value=1)
-        test_viz.nest_values = Mock(return_value=1)
-        test_viz.form_data["groupby"] = ["groups"]
-        test_viz.form_data["time_series_option"] = "not_time"
-        test_viz.get_data(df)
-        assert "agg_sum" == test_viz.levels_for.mock_calls[0][1][0]
-        test_viz.form_data["time_series_option"] = "agg_sum"
-        test_viz.get_data(df)
-        assert "agg_sum" == test_viz.levels_for.mock_calls[1][1][0]
-        test_viz.form_data["time_series_option"] = "agg_mean"
-        test_viz.get_data(df)
-        assert "agg_mean" == test_viz.levels_for.mock_calls[2][1][0]
-        test_viz.form_data["time_series_option"] = "point_diff"
-        test_viz.levels_for_diff = Mock(return_value=1)
-        test_viz.get_data(df)
-        assert "point_diff" == test_viz.levels_for_diff.mock_calls[0][1][0]
-        test_viz.form_data["time_series_option"] = "point_percent"
-        test_viz.get_data(df)
-        assert "point_percent" == test_viz.levels_for_diff.mock_calls[1][1][0]
-        test_viz.form_data["time_series_option"] = "point_factor"
-        test_viz.get_data(df)
-        assert "point_factor" == test_viz.levels_for_diff.mock_calls[2][1][0]
-        test_viz.levels_for_time = Mock(return_value=1)
-        test_viz.nest_procs = Mock(return_value=1)
-        test_viz.form_data["time_series_option"] = "adv_anal"
-        test_viz.get_data(df)
-        assert 1 == len(test_viz.levels_for_time.mock_calls)
-        assert 1 == len(test_viz.nest_procs.mock_calls)
-        test_viz.form_data["time_series_option"] = "time_series"
-        test_viz.get_data(df)
-        assert "agg_sum" == test_viz.levels_for.mock_calls[3][1][0]
-        assert 7 == len(test_viz.nest_values.mock_calls)
-
-
-class TestRoseVis(SupersetTestCase):
-    def test_rose_vis_get_data(self):
-        raw = {}
-        t1 = pd.Timestamp("2000")
-        t2 = pd.Timestamp("2002")
-        t3 = pd.Timestamp("2004")
-        raw[DTTM_ALIAS] = [t1, t2, t3, t1, t2, t3, t1, t2, t3]
-        raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"]
-        raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"]
-        raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"]
-        raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-        df = pd.DataFrame(raw)
-        fd = {"metrics": ["metric1"], "groupby": ["groupA"]}
-        test_viz = viz.RoseViz(Mock(), fd)
-        test_viz.metrics = fd["metrics"]
-        res = test_viz.get_data(df)
-        expected = {
-            946684800000000000: [
-                {"time": t1, "value": 1, "key": ("a1",), "name": ("a1",)},
-                {"time": t1, "value": 4, "key": ("b1",), "name": ("b1",)},
-                {"time": t1, "value": 7, "key": ("c1",), "name": ("c1",)},
-            ],
-            1009843200000000000: [
-                {"time": t2, "value": 2, "key": ("a1",), "name": ("a1",)},
-                {"time": t2, "value": 5, "key": ("b1",), "name": ("b1",)},
-                {"time": t2, "value": 8, "key": ("c1",), "name": ("c1",)},
-            ],
-            1072915200000000000: [
-                {"time": t3, "value": 3, "key": ("a1",), "name": ("a1",)},
-                {"time": t3, "value": 6, "key": ("b1",), "name": ("b1",)},
-                {"time": t3, "value": 9, "key": ("c1",), "name": ("c1",)},
-            ],
-        }
-        assert expected == res
-
-
-class TestTimeSeriesTableViz(SupersetTestCase):
-    def test_get_data_metrics(self):
-        form_data = {"metrics": ["sum__A", "count"], "groupby": []}
-        datasource = self.get_datasource_mock()
-        raw = {}
-        t1 = pd.Timestamp("2000")
-        t2 = pd.Timestamp("2002")
-        raw[DTTM_ALIAS] = [t1, t2]
-        raw["sum__A"] = [15, 20]
-        raw["count"] = [6, 7]
-        df = pd.DataFrame(raw)
-        test_viz = viz.TimeTableViz(datasource, form_data)
-        data = test_viz.get_data(df)
-        # Check method correctly transforms data
-        assert {"count", "sum__A"} == set(data["columns"])
-        time_format = "%Y-%m-%d %H:%M:%S"
-        expected = {
-            t1.strftime(time_format): {"sum__A": 15, "count": 6},
-            t2.strftime(time_format): {"sum__A": 20, "count": 7},
-        }
-        assert expected == data["records"]
-
-    def test_get_data_group_by(self):
-        form_data = {"metrics": ["sum__A"], "groupby": ["groupby1"]}
-        datasource = self.get_datasource_mock()
-        raw = {}
-        t1 = pd.Timestamp("2000")
-        t2 = pd.Timestamp("2002")
-        raw[DTTM_ALIAS] = [t1, t1, t1, t2, t2, t2]
-        raw["sum__A"] = [15, 20, 25, 30, 35, 40]
-        raw["groupby1"] = ["a1", "a2", "a3", "a1", "a2", "a3"]
-        df = pd.DataFrame(raw)
-        test_viz = viz.TimeTableViz(datasource, form_data)
-        data = test_viz.get_data(df)
-        # Check method correctly transforms data
-        assert {"a1", "a2", "a3"} == set(data["columns"])
-        time_format = "%Y-%m-%d %H:%M:%S"
-        expected = {
-            t1.strftime(time_format): {"a1": 15, "a2": 20, "a3": 25},
-            t2.strftime(time_format): {"a1": 30, "a2": 35, "a3": 40},
-        }
-        assert expected == data["records"]
-
-    def test_get_data_multiple_group_by(self):
-        sep = FLAT_COLUMN_SEPARATOR
-        form_data = {"metrics": ["sum__A"], "groupby": ["groupby1", "groupby2"]}
-        datasource = self.get_datasource_mock()
-        raw = {}
-        t1 = pd.Timestamp("2000")
-        t2 = pd.Timestamp("2002")
-        raw[DTTM_ALIAS] = [t1, t1, t1, t1, t2, t2, t2, t2]
-        raw["sum__A"] = [15, 20, 25, 30, 35, 40, 45, 50]
-        raw["groupby1"] = ["a1", "a2", "a1", "a2", "a1", "a2", "a1", "a2"]
-        raw["groupby2"] = ["b1", "b1", "b2", "b2", "b1", "b1", "b2", "b2"]
-        df = pd.DataFrame(raw)
-        test_viz = viz.TimeTableViz(datasource, form_data)
-        data = test_viz.get_data(df)
-        # Columns should be flattened strings, not tuples
-        a1_b1 = f"a1{sep}b1"
-        a1_b2 = f"a1{sep}b2"
-        a2_b1 = f"a2{sep}b1"
-        a2_b2 = f"a2{sep}b2"
-        assert {a1_b1, a1_b2, a2_b1, a2_b2} == set(data["columns"])
-        time_format = "%Y-%m-%d %H:%M:%S"
-        expected = {
-            t1.strftime(time_format): {
-                a1_b1: 15,
-                a1_b2: 25,
-                a2_b1: 20,
-                a2_b2: 30,
-            },
-            t2.strftime(time_format): {
-                a1_b1: 35,
-                a1_b2: 45,
-                a2_b1: 40,
-                a2_b2: 50,
-            },
-        }
-        assert expected == data["records"]
-        assert data["is_group_by"] is True
-
-    @patch("superset.viz.BaseViz.query_obj")
-    def test_query_obj_throws_metrics_and_groupby(self, super_query_obj):
-        datasource = self.get_datasource_mock()
-        form_data = {"groupby": ["a"]}
-        super_query_obj.return_value = {}
-        test_viz = viz.TimeTableViz(datasource, form_data)
-        with self.assertRaises(Exception):  # noqa: B017, PT027
-            test_viz.query_obj()
-        form_data["metrics"] = ["x", "y"]
-        test_viz = viz.TimeTableViz(datasource, form_data)
-        with self.assertRaises(Exception):  # noqa: B017, PT027
-            test_viz.query_obj()
-
-    def test_query_obj_order_by(self):
-        test_viz = viz.TimeTableViz(
-            self.get_datasource_mock(), {"metrics": ["sum__A", "count"], "groupby": []}
-        )
-        query_obj = test_viz.query_obj()
-        assert query_obj["orderby"] == [("sum__A", False)]
-
-
-class TestBaseDeckGLViz(SupersetTestCase):
-    def test_get_metrics(self):
-        form_data = load_fixture("deck_path_form_data.json")
-        datasource = self.get_datasource_mock()
-        test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data)
-        result = test_viz_deckgl.get_metrics()
-        assert result == [form_data.get("size")]
-
-        form_data = {}
-        test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data)
-        result = test_viz_deckgl.get_metrics()
-        assert result == []
-
-    def test_extract_tooltip_columns_string_items(self):
-        """Test _extract_tooltip_columns with string items in tooltip_contents"""
-        form_data = {"tooltip_contents": ["column1", "column2", "column3"]}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._extract_tooltip_columns()
-        assert result == ["column1", "column2", "column3"]
-
-    def test_extract_tooltip_columns_dict_items(self):
-        """Test _extract_tooltip_columns with dict items in tooltip_contents"""
-        form_data = {
-            "tooltip_contents": [
-                {"item_type": "column", "column_name": "LAT"},
-                {"item_type": "column", "column_name": "LON"},
-                {"item_type": "metric", "metric_name": "count"},
-                {"item_type": "column", "column_name": "CITY"},
-            ]
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._extract_tooltip_columns()
-        assert result == ["LAT", "LON", "CITY"]
-
-    def test_extract_tooltip_columns_mixed_items(self):
-        """Test _extract_tooltip_columns with mixed string and dict items"""
-        form_data = {
-            "tooltip_contents": [
-                "string_column",
-                {"item_type": "column", "column_name": "dict_column"},
-                {
-                    "item_type": "invalid",
-                    "column_name": "invalid_column",
-                },
-                {"item_type": "column"},
-            ]
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._extract_tooltip_columns()
-        assert result == ["string_column", "dict_column"]
-
-    def test_extract_tooltip_columns_empty(self):
-        """Test _extract_tooltip_columns with empty or missing tooltip_contents"""
-        form_data = {"tooltip_contents": []}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._extract_tooltip_columns()
-        assert result == []
-
-        form_data = {}
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._extract_tooltip_columns()
-        assert result == []
-
-    def test_add_tooltip_columns_to_query_with_metrics(self):
-        """Test _add_tooltip_columns_to_query when query has metrics"""
-        form_data = {}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        query_obj = {
-            "metrics": ["count", "avg_value"],
-            "groupby": ["existing_column"],
-            "columns": [],
-        }
-        tooltip_columns = ["tooltip_col1", "tooltip_col2", "existing_column"]
-
-        test_viz._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-
-        # Should add to groupby, avoiding duplicates
-        assert "tooltip_col1" in query_obj["groupby"]
-        assert "tooltip_col2" in query_obj["groupby"]
-        assert query_obj["groupby"].count("existing_column") == 1  # No duplicate
-
-    def test_add_tooltip_columns_to_query_without_metrics(self):
-        """Test _add_tooltip_columns_to_query when query has no metrics"""
-        form_data = {}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        query_obj = {"metrics": [], "groupby": [], "columns": ["existing_column"]}
-        tooltip_columns = ["tooltip_col1", "tooltip_col2", "existing_column"]
-
-        test_viz._add_tooltip_columns_to_query(query_obj, tooltip_columns)
-
-        # Should add to columns, avoiding duplicates
-        assert "tooltip_col1" in query_obj["columns"]
-        assert "tooltip_col2" in query_obj["columns"]
-        assert query_obj["columns"].count("existing_column") == 1  # No duplicate
-
-    def test_add_tooltip_columns_to_query_empty_columns(self):
-        """Test _add_tooltip_columns_to_query with empty tooltip columns"""
-        form_data = {}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        query_obj = {"metrics": [], "groupby": [], "columns": []}
-        original_query_obj = query_obj.copy()
-
-        test_viz._add_tooltip_columns_to_query(query_obj, [])
-
-        # Should not modify query_obj
-        assert query_obj == original_query_obj
-
-    def test_integrate_tooltip_columns(self):
-        """Test _integrate_tooltip_columns helper method"""
-        form_data = {"tooltip_contents": ["LON", "LAT"]}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        query_obj = {"metrics": ["count"], "groupby": ["region"]}
-
-        result = test_viz._integrate_tooltip_columns(query_obj)
-
-        assert "LON" in result["groupby"]
-        assert "LAT" in result["groupby"]
-        assert "region" in result["groupby"]
-        assert result is query_obj  # Should return the same object
-
-    def test_add_tooltip_properties(self):
-        """Test _add_tooltip_properties helper method"""
-        form_data = {"tooltip_contents": ["CITY", "POPULATION", "missing_column"]}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        properties = {"position": [1, 2], "weight": 100}
-        data = {"CITY": "New York", "POPULATION": 8000000, "other_column": "value"}
-
-        result = test_viz._add_tooltip_properties(properties, data)
-
-        assert result["CITY"] == "New York"
-        assert result["POPULATION"] == 8000000
-        assert "missing_column" not in result  # Missing column should not be added
-        assert "other_column" not in result  # Non-tooltip column should not be added
-        assert result["position"] == [1, 2]  # Original properties should remain
-        assert result["weight"] == 100
-        assert result is properties  # Should return the same object
-
-    def test_get_base_properties(self):
-        """Test _get_base_properties helper method"""
-        form_data = {}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-        test_viz.metric_label = "test_metric"
-
-        data = {"spatial": [10.5, 20.3], "test_metric": 42, "other_field": "value"}
-
-        result = test_viz._get_base_properties(data)
-
-        assert result["position"] == [10.5, 20.3]
-        assert result["weight"] == 42
-        assert len(result) == 2  # Should only have position and weight
-
-    def test_get_base_properties_no_metric(self):
-        """Test _get_base_properties when no metric is available"""
-        form_data = {}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-        test_viz.metric_label = None
-
-        data = {"spatial": [10.5, 20.3]}
-
-        result = test_viz._get_base_properties(data)
-
-        assert result["position"] == [10.5, 20.3]
-        assert result["weight"] == 1  # Should default to 1
-
-    def test_setup_metric_label(self):
-        """Test _setup_metric_label helper method"""
-        form_data = {}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-        test_viz.metric = "SUM(sales)"
-
-        with patch("superset.utils.core.get_metric_name") as mock_get_metric_name:
-            mock_get_metric_name.return_value = "sum__sales"
-
-            test_viz._setup_metric_label()
-
-            assert test_viz.metric_label == "sum__sales"
-            mock_get_metric_name.assert_called_once_with("SUM(sales)")
-
-    def test_setup_metric_label_no_metric(self):
-        """Test _setup_metric_label when metric is None"""
-        form_data = {}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-        test_viz.metric = None
-
-        test_viz._setup_metric_label()
-
-        assert test_viz.metric_label is None
-
-    def test_scatterviz_get_metrics(self):
-        form_data = load_fixture("deck_path_form_data.json")
-        datasource = self.get_datasource_mock()
-
-        form_data = {}
-        test_viz_deckgl = viz.DeckScatterViz(datasource, form_data)
-        test_viz_deckgl.point_radius_fixed = {"type": "metric", "value": "int"}
-        result = test_viz_deckgl.get_metrics()
-        assert result == ["int"]
-
-        form_data = {}
-        test_viz_deckgl = viz.DeckScatterViz(datasource, form_data)
-        test_viz_deckgl.point_radius_fixed = {}
-        result = test_viz_deckgl.get_metrics()
-        assert result == []
-
-    def test_get_js_columns(self):
-        form_data = load_fixture("deck_path_form_data.json")
-        datasource = self.get_datasource_mock()
-        mock_d = {"a": "dummy1", "b": "dummy2", "c": "dummy3"}
-        test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data)
-        result = test_viz_deckgl.get_js_columns(mock_d)
-
-        assert result == {"color": None}
-
-    def test_get_properties(self):
-        mock_d = {}
-        form_data = load_fixture("deck_path_form_data.json")
-        datasource = self.get_datasource_mock()
-        test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data)
-
-        with self.assertRaises(NotImplementedError) as context:  # noqa: PT027
-            test_viz_deckgl.get_properties(mock_d)
-
-        assert "" in str(context.exception)
-
-    def test_process_spatial_query_obj(self):
-        form_data = load_fixture("deck_path_form_data.json")
-        datasource = self.get_datasource_mock()
-        mock_key = "spatial_key"
-        mock_gb = []
-        test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data)
-
-        with self.assertRaises(ValueError) as context:  # noqa: PT027
-            test_viz_deckgl.process_spatial_query_obj(mock_key, mock_gb)
-
-        assert "Bad spatial key" in str(context.exception)
-
-        test_form_data = {
-            "latlong_key": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
-            "delimited_key": {"type": "delimited", "lonlatCol": "lonlat"},
-            "geohash_key": {"type": "geohash", "geohashCol": "geo"},
-        }
-
-        datasource = self.get_datasource_mock()
-        expected_results = {
-            "latlong_key": ["lon", "lat"],
-            "delimited_key": ["lonlat"],
-            "geohash_key": ["geo"],
-        }
-        for mock_key in ["latlong_key", "delimited_key", "geohash_key"]:
-            mock_gb = []
-            test_viz_deckgl = viz.BaseDeckGLViz(datasource, test_form_data)
-            test_viz_deckgl.process_spatial_query_obj(mock_key, mock_gb)
-            assert expected_results.get(mock_key) == mock_gb
-
-    def test_geojson_query_obj(self):
-        form_data = load_fixture("deck_geojson_form_data.json")
-        datasource = self.get_datasource_mock()
-        test_viz_deckgl = viz.DeckGeoJson(datasource, form_data)
-        results = test_viz_deckgl.query_obj()
-
-        assert results["metrics"] == []
-        assert results["groupby"] == []
-        assert results["columns"] == ["test_col"]
-
-    def test_parse_coordinates(self):
-        form_data = load_fixture("deck_path_form_data.json")
-        datasource = self.get_datasource_mock()
-        viz_instance = viz.BaseDeckGLViz(datasource, form_data)
-
-        coord = viz_instance.parse_coordinates("1.23, 3.21")
-        assert coord == (1.23, 3.21)
-
-        coord = viz_instance.parse_coordinates("1.23 3.21")
-        assert coord == (1.23, 3.21)
-
-        assert viz_instance.parse_coordinates(None) is None
-
-        assert viz_instance.parse_coordinates("") is None
-
-    def test_parse_coordinates_raises(self):
-        form_data = load_fixture("deck_path_form_data.json")
-        datasource = self.get_datasource_mock()
-        test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data)
-
-        with self.assertRaises(SpatialException):  # noqa: PT027
-            test_viz_deckgl.parse_coordinates("NULL")
-
-        with self.assertRaises(SpatialException):  # noqa: PT027
-            test_viz_deckgl.parse_coordinates("fldkjsalkj,fdlaskjfjadlksj")
-
-    def test_deckscatter_query_obj_integration(self):
-        """Test DeckScatterViz query_obj uses new tooltip integration"""
-        form_data = {
-            "tooltip_contents": ["CITY", "POPULATION"],
-            "time_grain_sqla": "P1D",
-            "point_radius_fixed": {"type": "fix", "value": 500},
-            "spatial": {
-                "type": "latlong",
-                "lonCol": "longitude",
-                "latCol": "latitude",
-            },
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.DeckScatterViz(datasource, form_data)
-
-        with patch.object(test_viz, "_integrate_tooltip_columns") as mock_integrate:
-            mock_integrate.return_value = {"mocked": "result"}
-
-            result = test_viz.query_obj()
-
-            # Should call the new helper method
-            mock_integrate.assert_called_once()
-            assert result == {"mocked": "result"}
-
-            # Should set up instance variables
-            assert test_viz.is_timeseries is True
-            assert test_viz.point_radius_fixed == {"type": "fix", "value": 500}
-
-    def test_deckscatter_query_obj_no_time_grain(self):
-        """Test DeckScatterViz query_obj when time_grain_sqla is not set"""
-        form_data = {
-            "tooltip_contents": ["CITY"],
-            "point_radius_fixed": {"type": "metric", "value": "count"},
-            "spatial": {
-                "type": "latlong",
-                "lonCol": "longitude",
-                "latCol": "latitude",
-            },
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.DeckScatterViz(datasource, form_data)
-
-        with patch.object(test_viz, "_integrate_tooltip_columns") as mock_integrate:
-            mock_integrate.return_value = {"result": "data"}
-
-            test_viz.query_obj()
-
-            # Should set timeseries to False when no time_grain_sqla
-            assert test_viz.is_timeseries is False
-
-    def test_deckscatter_get_properties_uses_helper(self):
-        """Test DeckScatterViz get_properties uses new helper method"""
-        form_data = {"tooltip_contents": ["CITY", "STATE"], "dimension": "region"}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.DeckScatterViz(datasource, form_data)
-        test_viz.metric_label = "count"
-        test_viz.fixed_value = 10
-        test_viz.dim = "region"
-
-        data = {
-            "spatial": [40.7, -74.0],
-            "count": 100,
-            "region": "northeast",
-            "CITY": "New York",
-            "STATE": "NY",
-            "__timestamp": "2023-01-01",
-        }
-
-        with patch.object(test_viz, "_add_tooltip_properties") as mock_add_tooltip:
-            mock_add_tooltip.return_value = {"final": "result"}
-
-            result = test_viz.get_properties(data)
-
-            expected_base_properties = {
-                "metric": 100,
-                "radius": 10,
-                "cat_color": "northeast",
-                "position": [40.7, -74.0],
-                "__timestamp": "2023-01-01",
-            }
-            mock_add_tooltip.assert_called_once_with(expected_base_properties, data)
-            assert result == {"final": "result"}
-
-    def test_deckscatter_get_data_uses_setup_metric_label(self):
-        """Test DeckScatterViz get_data uses new _setup_metric_label helper"""
-        form_data = {
-            "point_radius_fixed": {"type": "metric", "value": "avg_sales"},
-            "dimension": "category",
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.DeckScatterViz(datasource, form_data)
-        test_df = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]})
-
-        with patch.object(test_viz, "_setup_metric_label") as mock_setup:
-            with patch("superset.viz.BaseDeckGLViz.get_data") as mock_super_get_data:
-                mock_super_get_data.return_value = {"test": "data"}
-
-                result = test_viz.get_data(test_df)
-
-                mock_setup.assert_called_once()
-
-                assert test_viz.point_radius_fixed == {
-                    "type": "metric",
-                    "value": "avg_sales",
-                }
-                assert test_viz.fixed_value is None
-                assert test_viz.dim == "category"
-
-                mock_super_get_data.assert_called_once_with(test_df)
-                assert result == {"test": "data"}
-
-    def test_deckscatter_get_data_with_fixed_radius(self):
-        """Test DeckScatterViz get_data with fixed radius type"""
-        form_data = {
-            "point_radius_fixed": {"type": "fix", "value": 25},
-            "dimension": "location",
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.DeckScatterViz(datasource, form_data)
-        test_df = pd.DataFrame({"col1": [1, 2]})
-
-        with patch.object(test_viz, "_setup_metric_label"):
-            with patch("superset.viz.BaseDeckGLViz.get_data") as mock_super_get_data:
-                mock_super_get_data.return_value = {"result": "ok"}
-
-                test_viz.get_data(test_df)
-
-                # Should set fixed_value when type is not 'metric'
-                assert test_viz.fixed_value == 25
-
-    def test_tooltip_integration_end_to_end(self):
-        """Test complete tooltip integration from query_obj to get_properties"""
-        form_data = {
-            "tooltip_contents": ["CITY", "POPULATION"],
-            "size": "count",
-            "time_grain_sqla": None,
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.DeckScatterViz(datasource, form_data)
-
-        with patch("superset.viz.BaseDeckGLViz.query_obj") as mock_super_query:
-            mock_super_query.return_value = {
-                "metrics": ["count"],
-                "groupby": ["region"],
-                "columns": [],
-            }
-
-            query_result = test_viz.query_obj()
-
-            # Should have added tooltip columns to groupby
-            assert "CITY" in query_result["groupby"]
-            assert "POPULATION" in query_result["groupby"]
-            assert "region" in query_result["groupby"]
-
-        test_viz.metric_label = "count"
-        test_viz.fixed_value = None
-        test_viz.dim = "region"
-
-        data = {
-            "spatial": [40.7, -74.0],
-            "count": 5000,
-            "region": "northeast",
-            "CITY": "New York",
-            "POPULATION": 8000000,
-            "__timestamp": "2023-01-01",
-        }
-
-        properties_result = test_viz.get_properties(data)
-
-        # Should include both base properties and tooltip properties
-        assert properties_result["position"] == [40.7, -74.0]
-        assert properties_result["metric"] == 5000
-        assert properties_result["cat_color"] == "northeast"
-        assert properties_result["__timestamp"] == "2023-01-01"
-        assert properties_result["CITY"] == "New York"
-        assert properties_result["POPULATION"] == 8000000
-
-    def test_filter_nulls(self):
-        test_form_data = {
-            "latlong_key": {"type": "latlong", "lonCol": "lon", "latCol": "lat"},
-            "delimited_key": {"type": "delimited", "lonlatCol": "lonlat"},
-            "geohash_key": {"type": "geohash", "geohashCol": "geo"},
-        }
-
-        datasource = self.get_datasource_mock()
-        # SHA-256 filterOptionName hashes with default HASH_ALGORITHM
-        expected_results = {
-            "latlong_key": [
-                {
-                    "clause": "WHERE",
-                    "expressionType": "SIMPLE",
-                    "filterOptionName": (
-                        "980dd3068274177120307d9182ea8e8ee1b7824d34fbc21c529441f5d3279f7f"
-                    ),
-                    "comparator": "",
-                    "operator": "IS NOT NULL",
-                    "subject": "lat",
-                },
-                {
-                    "clause": "WHERE",
-                    "expressionType": "SIMPLE",
-                    "filterOptionName": (
-                        "e368c259da27e5ec6a854772d9bff2c2af8dd5762352cef4ff6afc5bd8b6b9ea"
-                    ),
-                    "comparator": "",
-                    "operator": "IS NOT NULL",
-                    "subject": "lon",
-                },
-            ],
-            "delimited_key": [
-                {
-                    "clause": "WHERE",
-                    "expressionType": "SIMPLE",
-                    "filterOptionName": (
-                        "6ea33b70ab781033af421240019d3e3ad782928a3ad2999538f1f4b2a52305e2"
-                    ),
-                    "comparator": "",
-                    "operator": "IS NOT NULL",
-                    "subject": "lonlat",
-                }
-            ],
-            "geohash_key": [
-                {
-                    "clause": "WHERE",
-                    "expressionType": "SIMPLE",
-                    "filterOptionName": (
-                        "48bbd94cd6afb1885d8550e2928bc01a2d3bc7d1f4f1d0929b10d6f4021b7f14"
-                    ),
-                    "comparator": "",
-                    "operator": "IS NOT NULL",
-                    "subject": "geo",
-                }
-            ],
-        }
-        for mock_key in ["latlong_key", "delimited_key", "geohash_key"]:
-            test_viz_deckgl = viz.BaseDeckGLViz(datasource, test_form_data.copy())
-            test_viz_deckgl.spatial_control_keys = [mock_key]
-            test_viz_deckgl.add_null_filters()
-            adhoc_filters = test_viz_deckgl.form_data["adhoc_filters"]
-            assert expected_results.get(mock_key) == adhoc_filters
-
-    def test_init_with_layer_filtering_applied(self):
-        """Test BaseDeckGLViz.__init__ applies layer filtering when conditions are
-        met."""
-        datasource = self.get_datasource_mock()
-        form_data = {
-            "slice_id": 123,
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": [0, 1],
-                    "deck_slices": [123, 456],
-                },
-                {
-                    "clause": "WHERE",
-                    "subject": "col2",
-                    "operator": "!=",
-                    "comparator": "value2",
-                    "layerFilterScope": [1],
-                    "deck_slices": [123, 456],
-                },
-            ],
-        }
-
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        # Should only have the first filter (scoped to layer 0)
-        assert len(test_viz.form_data["adhoc_filters"]) == 1
-        assert test_viz.form_data["adhoc_filters"][0]["subject"] == "col1"
-
-    def test_init_without_layer_filtering(self):
-        """Test BaseDeckGLViz.__init__ doesn't apply filtering when conditions
-        aren't met."""
-        datasource = self.get_datasource_mock()
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                }
-            ]
-        }
-        original_filters = form_data["adhoc_filters"].copy()
-
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        # Filters should remain unchanged
-        assert test_viz.form_data["adhoc_filters"] == original_filters
-
-    def test_should_apply_layer_filtering_true(self):
-        """Test _should_apply_layer_filtering returns True when all conditions are
-        met."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        form_data = {"slice_id": 123, "adhoc_filters": [{"layerFilterScope": [0, 1]}]}
-
-        result = test_viz._should_apply_layer_filtering(form_data)
-        assert result is True
-
-    def test_should_apply_layer_filtering_false_missing_slice_id(self):
-        """Test _should_apply_layer_filtering returns False when slice_id is missing."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        form_data = {"adhoc_filters": [{"layerFilterScope": [0, 1]}]}
-
-        result = test_viz._should_apply_layer_filtering(form_data)
-        assert result is False
-
-    def test_should_apply_layer_filtering_false_missing_adhoc_filters(self):
-        """Test _should_apply_layer_filtering returns False when
-        adhoc_filters missing"""
-        form_data = {"slice_id": 123}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._should_apply_layer_filtering(form_data)
-
-        assert result is False
-
-    def test_should_apply_layer_filtering_false_no_layer_scoped_filters(self):
-        """Test _should_apply_layer_filtering returns False when no
-        layer scoped filters"""
-        form_data = {
-            "slice_id": 123,
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._should_apply_layer_filtering(form_data)
-
-        assert result is False
-
-    def test_has_layer_scoped_filters_true_with_dict(self):
-        """Test _has_layer_scoped_filters returns True when filter
-        has layerFilterScope"""
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": [0, 1],
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._has_layer_scoped_filters(form_data)
-
-        assert result is True
-
-    def test_has_layer_scoped_filters_true_with_non_none_value(self):
-        """Test _has_layer_scoped_filters returns True when layerFilterScope not None"""
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": [],
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._has_layer_scoped_filters(form_data)
-
-        assert result is True
-
-    def test_has_layer_scoped_filters_false_none_value(self):
-        """Test _has_layer_scoped_filters returns False when layerFilterScope is None"""
-        form_data = {"adhoc_filters": [{"layerFilterScope": None}, {"clause": "WHERE"}]}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._has_layer_scoped_filters(form_data)
-
-        assert result is False
-
-    def test_has_layer_scoped_filters_false_no_scoped_filters(self):
-        """Test _has_layer_scoped_filters returns False when no filters
-        have layerFilterScope"""
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._has_layer_scoped_filters(form_data)
-
-        assert result is False
-
-    def test_apply_multilayer_filtering_filters_by_layer_scope(self):
-        """Test _apply_multilayer_filtering correctly filters by layer scope."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        form_data = {
-            "slice_id": 456,  # This is layer index 1
-            "adhoc_filters": [
-                {
-                    "subject": "global_filter",
-                    "deck_slices": [123, 456],
-                    # No layerFilterScope = global filter
-                },
-                {
-                    "subject": "layer_0_filter",
-                    "layerFilterScope": [0],
-                    "deck_slices": [123, 456],
-                },
-                {
-                    "subject": "layer_1_filter",
-                    "layerFilterScope": [1],
-                    "deck_slices": [123, 456],
-                },
-                {
-                    "subject": "layer_0_1_filter",
-                    "layerFilterScope": [0, 1],
-                    "deck_slices": [123, 456],
-                },
-            ],
-        }
-
-        result = test_viz._apply_multilayer_filtering(form_data)
-
-        # Should include: global_filter, layer_1_filter, layer_0_1_filter
-        assert len(result["adhoc_filters"]) == 3
-        subjects = [f["subject"] for f in result["adhoc_filters"]]
-        assert "global_filter" in subjects
-        assert "layer_1_filter" in subjects
-        assert "layer_0_1_filter" in subjects
-        assert "layer_0_filter" not in subjects
-
-    def test_apply_multilayer_filtering_no_deck_slices(self):
-        """Test _apply_multilayer_filtering returns original form_data when no
-        deck_slices."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        form_data = {"slice_id": 123, "adhoc_filters": [{"subject": "filter1"}]}
-
-        result = test_viz._apply_multilayer_filtering(form_data)
-
-        # Should return original form_data unchanged
-        assert result == form_data
-
-    def test_apply_multilayer_filtering_slice_not_in_deck_slices(self):
-        """Test _apply_multilayer_filtering returns original when slice_id not in
-        deck_slices."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        form_data = {
-            "slice_id": 999,  # Not in deck_slices
-            "adhoc_filters": [{"subject": "filter1", "deck_slices": [123, 456]}],
-        }
-
-        result = test_viz._apply_multilayer_filtering(form_data)
-
-        # Should return original form_data unchanged
-        assert result == form_data
-
-    def test_get_deck_slices_from_filters_found(self):
-        """Test _get_deck_slices_from_filters returns deck_slices when found."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        form_data = {
-            "adhoc_filters": [
-                {"subject": "filter1"},
-                {"subject": "filter2", "deck_slices": [123, 456, 789]},
-                {"subject": "filter3"},
-            ]
-        }
-
-        result = test_viz._get_deck_slices_from_filters(form_data)
-
-        assert result == [123, 456, 789]
-
-    def test_get_deck_slices_from_filters_not_found(self):
-        """Test _get_deck_slices_from_filters returns None when not found."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        form_data = {"adhoc_filters": [{"subject": "filter1"}, {"subject": "filter2"}]}
-
-        result = test_viz._get_deck_slices_from_filters(form_data)
-
-        assert result is None
-
-    def test_get_deck_slices_from_filters_empty_filters(self):
-        """Test _get_deck_slices_from_filters returns None when adhoc_filters is
-        empty."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        form_data = {"adhoc_filters": []}
-
-        result = test_viz._get_deck_slices_from_filters(form_data)
-
-        assert result is None
-
-    def test_get_filter_layer_scope_dict(self):
-        """Test _get_filter_layer_scope with dict filter item."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        filter_item = {"layerFilterScope": [0, 1, 2]}
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result == [0, 1, 2]
-
-    def test_get_filter_layer_scope_dict_none(self):
-        """Test _get_filter_layer_scope with dict that has None layerFilterScope."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        filter_item = {"layerFilterScope": None}
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result is None
-
-    def test_get_filter_layer_scope_dict_missing_key(self):
-        """Test _get_filter_layer_scope with dict missing layerFilterScope."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        filter_item = {"subject": "col1"}
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result is None
-
-    def test_get_filter_layer_scope_object_with_attribute(self):
-        """Test _get_filter_layer_scope with object having layerFilterScope
-        attribute."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        from unittest.mock import Mock
-
-        filter_item = Mock()
-        filter_item.layerFilterScope = [1, 2]
-
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result == [1, 2]
-
-    def test_get_filter_layer_scope_object_without_attribute(self):
-        """Test _get_filter_layer_scope with object missing layerFilterScope
-        attribute."""
-        filter_item = Mock()
-        del filter_item.layerFilterScope
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result is None
-
-    def test_get_filter_layer_scope_non_dict_non_object(self):
-        """Test _get_filter_layer_scope with non-dict, non-object types."""
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        # Test with string
-        result = test_viz._get_filter_layer_scope("string_filter")
-        assert result is None
-
-        # Test with number
-        result = test_viz._get_filter_layer_scope(123)
-        assert result is None
-
-        # Test with None
-        result = test_viz._get_filter_layer_scope(None)
-        assert result is None
-
-
-class TestDeckGLMultiLayer(SupersetTestCase):
-    def test_filter_items_by_scope_with_filter_id(self):
-        """Test _filter_items_by_scope method with items having filterId."""
-        datasource = self.get_datasource_mock()
-        form_data = {}
-        test_viz = viz.DeckGLMultiLayer(datasource, form_data)
-
-        filter_item_1 = Mock()
-        filter_item_1.filterId = "filter_1"
-        filter_item_2 = Mock()
-        filter_item_2.filterId = "filter_2"
-        filter_item_3 = Mock()
-        filter_item_3.filterId = "filter_3"
-
-        items = [filter_item_1, filter_item_2, filter_item_3]
-        layer_index = 0
-        layer_filter_scope = {"filter_1": [0, 1], "filter_2": [1], "filter_3": []}
-
-        result = test_viz._filter_items_by_scope(items, layer_index, layer_filter_scope)
-
-        assert len(result) == 2
-        assert filter_item_1 in result
-        assert filter_item_3 in result
-        assert filter_item_2 not in result
-
-    def test_filter_items_by_scope_without_filter_id(self):
-        """Test _filter_items_by_scope method with items without filterId."""
-        datasource = self.get_datasource_mock()
-        form_data = {}
-        test_viz = viz.DeckGLMultiLayer(datasource, form_data)
-
-        filter_item_1 = Mock()
-        del filter_item_1.filterId
-        filter_item_2 = Mock()
-        filter_item_2.filterId = None
-
-        items = [filter_item_1, filter_item_2]
-        layer_index = 0
-        layer_filter_scope = {"filter_1": [1]}
-
-        result = test_viz._filter_items_by_scope(items, layer_index, layer_filter_scope)
-
-        assert len(result) == 2
-        assert filter_item_1 in result
-        assert filter_item_2 in result
-
-    def test_process_extra_form_data_filters(self):
-        """Test _process_extra_form_data_filters method."""
-        datasource = self.get_datasource_mock()
-        form_data = {}
-        test_viz = viz.DeckGLMultiLayer(datasource, form_data)
-
-        layer_index = 0
-        layer_filter_scope = {"filter_1": [0, 1], "filter_2": [1], "filter_3": []}
-        filter_data_mapping = {
-            "filter_1": [{"column": "col1", "op": "==", "val": "value1"}],
-            "filter_2": [{"column": "col2", "op": "!=", "val": "value2"}],
-            "filter_3": [{"column": "col3", "op": ">", "val": 100}],
-        }
-        extra_form_data = {"existing_key": "existing_value"}
-
-        result = test_viz._process_extra_form_data_filters(
-            layer_index, layer_filter_scope, filter_data_mapping, extra_form_data
-        )
-
-        expected_filters = [
-            {"column": "col1", "op": "==", "val": "value1"},
-            {"column": "col3", "op": ">", "val": 100},
-        ]
-        assert result["filters"] == expected_filters
-        assert result["existing_key"] == "existing_value"
-
-    def test_process_extra_form_data_filters_empty_inputs(self):
-        """Test _process_extra_form_data_filters with empty inputs."""
-        datasource = self.get_datasource_mock()
-        form_data = {}
-        test_viz = viz.DeckGLMultiLayer(datasource, form_data)
-
-        result = test_viz._process_extra_form_data_filters(0, {}, {}, {})
-        assert result == {}
-
-        extra_form_data = {"key": "value"}
-        result = test_viz._process_extra_form_data_filters(0, {}, {}, extra_form_data)
-        assert result == extra_form_data
-
-    def test_apply_layer_filtering_without_layer_filter_scope(self):
-        """Test _apply_layer_filtering when layer_filter_scope is empty."""
-        datasource = self.get_datasource_mock()
-        form_data = {
-            "extra_filters": [Mock(), Mock()],
-            "adhoc_filters": [Mock()],
-            "extra_form_data": {"key": "value"},
-        }
-        test_viz = viz.DeckGLMultiLayer(datasource, form_data)
-
-        layer_form_data = {"viz_type": "deck_scatter"}
-        layer_index = 0
-
-        result = test_viz._apply_layer_filtering(layer_form_data, layer_index)
-
-        assert result["extra_filters"] == form_data["extra_filters"]
-        assert result["adhoc_filters"] == form_data["adhoc_filters"]
-        assert result["extra_form_data"] == form_data["extra_form_data"]
-
-    def test_apply_layer_filtering_with_layer_filter_scope(self):
-        """Test _apply_layer_filtering with layer_filter_scope."""
-        datasource = self.get_datasource_mock()
-
-        extra_filter_1 = Mock()
-        extra_filter_1.filterId = "filter_1"
-        extra_filter_2 = Mock()
-        extra_filter_2.filterId = "filter_2"
-
-        adhoc_filter_1 = Mock()
-        adhoc_filter_1.filterId = "filter_1"
-
-        form_data = {
-            "layer_filter_scope": {"filter_1": [0], "filter_2": [1]},
-            "filter_data_mapping": {
-                "filter_1": [{"column": "col1", "op": "==", "val": "value1"}]
-            },
-            "extra_filters": [extra_filter_1, extra_filter_2],
-            "adhoc_filters": [adhoc_filter_1],
-            "extra_form_data": {"existing": "data"},
-        }
-        test_viz = viz.DeckGLMultiLayer(datasource, form_data)
-
-        layer_form_data = {"viz_type": "deck_scatter"}
-        layer_index = 0
-
-        result = test_viz._apply_layer_filtering(layer_form_data, layer_index)
-
-        assert len(result["extra_filters"]) == 1
-        assert result["extra_filters"][0].filterId == "filter_1"
-        assert len(result["adhoc_filters"]) == 1
-        assert result["adhoc_filters"][0].filterId == "filter_1"
-        assert result["extra_form_data"]["filters"] == [
-            {"column": "col1", "op": "==", "val": "value1"}
-        ]
-
-    @with_config({"MAPBOX_API_KEY": "test_key"})
-    @patch("superset.viz.viz_types")
-    @patch("superset.db.session")
-    def test_get_data_with_layer_filtering(self, mock_db_session, mock_viz_types):
-        """Test get_data method with layer filtering enabled."""
-        datasource = self.get_datasource_mock()
-
-        slice_1 = Mock()
-        slice_1.form_data = {"viz_type": "deck_scatter", "layer_name": "Layer 1"}
-        slice_1.data = {"features": [{"type": "Feature"}]}
-        slice_1.datasource = datasource
-
-        slice_2 = Mock()
-        slice_2.form_data = {"viz_type": "deck_path", "layer_name": "Layer 2"}
-        slice_2.data = {"features": [{"type": "Feature"}]}
-        slice_2.datasource = datasource
-
-        # Mock the database query to return our slice objects
-        mock_db_session.query.return_value.filter.return_value.all.return_value = [
-            slice_1,
-            slice_2,
-        ]
-
-        mock_scatter_viz_class = Mock()
-        mock_scatter_viz_instance = Mock()
-        mock_scatter_viz_instance.get_payload.return_value = {
-            "data": {"features": [{"id": 1}]}
-        }
-        mock_scatter_viz_class.return_value = mock_scatter_viz_instance
-
-        mock_path_viz_class = Mock()
-        mock_path_viz_instance = Mock()
-        mock_path_viz_instance.get_payload.return_value = {
-            "data": {"features": [{"id": 2}]}
-        }
-        mock_path_viz_class.return_value = mock_path_viz_instance
-
-        mock_viz_types.get.side_effect = lambda viz_type: {
-            "deck_scatter": mock_scatter_viz_class,
-            "deck_path": mock_path_viz_class,
-        }.get(viz_type)
-
-        form_data = {
-            "layer_filter_scope": {"filter_1": [0], "filter_2": [1]},
-            "filter_data_mapping": {
-                "filter_1": [{"column": "col1", "op": "==", "val": "value1"}],
-                "filter_2": [{"column": "col2", "op": "!=", "val": "value2"}],
-            },
-            "deck_slices": [1, 2],  # Use integer IDs instead of Mock objects
-        }
-
-        test_viz = viz.DeckGLMultiLayer(datasource, form_data)
-
-        test_viz._apply_layer_filtering = Mock(
-            side_effect=lambda form_data, idx: form_data
-        )
-
-        result = test_viz.get_data(pd.DataFrame())
-
-        assert test_viz._apply_layer_filtering.call_count == 2
-        test_viz._apply_layer_filtering.assert_any_call(slice_1.form_data, 0)
-        test_viz._apply_layer_filtering.assert_any_call(slice_2.form_data, 1)
-
-        assert isinstance(result, dict)
-        assert "features" in result
-        assert "mapboxApiKey" in result
-        assert "slices" in result
-        assert result["mapboxApiKey"] == "test_key"
-
-    @with_config({"MAPBOX_API_KEY": "test_key"})
-    @patch("superset.viz.viz_types")
-    @patch("superset.db.session")
-    def test_get_data_filters_none_data_slices(self, mock_db_session, mock_viz_types):
-        """Test get_data method filters out slices with None data."""
-        datasource = self.get_datasource_mock()
-
-        slice_1 = Mock()
-        slice_1.form_data = {"viz_type": "deck_scatter"}
-        slice_1.data = {"features": [{"type": "Feature"}]}
-        slice_1.datasource = datasource
-
-        slice_2 = Mock()
-        slice_2.form_data = {"viz_type": "deck_path"}
-        slice_2.data = None
-        slice_2.datasource = datasource
-
-        # Mock the database query to return our slice objects
-        mock_db_session.query.return_value.filter.return_value.all.return_value = [
-            slice_1,
-            slice_2,
-        ]
-
-        mock_viz_class = Mock()
-        mock_viz_instance = Mock()
-        mock_viz_instance.get_payload.return_value = {"data": {"features": []}}
-        mock_viz_class.return_value = mock_viz_instance
-        mock_viz_types.get.return_value = mock_viz_class
-
-        form_data = {"deck_slices": [1, 2]}  # Use integer IDs instead of Mock objects
-
-        test_viz = viz.DeckGLMultiLayer(datasource, form_data)
-        result = test_viz.get_data(pd.DataFrame())
-
-        assert isinstance(result, dict)
-        assert len(result["slices"]) == 1
-        assert result["slices"][0] == slice_1.data
-
-    @with_config({"MAPBOX_API_KEY": "test_key"})
-    @patch("superset.viz.viz_types")
-    @patch("superset.db.session")
-    def test_get_payload_includes_subslice_filter_metadata(
-        self,
-        mock_db_session,
-        mock_viz_types,
-    ):
-        """Test deck.gl multi-layer payload includes child filter metadata."""
-        datasource = self.get_datasource_mock()
-
-        slice_1 = Mock()
-        slice_1.form_data = {"viz_type": "deck_scatter"}
-        slice_1.data = {"features": [{"type": "Feature"}]}
-        slice_1.datasource = datasource
-
-        slice_2 = Mock()
-        slice_2.form_data = {"viz_type": "deck_path"}
-        slice_2.data = {"features": [{"type": "Feature"}]}
-        slice_2.datasource = datasource
-
-        mock_db_session.query.return_value.filter.return_value.all.return_value = [
-            slice_1,
-            slice_2,
-        ]
-
-        mock_scatter_viz_class = Mock()
-        mock_scatter_viz_instance = Mock()
-        mock_scatter_viz_instance.get_payload.return_value = {
-            "data": {"features": [{"id": 1}]},
-            "applied_filters": [{"column": "Latitude"}],
-            "rejected_filters": [],
-        }
-        mock_scatter_viz_class.return_value = mock_scatter_viz_instance
-
-        mock_path_viz_class = Mock()
-        mock_path_viz_instance = Mock()
-        mock_path_viz_instance.get_payload.return_value = {
-            "data": {"features": [{"id": 2}]},
-            "applied_filters": [
-                {"column": "Latitude"},
-                {"column": "Longitude"},
-            ],
-            "rejected_filters": [
-                {
-                    "column": "Country",
-                    "reason": ExtraFiltersReasonType.COL_NOT_IN_DATASOURCE,
-                },
-            ],
-        }
-        mock_path_viz_class.return_value = mock_path_viz_instance
-
-        mock_viz_types.get.side_effect = lambda viz_type: {
-            "deck_scatter": mock_scatter_viz_class,
-            "deck_path": mock_path_viz_class,
-        }.get(viz_type)
-
-        test_viz = viz.DeckGLMultiLayer(datasource, {"deck_slices": [1, 2]})
-        test_viz.get_df_payload = Mock(return_value={"df": pd.DataFrame()})
-
-        result = test_viz.get_payload()
-
-        assert result["applied_filters"] == [
-            {"column": "Latitude"},
-            {"column": "Longitude"},
-        ]
-        assert result["rejected_filters"] == [
-            {
-                "column": "Country",
-                "reason": ExtraFiltersReasonType.COL_NOT_IN_DATASOURCE,
-            },
-        ]
-
-    @with_config({"MAPBOX_API_KEY": "test_key"})
-    def test_get_data_empty_deck_slices(self):
-        """Test get_data method with empty deck_slices."""
-        datasource = self.get_datasource_mock()
-        form_data = {"deck_slices": []}
-
-        test_viz = viz.DeckGLMultiLayer(datasource, form_data)
-        result = test_viz.get_data(pd.DataFrame())
-
-        assert isinstance(result, dict)
-        assert result["features"] == {}
-        assert result["slices"] == []
-        assert result["mapboxApiKey"] == "test_key"
-
-    def test_get_base_properties_no_metric(self):
-        """Test _get_base_properties when metric_label is None"""
-        form_data = {}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-        test_viz.metric_label = None
-
-        data = {"spatial": [10.5, 20.3], "other_field": "value"}
-
-        result = test_viz._get_base_properties(data)
-
-        assert result["position"] == [10.5, 20.3]
-        assert result["weight"] == 1
-        assert len(result) == 2
-
-    def test_setup_metric_label(self):
-        """Test _setup_metric_label with metric"""
-        form_data = {"size": "test_metric"}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-        test_viz.metric = Mock()
-        test_viz.metric.value = "test_metric"
-
-        with patch("superset.viz.utils.get_metric_name") as mock_get_name:
-            mock_get_name.return_value = "test_metric"
-            test_viz._setup_metric_label()
-
-            assert test_viz.metric_label == "test_metric"
-            mock_get_name.assert_called_once_with(test_viz.metric)
-
-    def test_setup_metric_label_no_metric(self):
-        """Test _setup_metric_label without metric"""
-        form_data = {}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-        test_viz.metric = None
-
-        test_viz._setup_metric_label()
-
-        assert test_viz.metric_label is None
-
-    def test_should_apply_layer_filtering_true(self):
-        """Test _should_apply_layer_filtering returns True when conditions are met"""
-        form_data = {
-            "slice_id": 123,
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": [0, 1],
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._should_apply_layer_filtering(form_data)
-
-        assert result is True
-
-    def test_should_apply_layer_filtering_false_missing_slice_id(self):
-        """Test _should_apply_layer_filtering returns False when slice_id is missing"""
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": [0, 1],
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._should_apply_layer_filtering(form_data)
-
-        assert result is False
-
-    def test_should_apply_layer_filtering_false_missing_adhoc_filters(self):
-        """Test _should_apply_layer_filtering returns False when
-        adhoc_filters missing"""
-        form_data = {"slice_id": 123}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._should_apply_layer_filtering(form_data)
-
-        assert result is False
-
-    def test_should_apply_layer_filtering_false_no_layer_scoped_filters(self):
-        """Test _should_apply_layer_filtering returns False when no layer
-        scoped filters"""
-        form_data = {
-            "slice_id": 123,
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._should_apply_layer_filtering(form_data)
-
-        assert result is False
-
-    def test_has_layer_scoped_filters_true_with_dict(self):
-        """Test _has_layer_scoped_filters returns True when filter
-        has layerFilterScope"""
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": [0, 1],
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._has_layer_scoped_filters(form_data)
-
-        assert result is True
-
-    def test_has_layer_scoped_filters_true_with_non_none_value(self):
-        """Test _has_layer_scoped_filters returns True when layerFilterScope not None"""
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": [],
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._has_layer_scoped_filters(form_data)
-
-        assert result is True
-
-    def test_has_layer_scoped_filters_false_none_value(self):
-        """Test _has_layer_scoped_filters returns False when layerFilterScope is None"""
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": None,
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._has_layer_scoped_filters(form_data)
-
-        assert result is False
-
-    def test_has_layer_scoped_filters_false_no_scoped_filters(self):
-        """Test _has_layer_scoped_filters returns False when no filters have
-        layerFilterScope"""
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._has_layer_scoped_filters(form_data)
-
-        assert result is False
-
-    def test_apply_multilayer_filtering_filters_by_layer_scope(self):
-        """Test _apply_multilayer_filtering filters adhoc_filters by layer scope"""
-        form_data = {
-            "slice_id": 123,
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": [0, 1],
-                    "deck_slices": [123, 456],
-                },
-                {
-                    "clause": "WHERE",
-                    "subject": "col2",
-                    "operator": "!=",
-                    "comparator": "value2",
-                    "layerFilterScope": [1],
-                    "deck_slices": [123, 456],
-                },
-                {
-                    "clause": "WHERE",
-                    "subject": "col3",
-                    "operator": ">",
-                    "comparator": "value3",
-                    "layerFilterScope": [2],
-                    "deck_slices": [123, 456],
-                },
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._apply_multilayer_filtering(form_data)
-
-        assert len(result["adhoc_filters"]) == 1
-        assert result["adhoc_filters"][0]["subject"] == "col1"
-
-    def test_apply_multilayer_filtering_no_deck_slices(self):
-        """Test _apply_multilayer_filtering when no deck_slices in filters"""
-        form_data = {
-            "slice_id": 123,
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": [0],
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._apply_multilayer_filtering(form_data)
-
-        assert len(result["adhoc_filters"]) == 1
-        assert result["adhoc_filters"][0]["subject"] == "col1"
-
-    def test_apply_multilayer_filtering_slice_not_in_deck_slices(self):
-        """Test _apply_multilayer_filtering when slice_id not in deck_slices"""
-        form_data = {
-            "slice_id": 999,
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "layerFilterScope": [0],
-                    "deck_slices": [123, 456],
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._apply_multilayer_filtering(form_data)
-
-        # Should return original form_data unchanged when slice_id not in deck_slices
-        assert result == form_data
-
-    def test_get_deck_slices_from_filters_found(self):
-        """Test _get_deck_slices_from_filters when deck_slices found"""
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                    "deck_slices": [123, 456],
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._get_deck_slices_from_filters(form_data)
-
-        assert result == [123, 456]
-
-    def test_get_deck_slices_from_filters_not_found(self):
-        """Test _get_deck_slices_from_filters when no deck_slices found"""
-        form_data = {
-            "adhoc_filters": [
-                {
-                    "clause": "WHERE",
-                    "subject": "col1",
-                    "operator": "==",
-                    "comparator": "value1",
-                }
-            ],
-        }
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._get_deck_slices_from_filters(form_data)
-
-        assert result is None
-
-    def test_get_deck_slices_from_filters_empty_filters(self):
-        """Test _get_deck_slices_from_filters with empty adhoc_filters"""
-        form_data = {"adhoc_filters": []}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, form_data)
-
-        result = test_viz._get_deck_slices_from_filters(form_data)
-
-        assert result is None
-
-    def test_get_filter_layer_scope_dict(self):
-        """Test _get_filter_layer_scope with dict filter"""
-        filter_item = {"layerFilterScope": [0, 1]}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result == [0, 1]
-
-    def test_get_filter_layer_scope_dict_none(self):
-        """Test _get_filter_layer_scope with dict filter having None layerFilterScope"""
-        filter_item = {"layerFilterScope": None}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result is None
-
-    def test_get_filter_layer_scope_dict_missing_key(self):
-        """Test _get_filter_layer_scope with dict filter missing layerFilterScope key"""
-        filter_item = {"other_key": "value"}
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result is None
-
-    def test_get_filter_layer_scope_object_with_attribute(self):
-        """Test _get_filter_layer_scope with object having layerFilterScope attribute"""
-        filter_item = Mock()
-        filter_item.layerFilterScope = [0, 1]
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result == [0, 1]
-
-    def test_get_filter_layer_scope_object_without_attribute(self):
-        """Test _get_filter_layer_scope with object missing
-        layerFilterScope attribute"""
-        filter_item = Mock()
-        del filter_item.layerFilterScope
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result is None
-
-    def test_get_filter_layer_scope_non_dict_non_object(self):
-        """Test _get_filter_layer_scope with non-dict, non-object filter"""
-        filter_item = "string_filter"
-        datasource = self.get_datasource_mock()
-        test_viz = viz.BaseDeckGLViz(datasource, {})
-
-        result = test_viz._get_filter_layer_scope(filter_item)
-
-        assert result is None
-
-
-class TestTimeSeriesViz(SupersetTestCase):
-    def test_timeseries_unicode_data(self):
-        datasource = self.get_datasource_mock()
-        form_data = {"groupby": ["name"], "metrics": ["sum__payout"]}
-        raw = {}
-        raw["name"] = [
-            "Real Madrid C.F.πŸ‡ΊπŸ‡ΈπŸ‡¬πŸ‡§",
-            "Real Madrid C.F.πŸ‡ΊπŸ‡ΈπŸ‡¬πŸ‡§",
-            "Real Madrid Basket",
-            "Real Madrid Basket",
-        ]
-        raw["__timestamp"] = [
-            "2018-02-20T00:00:00",
-            "2018-03-09T00:00:00",
-            "2018-02-20T00:00:00",
-            "2018-03-09T00:00:00",
-        ]
-        raw["sum__payout"] = [2, 2, 4, 4]
-        df = pd.DataFrame(raw)
-
-        test_viz = viz.NVD3TimeSeriesViz(datasource, form_data)
-        viz_data = {}
-        viz_data = test_viz.get_data(df)
-        expected = [
-            {
-                "values": [
-                    {"y": 4, "x": "2018-02-20T00:00:00"},
-                    {"y": 4, "x": "2018-03-09T00:00:00"},
-                ],
-                "key": ("Real Madrid Basket",),
-            },
-            {
-                "values": [
-                    {"y": 2, "x": "2018-02-20T00:00:00"},
-                    {"y": 2, "x": "2018-03-09T00:00:00"},
-                ],
-                "key": ("Real Madrid C.F.\U0001f1fa\U0001f1f8\U0001f1ec\U0001f1e7",),
-            },
-        ]
-        assert expected == viz_data
-
-    def test_process_data_resample(self):
-        datasource = self.get_datasource_mock()
-
-        df = pd.DataFrame(
-            {
-                "__timestamp": pd.to_datetime(
-                    ["2019-01-01", "2019-01-02", "2019-01-05", "2019-01-07"]
-                ),
-                "y": [1.0, 2.0, 5.0, 7.0],
-            }
-        )
-
-        assert viz.NVD3TimeSeriesViz(
-            datasource,
-            {"metrics": ["y"], "resample_method": "sum", "resample_rule": "1D"},
-        ).process_data(df)["y"].tolist() == [1.0, 2.0, 0.0, 0.0, 5.0, 0.0, 7.0]
-
-        np.testing.assert_equal(
-            viz.NVD3TimeSeriesViz(
-                datasource,
-                {"metrics": ["y"], "resample_method": "asfreq", "resample_rule": "1D"},
-            )
-            .process_data(df)["y"]
-            .tolist(),
-            [1.0, 2.0, np.nan, np.nan, 5.0, np.nan, 7.0],
-        )
-
-    def test_apply_rolling(self):
-        datasource = self.get_datasource_mock()
-        df = pd.DataFrame(
-            index=pd.to_datetime(
-                ["2019-01-01", "2019-01-02", "2019-01-05", "2019-01-07"]
-            ),
-            data={"y": [1.0, 2.0, 3.0, 4.0]},
-        )
-        assert viz.NVD3TimeSeriesViz(
-            datasource,
-            {
-                "metrics": ["y"],
-                "rolling_type": "cumsum",
-                "rolling_periods": 0,
-                "min_periods": 0,
-            },
-        ).apply_rolling(df)["y"].tolist() == [1.0, 3.0, 6.0, 10.0]
-        assert viz.NVD3TimeSeriesViz(
-            datasource,
-            {
-                "metrics": ["y"],
-                "rolling_type": "sum",
-                "rolling_periods": 2,
-                "min_periods": 0,
-            },
-        ).apply_rolling(df)["y"].tolist() == [1.0, 3.0, 5.0, 7.0]
-        assert viz.NVD3TimeSeriesViz(
-            datasource,
-            {
-                "metrics": ["y"],
-                "rolling_type": "mean",
-                "rolling_periods": 10,
-                "min_periods": 0,
-            },
-        ).apply_rolling(df)["y"].tolist() == [1.0, 1.5, 2.0, 2.5]
-
-    def test_apply_rolling_without_data(self):
-        datasource = self.get_datasource_mock()
-        df = pd.DataFrame(
-            index=pd.to_datetime(
-                ["2019-01-01", "2019-01-02", "2019-01-05", "2019-01-07"]
-            ),
-            data={"y": [1.0, 2.0, 3.0, 4.0]},
-        )
-        test_viz = viz.NVD3TimeSeriesViz(
-            datasource,
-            {
-                "metrics": ["y"],
-                "rolling_type": "cumsum",
-                "rolling_periods": 4,
-                "min_periods": 4,
-            },
-        )
-        with pytest.raises(QueryObjectValidationError):
-            test_viz.apply_rolling(df)
diff --git a/tests/unit_tests/async_events/async_query_manager_tests.py b/tests/unit_tests/async_events/async_query_manager_tests.py
index 534d402e48a..bd1998e5f4b 100644
--- a/tests/unit_tests/async_events/async_query_manager_tests.py
+++ b/tests/unit_tests/async_events/async_query_manager_tests.py
@@ -322,54 +322,3 @@ def test_validate_session_guest_user_creates_valid_token(async_query_manager):
         mock_request.cookies = {JWT_TOKEN_COOKIE_NAME: token}
         channel = async_query_manager.parse_channel_id_from_request(mock_request)
         assert channel  # valid UUID string
-
-
-@mark.parametrize(
-    "cache_type, cache_backend",
-    [
-        ("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)),
-        ("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)),
-    ],
-)
-@mock.patch("superset.is_feature_enabled")
-def test_submit_explore_json_job_as_guest_user(
-    is_feature_enabled_mock, async_query_manager, cache_type, cache_backend
-):
-    is_feature_enabled_mock.return_value = True
-    set_current_as_guest_user()
-
-    # Mock the get_cache_backend method to return the current cache backend
-    async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend)
-
-    job_mock = Mock()
-    async_query_manager._load_explore_json_into_cache_job = job_mock
-    job_meta = async_query_manager.submit_explore_json_job(
-        channel_id="test_channel_id",
-        form_data={},
-        response_type="json",
-    )
-
-    job_mock.delay.assert_called_once_with(
-        {
-            "channel_id": "test_channel_id",
-            "errors": [],
-            "guest_token": {
-                "user": {},
-                "resources": [{"type": "dashboard", "id": "some-uuid"}],
-                "rls_rules": [{"clause": '"STATEID" = 3'}],
-                "iat": 1700000000.0,
-                "exp": 1700000300.0,
-                "aud": "http://0.0.0.0:8080/",
-                "type": "guest",
-            },
-            "job_id": ANY,
-            "result_url": None,
-            "status": "pending",
-            "user_id": None,
-        },
-        {},
-        "json",
-        False,
-    )
-
-    assert "guest_token" not in job_meta
diff --git a/tests/unit_tests/commands/chart/warm_up_cache_test.py b/tests/unit_tests/commands/chart/warm_up_cache_test.py
index f178ab85e75..5f97e9b3fa2 100644
--- a/tests/unit_tests/commands/chart/warm_up_cache_test.py
+++ b/tests/unit_tests/commands/chart/warm_up_cache_test.py
@@ -57,33 +57,29 @@ def test_applies_dashboard_filters_to_non_legacy_chart(
 
     # Mock dependencies
     with patch.object(chart, "get_query_context", return_value=mock_qc):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "echarts_timeseries_bar"}],
-        ):
-            mock_chart_data_command.return_value.run.return_value = {
-                "queries": [{"error": None, "status": "success"}]
-            }
+        mock_chart_data_command.return_value.run.return_value = {
+            "queries": [{"error": None, "status": "success"}]
+        }
 
-            # Execute with dashboard_id
-            result = ChartWarmUpCacheCommand(chart, 42, None).run()
+        # Execute with dashboard_id
+        result = ChartWarmUpCacheCommand(chart, 42, None).run()
 
-            # VALIDATE: Filters were added to query.filter
-            assert len(mock_query.filter) == 1, "Filter should be added to query"
-            assert mock_query.filter[0] == {
-                "col": "country",
-                "op": "in",
-                "val": ["USA", "France"],
-            }, "Filter content should match dashboard filter"
+        # VALIDATE: Filters were added to query.filter
+        assert len(mock_query.filter) == 1, "Filter should be added to query"
+        assert mock_query.filter[0] == {
+            "col": "country",
+            "op": "in",
+            "val": ["USA", "France"],
+        }, "Filter content should match dashboard filter"
 
-            # VALIDATE: get_dashboard_extra_filters was called correctly
-            mock_get_dashboard_filters.assert_called_once_with(123, 42)
+        # VALIDATE: get_dashboard_extra_filters was called correctly
+        mock_get_dashboard_filters.assert_called_once_with(123, 42)
 
-            # VALIDATE: force flag was set
-            assert mock_qc.force is True, "force should be set to True"
+        # VALIDATE: force flag was set
+        assert mock_qc.force is True, "force should be set to True"
 
-            # VALIDATE: Result is correct
-            assert result["chart_id"] == 123
+        # VALIDATE: Result is correct
+        assert result["chart_id"] == 123
 
 
 @patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
@@ -104,22 +100,18 @@ def test_no_filters_applied_without_dashboard_id(mock_chart_data_command):
     mock_qc.queries = [mock_query]
 
     with patch.object(chart, "get_query_context", return_value=mock_qc):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "big_number"}],
-        ):
-            mock_chart_data_command.return_value.run.return_value = {
-                "queries": [{"error": None, "status": "success"}]
-            }
+        mock_chart_data_command.return_value.run.return_value = {
+            "queries": [{"error": None, "status": "success"}]
+        }
 
-            # Execute WITHOUT dashboard_id
-            ChartWarmUpCacheCommand(chart, None, None).run()
+        # Execute WITHOUT dashboard_id
+        ChartWarmUpCacheCommand(chart, None, None).run()
 
-            # VALIDATE: Filter list unchanged (no dashboard filters added)
-            assert len(mock_query.filter) == 1, "Filter count should remain the same"
-            assert mock_query.filter == [
-                {"col": "existing", "op": "==", "val": "filter"}
-            ], "Existing filters should be unchanged"
+        # VALIDATE: Filter list unchanged (no dashboard filters added)
+        assert len(mock_query.filter) == 1, "Filter count should remain the same"
+        assert mock_query.filter == [
+            {"col": "existing", "op": "==", "val": "filter"}
+        ], "Existing filters should be unchanged"
 
 
 @patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters")
@@ -142,24 +134,20 @@ def test_extra_filters_parameter_takes_precedence(
     mock_qc.queries = [mock_query]
 
     with patch.object(chart, "get_query_context", return_value=mock_qc):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "pie"}],
-        ):
-            mock_chart_data_command.return_value.run.return_value = {
-                "queries": [{"error": None, "status": "success"}]
-            }
+        mock_chart_data_command.return_value.run.return_value = {
+            "queries": [{"error": None, "status": "success"}]
+        }
 
-            # Execute with extra_filters parameter
-            extra_filters_json = '[{"col": "state", "op": "==", "val": "CA"}]'
-            ChartWarmUpCacheCommand(chart, 42, extra_filters_json).run()
+        # Execute with extra_filters parameter
+        extra_filters_json = '[{"col": "state", "op": "==", "val": "CA"}]'
+        ChartWarmUpCacheCommand(chart, 42, extra_filters_json).run()
 
-            # VALIDATE: get_dashboard_extra_filters should NOT be called
-            mock_get_dashboard_filters.assert_not_called()
+        # VALIDATE: get_dashboard_extra_filters should NOT be called
+        mock_get_dashboard_filters.assert_not_called()
 
-            # VALIDATE: extra_filters were parsed and applied
-            assert len(mock_query.filter) == 1
-            assert mock_query.filter[0] == {"col": "state", "op": "==", "val": "CA"}
+        # VALIDATE: extra_filters were parsed and applied
+        assert len(mock_query.filter) == 1
+        assert mock_query.filter[0] == {"col": "state", "op": "==", "val": "CA"}
 
 
 @patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters")
@@ -189,24 +177,20 @@ def test_handles_multiple_queries_in_query_context(
     mock_qc.queries = [mock_query1, mock_query2]
 
     with patch.object(chart, "get_query_context", return_value=mock_qc):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "heatmap_v2"}],
-        ):
-            mock_chart_data_command.return_value.run.return_value = {
-                "queries": [
-                    {"error": None, "status": "success"},
-                    {"error": None, "status": "success"},
-                ]
-            }
-
-            ChartWarmUpCacheCommand(chart, 42, None).run()
-
-            # VALIDATE: Filters added to BOTH queries
-            assert len(mock_query1.filter) == 1, "Filter should be added to query 1"
-            assert len(mock_query2.filter) == 1, "Filter should be added to query 2"
-            assert mock_query1.filter[0]["col"] == "country"
-            assert mock_query2.filter[0]["col"] == "country"
+        mock_chart_data_command.return_value.run.return_value = {
+            "queries": [
+                {"error": None, "status": "success"},
+                {"error": None, "status": "success"},
+            ]
+        }
+
+        ChartWarmUpCacheCommand(chart, 42, None).run()
+
+        # VALIDATE: Filters added to BOTH queries
+        assert len(mock_query1.filter) == 1, "Filter should be added to query 1"
+        assert len(mock_query2.filter) == 1, "Filter should be added to query 2"
+        assert mock_query1.filter[0]["col"] == "country"
+        assert mock_query2.filter[0]["col"] == "country"
 
 
 @patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters")
@@ -232,23 +216,19 @@ def test_handles_empty_dashboard_filters(
     mock_qc.queries = [mock_query]
 
     with patch.object(chart, "get_query_context", return_value=mock_qc):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "echarts_area"}],
-        ):
-            mock_chart_data_command.return_value.run.return_value = {
-                "queries": [{"error": None, "status": "success"}]
-            }
+        mock_chart_data_command.return_value.run.return_value = {
+            "queries": [{"error": None, "status": "success"}]
+        }
 
-            ChartWarmUpCacheCommand(chart, 42, None).run()
+        ChartWarmUpCacheCommand(chart, 42, None).run()
 
-            # VALIDATE: No filters added (empty list case)
-            assert len(mock_query.filter) == 0, (
-                "No filters should be added when dashboard has no filters"
-            )
-            assert mock_get_dashboard_filters.called, (
-                "Should still call get_dashboard_extra_filters"
-            )
+        # VALIDATE: No filters added (empty list case)
+        assert len(mock_query.filter) == 0, (
+            "No filters should be added when dashboard has no filters"
+        )
+        assert mock_get_dashboard_filters.called, (
+            "Should still call get_dashboard_extra_filters"
+        )
 
 
 @patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
@@ -271,22 +251,18 @@ def test_invalid_json_in_extra_filters_raises_error(mock_chart_data_command):
     mock_qc.queries = [mock_query]
 
     with patch.object(chart, "get_query_context", return_value=mock_qc):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "pie"}],
-        ):
-            result = ChartWarmUpCacheCommand(chart, 42, invalid_json).run()
-
-            assert result["viz_error"] is not None, "Should return an error"
-            assert result["chart_id"] == 128
-            # JSONDecodeError messages vary across Python versions
-            error_str = str(result["viz_error"]).lower()
-            assert (
-                "json" in error_str
-                or "decode" in error_str
-                or "expecting" in error_str
-                or "delimiter" in error_str
-            ), f"Error should be a JSON decode issue: {result['viz_error']}"
+        result = ChartWarmUpCacheCommand(chart, 42, invalid_json).run()
+
+        assert result["viz_error"] is not None, "Should return an error"
+        assert result["chart_id"] == 128
+        # JSONDecodeError messages vary across Python versions
+        error_str = str(result["viz_error"]).lower()
+        assert (
+            "json" in error_str
+            or "decode" in error_str
+            or "expecting" in error_str
+            or "delimiter" in error_str
+        ), f"Error should be a JSON decode issue: {result['viz_error']}"
 
 
 @patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
@@ -302,135 +278,17 @@ def test_none_query_context_raises_chart_invalid_error(mock_chart_data_command):
 
     # Mock get_query_context to return None (chart has no query_context)
     with patch.object(chart, "get_query_context", return_value=None):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "echarts_timeseries"}],
-        ):
-            result = ChartWarmUpCacheCommand(chart, None, None).run()
-
-            assert result["viz_error"] is not None, "Should return an error"
-            assert result["chart_id"] == 129
-            error_str = str(result["viz_error"]).lower()
-            assert "query context" in error_str, (
-                f"Error should mention query context: {result['viz_error']}"
-            )
-            assert "not exist" in error_str, (
-                f"Error should mention not exist: {result['viz_error']}"
-            )
-
-
-@patch("superset.commands.chart.warm_up_cache.viz_types", ["table"])
-def test_legacy_chart_without_datasource_raises_error():
-    """Verify that legacy chart without datasource raises ChartInvalidError"""
-    chart = Slice(
-        id=130,
-        slice_name="Legacy Chart",
-        viz_type="table",
-        datasource_id=None,
-        datasource_type=None,
-    )
-
-    with patch.object(
-        type(chart), "datasource", new_callable=lambda: property(lambda self: None)
-    ):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "table"}],
-        ):
-            result = ChartWarmUpCacheCommand(chart, None, None).run()
-
-            assert result["viz_error"] is not None, "Should return an error"
-            assert result["chart_id"] == 130
-            error_str = str(result["viz_error"]).lower()
-            assert "datasource" in error_str, (
-                f"Error should mention datasource: {result['viz_error']}"
-            )
-            assert "not exist" in error_str, (
-                f"Error should mention not exist: {result['viz_error']}"
-            )
-
-
-@patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters")
-@patch("superset.commands.chart.warm_up_cache.get_viz")
-@patch("superset.commands.chart.warm_up_cache.viz_types", ["table"])
-@patch("superset.commands.chart.warm_up_cache.g")
-def test_legacy_chart_warm_up_with_dashboard(
-    mock_g, mock_get_viz, mock_get_dashboard_filters
-):
-    """Test successful legacy chart warm-up with dashboard filters"""
-    mock_get_dashboard_filters.return_value = [
-        {"col": "country", "op": "==", "val": "USA"}
-    ]
-
-    chart = Slice(
-        id=131,
-        slice_name="Legacy Table",
-        viz_type="table",
-        datasource_id=1,
-        datasource_type="table",
-    )
-
-    mock_datasource = Mock()
-    mock_datasource.type = "table"
-    mock_datasource.id = 1
-
-    mock_viz = Mock()
-    mock_viz.get_payload.return_value = {"errors": None, "status": "success"}
-    mock_get_viz.return_value = mock_viz
-
-    with patch.object(
-        type(chart),
-        "datasource",
-        new_callable=lambda: property(lambda self: mock_datasource),
-    ):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "table"}],
-        ):
-            result = ChartWarmUpCacheCommand(chart, 42, None).run()
-
-            assert result["chart_id"] == 131
-            assert result["viz_error"] is None
-            assert result["viz_status"] == "success"
-            assert hasattr(mock_g, "form_data") is False
-            mock_get_dashboard_filters.assert_called_once_with(131, 42)
-
-
-@patch("superset.commands.chart.warm_up_cache.get_viz")
-@patch("superset.commands.chart.warm_up_cache.viz_types", ["table"])
-@patch("superset.commands.chart.warm_up_cache.g")
-def test_legacy_chart_warm_up_without_dashboard(mock_g, mock_get_viz):
-    """Test successful legacy chart warm-up without dashboard"""
-    chart = Slice(
-        id=134,
-        slice_name="Legacy Table",
-        viz_type="table",
-        datasource_id=1,
-        datasource_type="table",
-    )
-
-    mock_datasource = Mock()
-    mock_datasource.type = "table"
-    mock_datasource.id = 1
-
-    mock_viz = Mock()
-    mock_viz.get_payload.return_value = {"errors": None, "status": "success"}
-    mock_get_viz.return_value = mock_viz
-
-    with patch.object(
-        type(chart),
-        "datasource",
-        new_callable=lambda: property(lambda self: mock_datasource),
-    ):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "table"}],
-        ):
-            result = ChartWarmUpCacheCommand(chart, None, None).run()
+        result = ChartWarmUpCacheCommand(chart, None, None).run()
 
-            assert result["chart_id"] == 134
-            assert result["viz_error"] is None
-            assert result["viz_status"] == "success"
+        assert result["viz_error"] is not None, "Should return an error"
+        assert result["chart_id"] == 129
+        error_str = str(result["viz_error"]).lower()
+        assert "query context" in error_str, (
+            f"Error should mention query context: {result['viz_error']}"
+        )
+        assert "not exist" in error_str, (
+            f"Error should mention not exist: {result['viz_error']}"
+        )
 
 
 @patch("superset.commands.chart.warm_up_cache.ChartDataCommand")
@@ -450,22 +308,18 @@ def test_non_legacy_chart_returns_first_error(mock_chart_data_command):
     mock_qc.queries = [mock_query]
 
     with patch.object(chart, "get_query_context", return_value=mock_qc):
-        with patch(
-            "superset.commands.chart.warm_up_cache.get_form_data",
-            return_value=[{"viz_type": "echarts_timeseries"}],
-        ):
-            mock_chart_data_command.return_value.run.return_value = {
-                "queries": [
-                    {"error": "Database connection failed", "status": "failed"},
-                    {"error": None, "status": "success"},
-                ]
-            }
-
-            result = ChartWarmUpCacheCommand(chart, None, None).run()
-
-            assert result["chart_id"] == 132
-            assert result["viz_error"] == "Database connection failed"
-            assert result["viz_status"] == "failed"
+        mock_chart_data_command.return_value.run.return_value = {
+            "queries": [
+                {"error": "Database connection failed", "status": "failed"},
+                {"error": None, "status": "success"},
+            ]
+        }
+
+        result = ChartWarmUpCacheCommand(chart, None, None).run()
+
+        assert result["chart_id"] == 132
+        assert result["viz_error"] == "Database connection failed"
+        assert result["viz_status"] == "failed"
 
 
 @patch("superset.commands.chart.warm_up_cache.db")
diff --git a/tests/unit_tests/middleware/test_legacy_prefix_redirect.py b/tests/unit_tests/middleware/test_legacy_prefix_redirect.py
index dfb12933fce..f00e9aedbcf 100644
--- a/tests/unit_tests/middleware/test_legacy_prefix_redirect.py
+++ b/tests/unit_tests/middleware/test_legacy_prefix_redirect.py
@@ -84,9 +84,6 @@ _GET_REDIRECT_ROWS: list[tuple[str, str]] = [
     ("/superset/explore/", "/explore/"),
     ("/superset/explore/table/5/", "/explore/table/5/"),
     ("/superset/explore/p/key123/", "/explore/p/key123/"),
-    ("/superset/explore_json/", "/explore_json/"),
-    ("/superset/explore_json/table/5/", "/explore_json/table/5/"),
-    ("/superset/explore_json/data/cache_abc/", "/explore_json/data/cache_abc/"),
     ("/superset/tags/", "/tags/"),
     ("/superset/all_entities/", "/all_entities/"),
 ]
@@ -95,8 +92,6 @@ _GET_REDIRECT_ROWS: list[tuple[str, str]] = [
 _POST_BODY_PRESERVED_ROWS: list[tuple[str, str]] = [
     ("/superset/explore/", "/explore/"),
     ("/superset/explore/table/5/", "/explore/table/5/"),
-    ("/superset/explore_json/", "/explore_json/"),
-    ("/superset/explore_json/table/5/", "/explore_json/table/5/"),
     ("/superset/log/", "/log/"),
 ]
 
@@ -112,7 +107,6 @@ _POST_410_ROWS: list[str] = [
     "/superset/file-handler",
     "/superset/sqllab/history/",
     "/superset/explore/p/key123/",
-    "/superset/explore_json/data/cache_abc/",
     "/superset/tags/",
     "/superset/all_entities/",
 ]
@@ -513,8 +507,6 @@ _EXPECTED_KEYSET: frozenset[str] = frozenset(
         "/file-handler",
         "/log/",
         "/sqllab/history/",
-        "/explore_json/",
-        "/explore_json/data/",
         "/explore/",
         "/explore/p/",
         "/tags/",
diff --git a/tests/unit_tests/tasks/test_async_queries.py b/tests/unit_tests/tasks/test_async_queries.py
index d2e73466e07..77ad6b6fa7f 100644
--- a/tests/unit_tests/tasks/test_async_queries.py
+++ b/tests/unit_tests/tasks/test_async_queries.py
@@ -22,10 +22,8 @@ from flask_babel import lazy_gettext as _
 from superset.commands.chart.exceptions import ChartDataQueryFailedError
 from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
 from superset.exceptions import (
-    OAuth2RedirectError,
     SupersetErrorException,
     SupersetErrorsException,
-    SupersetVizException,
 )
 
 
@@ -154,170 +152,3 @@ def test_load_chart_data_into_cache_with_superset_errors_exception(
     assert errors[1]["message"] == "Table not found"
     assert errors[1]["error_type"] == SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR
     assert errors[1]["level"] == ErrorLevel.WARNING
-
-
-@mock.patch("superset.tasks.async_queries.security_manager")
-@mock.patch("superset.tasks.async_queries.async_query_manager")
-@mock.patch("superset.tasks.async_queries.get_viz")
-@mock.patch("superset.tasks.async_queries.get_datasource_info")
-def test_load_explore_json_into_cache_preserves_oauth2_redirect_error(
-    mock_get_datasource_info,
-    mock_get_viz,
-    mock_async_query_manager,
-    mock_security_manager,
-):
-    """
-    OAuth2RedirectError raised by ``viz_obj.get_payload`` must reach the async
-    job's errors list as a structured SIP-40 envelope so the frontend can
-    render the OAuth2 banner identically to the sync legacy path.
-    """
-    from superset.tasks.async_queries import load_explore_json_into_cache
-
-    job_metadata = {"user_id": 1}
-    form_data: dict = {}
-
-    mock_get_datasource_info.return_value = (1, "table")
-    mock_security_manager.get_user_by_id.return_value = mock.MagicMock()
-    mock_async_query_manager.STATUS_ERROR = "error"
-
-    viz_obj = mock.MagicMock()
-    viz_obj.get_payload.side_effect = OAuth2RedirectError(
-        url="https://accounts.example.com/o/oauth2/v2/auth?...",
-        tab_id="tab-123",
-        redirect_uri="https://superset.example.com/oauth2/redirect",
-    )
-    mock_get_viz.return_value = viz_obj
-
-    with pytest.raises(OAuth2RedirectError):
-        load_explore_json_into_cache(job_metadata, form_data)
-
-    call_args = mock_async_query_manager.update_job.call_args
-    assert call_args[0] == (job_metadata, "error")
-    errors = call_args[1]["errors"]
-    assert len(errors) == 1
-    assert errors[0]["error_type"] == SupersetErrorType.OAUTH2_REDIRECT
-    assert errors[0]["extra"] == {
-        "url": "https://accounts.example.com/o/oauth2/v2/auth?...",
-        "tab_id": "tab-123",
-        "redirect_uri": "https://superset.example.com/oauth2/redirect",
-    }
-
-
-@mock.patch("superset.tasks.async_queries.security_manager")
-@mock.patch("superset.tasks.async_queries.async_query_manager")
-@mock.patch("superset.tasks.async_queries.get_viz")
-@mock.patch("superset.tasks.async_queries.get_datasource_info")
-def test_load_explore_json_into_cache_preserves_superset_errors_exception(
-    mock_get_datasource_info,
-    mock_get_viz,
-    mock_async_query_manager,
-    mock_security_manager,
-):
-    """SupersetErrorsException must be preserved as a list of SIP-40 dicts."""
-    from superset.tasks.async_queries import load_explore_json_into_cache
-
-    job_metadata = {"user_id": 1}
-    form_data: dict = {}
-
-    mock_get_datasource_info.return_value = (1, "table")
-    mock_security_manager.get_user_by_id.return_value = mock.MagicMock()
-    mock_async_query_manager.STATUS_ERROR = "error"
-
-    viz_obj = mock.MagicMock()
-    viz_obj.get_payload.side_effect = SupersetErrorsException(
-        [
-            SupersetError(
-                message="Column not found",
-                error_type=SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR,
-                level=ErrorLevel.ERROR,
-            ),
-            SupersetError(
-                message="Table not found",
-                error_type=SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
-                level=ErrorLevel.WARNING,
-            ),
-        ]
-    )
-    mock_get_viz.return_value = viz_obj
-
-    with pytest.raises(SupersetErrorsException):
-        load_explore_json_into_cache(job_metadata, form_data)
-
-    errors = mock_async_query_manager.update_job.call_args[1]["errors"]
-    assert len(errors) == 2
-    assert errors[0]["error_type"] == SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR
-    assert errors[1]["error_type"] == SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR
-
-
-@mock.patch("superset.tasks.async_queries.security_manager")
-@mock.patch("superset.tasks.async_queries.async_query_manager")
-@mock.patch("superset.tasks.async_queries.get_viz")
-@mock.patch("superset.tasks.async_queries.get_datasource_info")
-def test_load_explore_json_into_cache_preserves_superset_viz_exception(
-    mock_get_datasource_info,
-    mock_get_viz,
-    mock_async_query_manager,
-    mock_security_manager,
-):
-    """
-    Test that SupersetVizException passes ``ex.errors`` straight through.
-    """
-    from superset.tasks.async_queries import load_explore_json_into_cache
-
-    job_metadata = {"user_id": 1}
-    form_data: dict = {}
-
-    mock_get_datasource_info.return_value = (1, "table")
-    mock_security_manager.get_user_by_id.return_value = mock.MagicMock()
-    mock_async_query_manager.STATUS_ERROR = "error"
-
-    payload_errors = [
-        {
-            "message": "Bad column",
-            "error_type": SupersetErrorType.VIZ_GET_DF_ERROR,
-            "level": ErrorLevel.ERROR,
-        }
-    ]
-    viz_obj = mock.MagicMock()
-    viz_obj.get_payload.return_value = {"errors": payload_errors}
-    viz_obj.has_error.return_value = True
-    mock_get_viz.return_value = viz_obj
-
-    with pytest.raises(SupersetVizException):
-        load_explore_json_into_cache(job_metadata, form_data)
-
-    errors = mock_async_query_manager.update_job.call_args[1]["errors"]
-    assert errors == payload_errors
-
-
-@mock.patch("superset.tasks.async_queries.security_manager")
-@mock.patch("superset.tasks.async_queries.async_query_manager")
-@mock.patch("superset.tasks.async_queries.get_viz")
-@mock.patch("superset.tasks.async_queries.get_datasource_info")
-def test_load_explore_json_into_cache_falls_back_to_string_for_generic_exception(
-    mock_get_datasource_info,
-    mock_get_viz,
-    mock_async_query_manager,
-    mock_security_manager,
-):
-    """
-    Test that Non-Superset exception are passed as plain-string error.
-    """
-    from superset.tasks.async_queries import load_explore_json_into_cache
-
-    job_metadata = {"user_id": 1}
-    form_data: dict = {}
-
-    mock_get_datasource_info.return_value = (1, "table")
-    mock_security_manager.get_user_by_id.return_value = mock.MagicMock()
-    mock_async_query_manager.STATUS_ERROR = "error"
-
-    viz_obj = mock.MagicMock()
-    viz_obj.get_payload.side_effect = RuntimeError("boom")
-    mock_get_viz.return_value = viz_obj
-
-    with pytest.raises(RuntimeError):
-        load_explore_json_into_cache(job_metadata, form_data)
-
-    errors = mock_async_query_manager.update_job.call_args[1]["errors"]
-    assert errors == ["boom"]
diff --git a/tests/unit_tests/test_subdirectory_url_for.py b/tests/unit_tests/test_subdirectory_url_for.py
index 45fecb0a5b8..e09042a6451 100644
--- a/tests/unit_tests/test_subdirectory_url_for.py
+++ b/tests/unit_tests/test_subdirectory_url_for.py
@@ -168,19 +168,6 @@ def test_dashboard_model_url_has_no_route_prefix() -> None:
     assert Dashboard.get_url(11, "births") == "/dashboard/births/"
 
 
-def test_slice_explore_json_url_has_no_route_prefix(app_context: None) -> None:
-    """`Slice.explore_json_url` previously baked `/superset/explore_json` into
-    the path. After `Superset.route_base = ""` the rule is `/explore_json/`,
-    so the literal pointed at a non-existent route. Pin the prefix-free shape
-    so downstream `ensureAppRoot` callers prepend the application root once.
-    """
-    from superset.models.slice import Slice
-
-    slc = Slice(id=42)
-    assert slc.explore_json_url.startswith("/explore_json/")
-    assert "/superset/explore_json" not in slc.explore_json_url
-
-
 def test_database_sql_url_resolves_to_live_sqllab_route() -> None:
     """`Database.sql_url` previously returned `/superset/sql/<id>/` β€” a route
     that was removed when SQL Lab moved to its own blueprint at `/sqllab/`.
diff --git a/tests/unit_tests/test_viz_cache_key.py b/tests/unit_tests/test_viz_cache_key.py
deleted file mode 100644
index d6eb614265c..00000000000
--- a/tests/unit_tests/test_viz_cache_key.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# 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.
-"""
-Behavioral tests for ``viz.BaseViz.cache_key`` covering per-user cache-key
-inclusion.
-"""
-
-from typing import Any
-from unittest.mock import patch
-
-from flask_appbuilder.security.sqla.models import User
-
-from superset import viz
-from superset.connectors.sqla.models import SqlaTable
-from superset.models.core import Database
-from superset.utils.core import override_user
-
-QUERY_OBJ: dict[str, Any] = {"row_limit": 100, "from_dttm": None, "to_dttm": None}
-
-
-def _viz_for(database: Database) -> viz.BaseViz:
-    datasource = SqlaTable(
-        table_name="t",
-        columns=[],
-        metrics=[],
-        main_dttm_col=None,
-        database=database,
-    )
-    return viz.BaseViz(datasource=datasource, form_data={"viz_type": "table"})
-
-
-def test_no_per_user_opt_in_keys_match_across_users():
-    """
-    Without any per-user caching opt-in, two different users on the same
-    database/query must produce the *same* cache key (regression guard β€” we
-    must not accidentally make every cache key per-user).
-    """
-    database = Database(database_name="d", sqlalchemy_uri="sqlite://")
-    obj = _viz_for(database)
-
-    with override_user(User(username="alice")):
-        key_a = obj.cache_key(QUERY_OBJ)
-    with override_user(User(username="bob")):
-        key_b = obj.cache_key(QUERY_OBJ)
-
-    assert key_a == key_b
-
-
-def test_per_user_caching_in_extra_yields_distinct_keys_per_user():
-    """
-    With ``per_user_caching: true`` set on the database, two different users
-    must produce *different* cache keys for the same query.
-    """
-    database = Database(
-        database_name="d",
-        sqlalchemy_uri="sqlite://",
-        extra='{"per_user_caching": true}',
-    )
-    obj = _viz_for(database)
-
-    with override_user(User(username="alice")):
-        key_a = obj.cache_key(QUERY_OBJ)
-    with override_user(User(username="bob")):
-        key_b = obj.cache_key(QUERY_OBJ)
-
-    assert key_a != key_b
-
-
-def test_same_user_same_query_idempotent():
-    database = Database(
-        database_name="d",
-        sqlalchemy_uri="sqlite://",
-        extra='{"per_user_caching": true}',
-    )
-    obj = _viz_for(database)
-
-    with override_user(User(username="alice")):
-        assert obj.cache_key(QUERY_OBJ) == obj.cache_key(QUERY_OBJ)
-
-
-@patch("superset.utils.cache_keys.feature_flag_manager")
-def test_cache_query_by_user_flag_yields_distinct_keys(feature_flag_mock):
-    """
-    Global ``CACHE_QUERY_BY_USER`` flag also reaches the legacy viz path.
-    """
-    feature_flag_mock.is_feature_enabled.side_effect = (
-        lambda feature=None: feature == "CACHE_QUERY_BY_USER"
-    )
-    database = Database(database_name="d", sqlalchemy_uri="sqlite://")
-    obj = _viz_for(database)
-
-    with override_user(User(username="alice")):
-        key_a = obj.cache_key(QUERY_OBJ)
-    with override_user(User(username="bob")):
-        key_b = obj.cache_key(QUERY_OBJ)
-
-    assert key_a != key_b
diff --git a/tests/unit_tests/test_viz_get_df_payload.py b/tests/unit_tests/test_viz_get_df_payload.py
deleted file mode 100644
index 96ae6c5e0d8..00000000000
--- a/tests/unit_tests/test_viz_get_df_payload.py
+++ /dev/null
@@ -1,244 +0,0 @@
-# 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.
-from typing import Any
-from unittest.mock import patch
-
-import pytest
-from flask import current_app
-
-from superset import viz
-from superset.common.db_query_status import QueryStatus
-from superset.connectors.sqla.models import SqlaTable
-from superset.errors import SupersetErrorType
-from superset.exceptions import (
-    OAuth2RedirectError,
-    QueryObjectValidationError,
-)
-from superset.models.core import Database
-
-QUERY_OBJ: dict[str, Any] = {"row_limit": 100, "from_dttm": None, "to_dttm": None}
-
-
-def _viz() -> viz.BaseViz:
-    database = Database(database_name="d", sqlalchemy_uri="sqlite://")
-    datasource = SqlaTable(
-        table_name="t",
-        columns=[],
-        metrics=[],
-        main_dttm_col=None,
-        database=database,
-    )
-    # ``force=True`` skips the data cache lookup branch so ``get_df`` is always
-    # invoked, which is what we want to assert error-handling against.
-    return viz.BaseViz(
-        datasource=datasource,
-        form_data={"viz_type": "table"},
-        force=True,
-    )
-
-
-def _timeseries_viz(form_data: dict[str, Any]) -> viz.NVD3TimeSeriesViz:
-    database = Database(database_name="d", sqlalchemy_uri="sqlite://")
-    datasource = SqlaTable(
-        table_name="t",
-        columns=[],
-        metrics=[],
-        main_dttm_col=None,
-        database=database,
-    )
-    base_form_data: dict[str, Any] = {"viz_type": "line", "metrics": ["value"]}
-    base_form_data.update(form_data)
-    return viz.NVD3TimeSeriesViz(
-        datasource=datasource,
-        form_data=base_form_data,
-        force=True,
-    )
-
-
-def _resample_df() -> Any:
-    import pandas as pd
-
-    from superset.utils.core import DTTM_ALIAS
-
-    return pd.DataFrame(
-        {
-            DTTM_ALIAS: pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]),
-            "value": [1, 2, 3],
-        }
-    )
-
-
-def test_process_data_rejects_unknown_resample_method() -> None:
-    """
-    A resample method outside the allowlist must raise
-    ``QueryObjectValidationError`` before any dynamic dispatch happens.
-    """
-    obj = _timeseries_viz({"resample_rule": "1D", "resample_method": "__class__"})
-
-    with pytest.raises(QueryObjectValidationError):
-        obj.process_data(_resample_df())
-
-
-def test_process_data_accepts_allowlisted_resample_method() -> None:
-    """
-    A valid resample method (e.g. ``mean``) is applied successfully.
-    """
-    obj = _timeseries_viz({"resample_rule": "1D", "resample_method": "mean"})
-
-    result = obj.process_data(_resample_df())
-
-    assert result is not None
-    assert not result.empty
-
-
-def test_run_extra_queries_rejects_excessive_time_compare() -> None:
-    """
-    Requesting more time-shift comparisons than ``VIZ_TIME_COMPARE_MAX`` must
-    raise ``QueryObjectValidationError`` before any sub-queries are issued.
-    """
-    max_compare = current_app.config["VIZ_TIME_COMPARE_MAX"]
-    obj = _timeseries_viz(
-        {"time_compare": [f"{i} days ago" for i in range(1, max_compare + 2)]}
-    )
-
-    with pytest.raises(QueryObjectValidationError):
-        obj.run_extra_queries()
-
-
-def test_deck_multi_rejects_excessive_sub_slices() -> None:
-    """
-    Requesting more deck.gl sub-slices than ``DECK_MULTI_MAX_SLICES`` must raise
-    ``QueryObjectValidationError`` before any sub-query is issued.
-    """
-    database = Database(database_name="d", sqlalchemy_uri="sqlite://")
-    datasource = SqlaTable(
-        table_name="t",
-        columns=[],
-        metrics=[],
-        main_dttm_col=None,
-        database=database,
-    )
-    max_slices = current_app.config["DECK_MULTI_MAX_SLICES"]
-    obj = viz.DeckGLMultiLayer(
-        datasource=datasource,
-        form_data={
-            "viz_type": "deck_multi",
-            "deck_slices": list(range(max_slices + 1)),
-        },
-        force=True,
-    )
-
-    with pytest.raises(QueryObjectValidationError):
-        obj.get_data(_resample_df())
-
-
-def test_get_df_payload_propagates_oauth2_redirect_error() -> None:
-    """
-    OAuth2RedirectError (a SupersetErrorException) must propagate out of
-    ``get_df_payload`` so the global Flask error handler can serialize it.
-    """
-    obj = _viz()
-    oauth_exc = OAuth2RedirectError(
-        url="https://accounts.example.com/o/oauth2/v2/auth?...",
-        tab_id="tab-123",
-        redirect_uri="https://superset.example.com/oauth2/redirect",
-    )
-
-    with patch.object(viz.BaseViz, "get_df", side_effect=oauth_exc):
-        with pytest.raises(OAuth2RedirectError) as exc_info:
-            obj.get_df_payload(QUERY_OBJ)
-
-    assert exc_info.value.error.error_type == SupersetErrorType.OAUTH2_REDIRECT
-    assert exc_info.value.error.extra == {
-        "url": "https://accounts.example.com/o/oauth2/v2/auth?...",
-        "tab_id": "tab-123",
-        "redirect_uri": "https://superset.example.com/oauth2/redirect",
-    }
-
-
-def test_get_df_payload_captures_generic_exception_as_viz_get_df_error() -> None:
-    """
-    Non-Superset exception raised by ``get_df`` are downgraded to a
-    ``VIZ_GET_DF_ERROR`` entry on ``self.errors``.
-    """
-    obj = _viz()
-
-    with patch.object(viz.BaseViz, "get_df", side_effect=RuntimeError("boom")):
-        payload = obj.get_df_payload(QUERY_OBJ)
-
-    assert obj.status == QueryStatus.FAILED
-    assert payload["status"] == QueryStatus.FAILED
-    assert len(obj.errors) == 1
-    assert obj.errors[0]["error_type"] == SupersetErrorType.VIZ_GET_DF_ERROR
-    assert obj.errors[0]["message"] == "boom"
-
-
-def test_get_df_payload_captures_query_object_validation_error() -> None:
-    """
-    ``QueryObjectValidationError`` is reported as ``VIZ_GET_DF_ERROR``.
-    """
-    obj = _viz()
-
-    with patch.object(
-        viz.BaseViz,
-        "get_df",
-        side_effect=QueryObjectValidationError("bad query"),
-    ):
-        payload = obj.get_df_payload(QUERY_OBJ)
-
-    assert obj.status == QueryStatus.FAILED
-    assert payload["status"] == QueryStatus.FAILED
-    assert len(obj.errors) == 1
-    assert obj.errors[0]["error_type"] == SupersetErrorType.VIZ_GET_DF_ERROR
-    assert obj.errors[0]["message"] == "bad query"
-
-
-def test_get_df_payload_hides_stacktrace_when_show_stacktrace_disabled() -> None:
-    """
-    The error payload must not expose a stacktrace when neither ``debug`` nor
-    ``SHOW_STACKTRACE`` is enabled.
-    """
-    obj = _viz()
-
-    # The test app runs with ``debug`` disabled, so toggling ``SHOW_STACKTRACE``
-    # off is enough to exercise the hidden-stacktrace branch.
-    assert current_app.debug is False
-    with (
-        patch.object(viz.BaseViz, "get_df", side_effect=RuntimeError("boom")),
-        patch.dict(current_app.config, {"SHOW_STACKTRACE": False}),
-    ):
-        payload = obj.get_df_payload(QUERY_OBJ)
-
-    assert obj.status == QueryStatus.FAILED
-    assert payload["stacktrace"] is None
-
-
-def test_get_df_payload_shows_stacktrace_when_show_stacktrace_enabled() -> None:
-    """
-    The error payload must expose a stacktrace when ``SHOW_STACKTRACE`` is enabled.
-    """
-    obj = _viz()
-
-    with (
-        patch.object(viz.BaseViz, "get_df", side_effect=RuntimeError("boom")),
-        patch.dict(current_app.config, {"SHOW_STACKTRACE": True}),
-    ):
-        payload = obj.get_df_payload(QUERY_OBJ)
-
-    assert obj.status == QueryStatus.FAILED
-    assert payload["stacktrace"] is not None
-    assert "RuntimeError" in payload["stacktrace"]