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

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new dce74014da refactor(deckgl): update deck.gl charts to use new api 
(#34859)
dce74014da is described below

commit dce74014dad2361a9a4ab7cd7b88efaedd33dc0a
Author: Damian Pendrak <[email protected]>
AuthorDate: Tue Sep 23 19:42:28 2025 +0200

    refactor(deckgl): update deck.gl charts to use new api (#34859)
---
 .../src/CategoricalDeckGLContainer.tsx             |  10 +-
 .../src/layers/Arc/buildQuery.ts                   |  96 ++++
 .../src/layers/Arc/index.ts                        |   5 +-
 .../src/layers/Arc/transformProps.ts               | 108 ++++
 .../src/layers/Contour/buildQuery.ts               |  34 ++
 .../src/layers/Contour/index.ts                    |   7 +-
 .../src/layers/Contour/transformProps.ts           |  21 +
 .../src/layers/Grid/Grid.tsx                       |   2 +-
 .../src/layers/Grid/buildQuery.ts                  |  27 +
 .../src/layers/Grid/index.ts                       |   5 +-
 .../src/layers/Grid/transformProps.ts              |  24 +
 .../src/layers/Heatmap/Heatmap.tsx                 |   2 +-
 .../src/layers/Heatmap/buildQuery.ts               |  23 +
 .../src/layers/Heatmap/index.ts                    |   7 +-
 .../src/layers/Heatmap/transformProps.ts           |  24 +
 .../src/layers/Hex/Hex.tsx                         |   2 +-
 .../src/layers/Hex/buildQuery.ts                   |  29 +
 .../src/layers/Hex/index.ts                        |   5 +-
 .../src/layers/Hex/transformProps.ts               |  24 +
 .../src/layers/Path/buildQuery.ts                  |  95 ++++
 .../src/layers/Path/index.ts                       |   5 +-
 .../src/layers/Path/transformProps.ts              | 166 ++++++
 .../src/layers/Polygon/Polygon.tsx                 |   2 +-
 .../src/layers/Polygon/buildQuery.ts               | 111 ++++
 .../src/layers/Polygon/index.ts                    |   5 +-
 .../src/layers/Polygon/transformProps.ts           | 143 +++++
 .../src/layers/Scatter/buildQuery.ts               | 105 ++++
 .../src/layers/Scatter/index.ts                    |   5 +-
 .../src/layers/Scatter/transformProps.ts           | 116 ++++
 .../src/layers/Screengrid/Screengrid.tsx           |   2 +-
 .../src/layers/Screengrid/buildQuery.ts            |  23 +
 .../src/layers/Screengrid/index.ts                 |   5 +-
 .../src/layers/Screengrid/transformProps.ts        |  24 +
 .../src/layers/buildQueryUtils.ts                  | 142 +++++
 .../src/layers/spatialUtils.test.ts                | 604 +++++++++++++++++++++
 .../src/layers/spatialUtils.ts                     | 400 ++++++++++++++
 .../src/layers/transformUtils.ts                   | 142 +++++
 .../src/utilities/Shared_DeckGL.tsx                |   2 +-
 .../src/utils/crossFiltersDataMask.ts              |   7 +-
 superset/views/base.py                             |   1 +
 superset/views/core.py                             |   4 +-
 41 files changed, 2530 insertions(+), 34 deletions(-)

diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
index 1757a7b77e..1400a15133 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
@@ -169,12 +169,12 @@ const CategoricalDeckGLContainer = (props: 
CategoricalDeckGLContainerProps) => {
           }));
         }
         case COLOR_SCHEME_TYPES.color_breakpoints: {
-          const defaultBreakpointColor = fd.deafult_breakpoint_color
+          const defaultBreakpointColor = fd.default_breakpoint_color
             ? [
-                fd.deafult_breakpoint_color.r,
-                fd.deafult_breakpoint_color.g,
-                fd.deafult_breakpoint_color.b,
-                fd.deafult_breakpoint_color.a * 255,
+                fd.default_breakpoint_color.r,
+                fd.default_breakpoint_color.g,
+                fd.default_breakpoint_color.b,
+                fd.default_breakpoint_color.a * 255,
               ]
             : [
                 DEFAULT_DECKGL_COLOR.r,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/buildQuery.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/buildQuery.ts
new file mode 100644
index 0000000000..6e5a714d12
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/buildQuery.ts
@@ -0,0 +1,96 @@
+/**
+ * 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 {
+  buildQueryContext,
+  ensureIsArray,
+  SqlaFormData,
+} from '@superset-ui/core';
+import {
+  getSpatialColumns,
+  addSpatialNullFilters,
+  SpatialFormData,
+} from '../spatialUtils';
+import { addTooltipColumnsToQuery } from '../buildQueryUtils';
+
+export interface DeckArcFormData extends SqlaFormData {
+  start_spatial: SpatialFormData['spatial'];
+  end_spatial: SpatialFormData['spatial'];
+  dimension?: string;
+  js_columns?: string[];
+  tooltip_contents?: unknown[];
+  tooltip_template?: string;
+}
+
+export default function buildQuery(formData: DeckArcFormData) {
+  const {
+    start_spatial,
+    end_spatial,
+    dimension,
+    js_columns,
+    tooltip_contents,
+  } = formData;
+
+  if (!start_spatial || !end_spatial) {
+    throw new Error(
+      'Start and end spatial configurations are required for Arc charts',
+    );
+  }
+
+  return buildQueryContext(formData, baseQueryObject => {
+    const startSpatialColumns = getSpatialColumns(start_spatial);
+    const endSpatialColumns = getSpatialColumns(end_spatial);
+
+    let columns = [
+      ...(baseQueryObject.columns || []),
+      ...startSpatialColumns,
+      ...endSpatialColumns,
+    ];
+
+    if (dimension) {
+      columns = [...columns, dimension];
+    }
+
+    const jsColumns = ensureIsArray(js_columns || []);
+    jsColumns.forEach(col => {
+      if (!columns.includes(col)) {
+        columns.push(col);
+      }
+    });
+
+    columns = addTooltipColumnsToQuery(columns, tooltip_contents);
+
+    let filters = addSpatialNullFilters(
+      start_spatial,
+      ensureIsArray(baseQueryObject.filters || []),
+    );
+    filters = addSpatialNullFilters(end_spatial, filters);
+
+    const isTimeseries = !!formData.time_grain_sqla;
+
+    return [
+      {
+        ...baseQueryObject,
+        columns,
+        filters,
+        is_timeseries: isTimeseries,
+        row_limit: baseQueryObject.row_limit,
+      },
+    ];
+  });
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/index.ts 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/index.ts
index 60a2c1db07..364e57c469 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/index.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/index.ts
@@ -21,7 +21,8 @@ import thumbnail from './images/thumbnail.png';
 import thumbnailDark from './images/thumbnail-dark.png';
 import example from './images/example.png';
 import exampleDark from './images/example-dark.png';
-import transformProps from '../../transformProps';
+import transformProps from './transformProps';
+import buildQuery from './buildQuery';
 import controlPanel from './controlPanel';
 
 const metadata = new ChartMetadata({
@@ -39,13 +40,13 @@ const metadata = new ChartMetadata({
   thumbnail,
   thumbnailDark,
   exampleGallery: [{ url: example, urlDark: exampleDark }],
-  useLegacyApi: true,
   tags: [t('deckGL'), t('Geo'), t('3D'), t('Relational'), t('Web')],
 });
 
 export default class ArcChartPlugin extends ChartPlugin {
   constructor() {
     super({
+      buildQuery,
       loadChart: () => import('./Arc'),
       controlPanel,
       metadata,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/transformProps.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/transformProps.ts
new file mode 100644
index 0000000000..85df90585d
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/transformProps.ts
@@ -0,0 +1,108 @@
+/**
+ * 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 { ChartProps } from '@superset-ui/core';
+import {
+  processSpatialData,
+  addJsColumnsToExtraProps,
+  DataRecord,
+} from '../spatialUtils';
+import {
+  createBaseTransformResult,
+  getRecordsFromQuery,
+  addPropertiesToFeature,
+} from '../transformUtils';
+import { DeckArcFormData } from './buildQuery';
+
+interface ArcPoint {
+  sourcePosition: [number, number];
+  targetPosition: [number, number];
+  cat_color?: string;
+  __timestamp?: number;
+  extraProps?: Record<string, unknown>;
+  [key: string]: unknown;
+}
+
+function processArcData(
+  records: DataRecord[],
+  startSpatial: DeckArcFormData['start_spatial'],
+  endSpatial: DeckArcFormData['end_spatial'],
+  dimension?: string,
+  jsColumns?: string[],
+): ArcPoint[] {
+  if (!startSpatial || !endSpatial || !records.length) {
+    return [];
+  }
+
+  const startFeatures = processSpatialData(records, startSpatial);
+  const endFeatures = processSpatialData(records, endSpatial);
+  const excludeKeys = new Set(
+    ['__timestamp', dimension, ...(jsColumns || [])].filter(
+      (key): key is string => key != null,
+    ),
+  );
+
+  return records
+    .map((record, index) => {
+      const startFeature = startFeatures[index];
+      const endFeature = endFeatures[index];
+
+      if (!startFeature || !endFeature) {
+        return null;
+      }
+
+      let arcPoint: ArcPoint = {
+        sourcePosition: startFeature.position,
+        targetPosition: endFeature.position,
+        extraProps: {},
+      };
+
+      arcPoint = addJsColumnsToExtraProps(arcPoint, record, jsColumns);
+
+      if (dimension && record[dimension] != null) {
+        arcPoint.cat_color = String(record[dimension]);
+      }
+
+      // eslint-disable-next-line no-underscore-dangle
+      if (record.__timestamp != null) {
+        // eslint-disable-next-line no-underscore-dangle
+        arcPoint.__timestamp = Number(record.__timestamp);
+      }
+
+      arcPoint = addPropertiesToFeature(arcPoint, record, excludeKeys);
+      return arcPoint;
+    })
+    .filter((point): point is ArcPoint => point !== null);
+}
+
+export default function transformProps(chartProps: ChartProps) {
+  const { rawFormData: formData } = chartProps;
+  const { start_spatial, end_spatial, dimension, js_columns } =
+    formData as DeckArcFormData;
+
+  const records = getRecordsFromQuery(chartProps.queriesData);
+  const features = processArcData(
+    records,
+    start_spatial,
+    end_spatial,
+    dimension,
+    js_columns,
+  );
+
+  return createBaseTransformResult(chartProps, features);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/buildQuery.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/buildQuery.ts
new file mode 100644
index 0000000000..294a0f997b
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/buildQuery.ts
@@ -0,0 +1,34 @@
+/**
+ * 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 { SpatialFormData, buildSpatialQuery } from '../spatialUtils';
+
+export interface DeckContourFormData extends SpatialFormData {
+  cellSize?: string;
+  aggregation?: string;
+  contours?: Array<{
+    color: { r: number; g: number; b: number };
+    lowerThreshold: number;
+    upperThreshold?: number;
+    strokeWidth?: number;
+  }>;
+}
+
+export default function buildQuery(formData: DeckContourFormData) {
+  return buildSpatialQuery(formData);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts
index 60b0f122fb..7d220b1ac0 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts
@@ -17,12 +17,13 @@
  * under the License.
  */
 import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
-import transformProps from '../../transformProps';
-import controlPanel from './controlPanel';
 import thumbnail from './images/thumbnail.png';
 import thumbnailDark from './images/thumbnail-dark.png';
 import example from './images/example.png';
 import exampleDark from './images/example-dark.png';
+import buildQuery from './buildQuery';
+import transformProps from './transformProps';
+import controlPanel from './controlPanel';
 
 const metadata = new ChartMetadata({
   category: t('Map'),
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
   name: t('deck.gl Contour'),
   thumbnail,
   thumbnailDark,
-  useLegacyApi: true,
   tags: [t('deckGL'), t('Spatial'), t('Comparison')],
   behaviors: [Behavior.InteractiveChart],
 });
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
 export default class ContourChartPlugin extends ChartPlugin {
   constructor() {
     super({
+      buildQuery,
       loadChart: () => import('./Contour'),
       controlPanel,
       metadata,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/transformProps.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/transformProps.ts
new file mode 100644
index 0000000000..1ca8144242
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/transformProps.ts
@@ -0,0 +1,21 @@
+/**
+ * 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 { transformSpatialProps } from '../spatialUtils';
+
+export default transformSpatialProps;
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx
index da1367a189..72faceb32a 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx
@@ -76,7 +76,7 @@ export const getLayer: GetLayerType<GridLayer> = function ({
 
   const colorSchemeType = fd.color_scheme_type;
   const colorRange = getColorRange({
-    defaultBreakpointsColor: fd.deafult_breakpoint_color,
+    defaultBreakpointsColor: fd.default_breakpoint_color,
     colorSchemeType,
     colorScale,
     colorBreakpoints,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/buildQuery.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/buildQuery.ts
new file mode 100644
index 0000000000..fdd73b1964
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/buildQuery.ts
@@ -0,0 +1,27 @@
+/**
+ * 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 { SpatialFormData, buildSpatialQuery } from '../spatialUtils';
+
+export interface DeckGridFormData extends SpatialFormData {
+  extruded?: boolean;
+}
+
+export default function buildQuery(formData: DeckGridFormData) {
+  return buildSpatialQuery(formData);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/index.ts 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/index.ts
index 7570121deb..18144934ac 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/index.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/index.ts
@@ -21,7 +21,8 @@ import thumbnail from './images/thumbnail.png';
 import thumbnailDark from './images/thumbnail-dark.png';
 import example from './images/example.png';
 import exampleDark from './images/example-dark.png';
-import transformProps from '../../transformProps';
+import buildQuery from './buildQuery';
+import transformProps from './transformProps';
 import controlPanel from './controlPanel';
 
 const metadata = new ChartMetadata({
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
   thumbnail,
   thumbnailDark,
   exampleGallery: [{ url: example, urlDark: exampleDark }],
-  useLegacyApi: true,
   tags: [t('deckGL'), t('3D'), t('Comparison')],
   behaviors: [Behavior.InteractiveChart],
 });
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
 export default class GridChartPlugin extends ChartPlugin {
   constructor() {
     super({
+      buildQuery,
       loadChart: () => import('./Grid'),
       controlPanel,
       metadata,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/transformProps.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/transformProps.ts
new file mode 100644
index 0000000000..4b8f437d7f
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/transformProps.ts
@@ -0,0 +1,24 @@
+/**
+ * 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 { ChartProps } from '@superset-ui/core';
+import { transformSpatialProps } from '../spatialUtils';
+
+export default function transformProps(chartProps: ChartProps) {
+  return transformSpatialProps(chartProps);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/Heatmap.tsx
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/Heatmap.tsx
index 68bdfac85d..03e163ea00 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/Heatmap.tsx
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/Heatmap.tsx
@@ -126,7 +126,7 @@ export const getLayer: GetLayerType<HeatmapLayer> = ({
 
   const colorSchemeType = fd.color_scheme_type;
   const colorRange = getColorRange({
-    defaultBreakpointsColor: fd.deafult_breakpoint_color,
+    defaultBreakpointsColor: fd.default_breakpoint_color,
     colorBreakpoints: fd.color_breakpoints,
     fixedColor: fd.color_picker,
     colorSchemeType,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/buildQuery.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/buildQuery.ts
new file mode 100644
index 0000000000..94607704ac
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/buildQuery.ts
@@ -0,0 +1,23 @@
+/**
+ * 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 { SpatialFormData, buildSpatialQuery } from '../spatialUtils';
+
+export default function buildQuery(formData: SpatialFormData) {
+  return buildSpatialQuery(formData);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/index.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/index.ts
index 418e08daa4..23fc2ad58a 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/index.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/index.ts
@@ -17,12 +17,13 @@
  * under the License.
  */
 import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
-import transformProps from '../../transformProps';
-import controlPanel from './controlPanel';
 import thumbnail from './images/thumbnail.png';
 import thumbnailDark from './images/thumbnail-dark.png';
 import example from './images/example.png';
 import exampleDark from './images/example-dark.png';
+import buildQuery from './buildQuery';
+import transformProps from './transformProps';
+import controlPanel from './controlPanel';
 
 const metadata = new ChartMetadata({
   category: t('Map'),
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
   name: t('deck.gl Heatmap'),
   thumbnail,
   thumbnailDark,
-  useLegacyApi: true,
   tags: [t('deckGL'), t('Spatial'), t('Comparison')],
   behaviors: [Behavior.InteractiveChart],
 });
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
 export default class HeatmapChartPlugin extends ChartPlugin {
   constructor() {
     super({
+      buildQuery,
       loadChart: () => import('./Heatmap'),
       controlPanel,
       metadata,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/transformProps.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/transformProps.ts
new file mode 100644
index 0000000000..4b8f437d7f
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/transformProps.ts
@@ -0,0 +1,24 @@
+/**
+ * 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 { ChartProps } from '@superset-ui/core';
+import { transformSpatialProps } from '../spatialUtils';
+
+export default function transformProps(chartProps: ChartProps) {
+  return transformSpatialProps(chartProps);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx
index 9377ee75b1..1f1e35f3dc 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx
@@ -75,7 +75,7 @@ export const getLayer: GetLayerType<HexagonLayer> = function 
({
 
   const colorSchemeType = fd.color_scheme_type;
   const colorRange = getColorRange({
-    defaultBreakpointsColor: fd.deafult_breakpoint_color,
+    defaultBreakpointsColor: fd.default_breakpoint_color,
     colorBreakpoints: fd.color_breakpoints,
     fixedColor: fd.color_picker,
     colorSchemeType,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/buildQuery.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/buildQuery.ts
new file mode 100644
index 0000000000..d5b9a56a13
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/buildQuery.ts
@@ -0,0 +1,29 @@
+/**
+ * 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 { SpatialFormData, buildSpatialQuery } from '../spatialUtils';
+
+export interface DeckHexFormData extends SpatialFormData {
+  extruded?: boolean;
+  js_agg_function?: string;
+  grid_size?: number;
+}
+
+export default function buildQuery(formData: DeckHexFormData) {
+  return buildSpatialQuery(formData);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/index.ts 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/index.ts
index 2712e847db..fda3cff9b4 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/index.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/index.ts
@@ -21,7 +21,8 @@ import thumbnail from './images/thumbnail.png';
 import thumbnailDark from './images/thumbnail-dark.png';
 import example from './images/example.png';
 import exampleDark from './images/example-dark.png';
-import transformProps from '../../transformProps';
+import buildQuery from './buildQuery';
+import transformProps from './transformProps';
 import controlPanel from './controlPanel';
 
 const metadata = new ChartMetadata({
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
   name: t('deck.gl 3D Hexagon'),
   thumbnail,
   thumbnailDark,
-  useLegacyApi: true,
   tags: [t('deckGL'), t('3D'), t('Geo'), t('Comparison')],
   behaviors: [Behavior.InteractiveChart],
 });
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
 export default class HexChartPlugin extends ChartPlugin {
   constructor() {
     super({
+      buildQuery,
       loadChart: () => import('./Hex'),
       controlPanel,
       metadata,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/transformProps.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/transformProps.ts
new file mode 100644
index 0000000000..4b8f437d7f
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/transformProps.ts
@@ -0,0 +1,24 @@
+/**
+ * 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 { ChartProps } from '@superset-ui/core';
+import { transformSpatialProps } from '../spatialUtils';
+
+export default function transformProps(chartProps: ChartProps) {
+  return transformSpatialProps(chartProps);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/buildQuery.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/buildQuery.ts
new file mode 100644
index 0000000000..b22ef0b6ee
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/buildQuery.ts
@@ -0,0 +1,95 @@
+/**
+ * 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 {
+  buildQueryContext,
+  ensureIsArray,
+  SqlaFormData,
+  QueryFormColumn,
+} from '@superset-ui/core';
+import { addNullFilters, addTooltipColumnsToQuery } from '../buildQueryUtils';
+
+export interface DeckPathFormData extends SqlaFormData {
+  line_column?: string;
+  line_type?: 'polyline' | 'json' | 'geohash';
+  metric?: string;
+  reverse_long_lat?: boolean;
+  js_columns?: string[];
+  tooltip_contents?: unknown[];
+  tooltip_template?: string;
+}
+
+export default function buildQuery(formData: DeckPathFormData) {
+  const { line_column, metric, js_columns, tooltip_contents } = formData;
+
+  if (!line_column) {
+    throw new Error('Line column is required for Path charts');
+  }
+
+  return buildQueryContext(formData, {
+    buildQuery: baseQueryObject => {
+      const columns = ensureIsArray(
+        baseQueryObject.columns || [],
+      ) as QueryFormColumn[];
+      const metrics = ensureIsArray(baseQueryObject.metrics || []);
+      const groupby = ensureIsArray(
+        baseQueryObject.groupby || [],
+      ) as QueryFormColumn[];
+      const jsColumns = ensureIsArray(js_columns || []);
+
+      if (baseQueryObject.metrics?.length || metric) {
+        if (metric && !metrics.includes(metric)) {
+          metrics.push(metric);
+        }
+        if (!groupby.includes(line_column)) {
+          groupby.push(line_column);
+        }
+      } else if (!columns.includes(line_column)) {
+        columns.push(line_column);
+      }
+
+      jsColumns.forEach(col => {
+        if (!columns.includes(col) && !groupby.includes(col)) {
+          columns.push(col);
+        }
+      });
+
+      const finalColumns = addTooltipColumnsToQuery(columns, tooltip_contents);
+      const finalGroupby = addTooltipColumnsToQuery(groupby, tooltip_contents);
+
+      const filters = addNullFilters(
+        ensureIsArray(baseQueryObject.filters || []),
+        [line_column],
+      );
+
+      const isTimeseries = Boolean(formData.time_grain_sqla);
+
+      return [
+        {
+          ...baseQueryObject,
+          columns: finalColumns,
+          metrics,
+          groupby: finalGroupby,
+          filters,
+          is_timeseries: isTimeseries,
+          row_limit: baseQueryObject.row_limit,
+        },
+      ];
+    },
+  });
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/index.ts 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/index.ts
index fd930a2780..0c5ea4b421 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/index.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/index.ts
@@ -21,7 +21,8 @@ import thumbnail from './images/thumbnail.png';
 import thumbnailDark from './images/thumbnail-dark.png';
 import example from './images/example.png';
 import exampleDark from './images/example-dark.png';
-import transformProps from '../../transformProps';
+import buildQuery from './buildQuery';
+import transformProps from './transformProps';
 import controlPanel from './controlPanel';
 
 const metadata = new ChartMetadata({
@@ -32,7 +33,6 @@ const metadata = new ChartMetadata({
   thumbnail,
   thumbnailDark,
   exampleGallery: [{ url: example, urlDark: exampleDark }],
-  useLegacyApi: true,
   tags: [t('deckGL'), t('Web')],
   behaviors: [Behavior.InteractiveChart],
 });
@@ -40,6 +40,7 @@ const metadata = new ChartMetadata({
 export default class PathChartPlugin extends ChartPlugin {
   constructor() {
     super({
+      buildQuery,
       loadChart: () => import('./Path'),
       controlPanel,
       metadata,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/transformProps.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/transformProps.ts
new file mode 100644
index 0000000000..702ca92b4a
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/transformProps.ts
@@ -0,0 +1,166 @@
+/**
+ * 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 { ChartProps, DTTM_ALIAS } from '@superset-ui/core';
+import { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils';
+import {
+  createBaseTransformResult,
+  getRecordsFromQuery,
+  getMetricLabelFromFormData,
+  parseMetricValue,
+  addPropertiesToFeature,
+} from '../transformUtils';
+import { DeckPathFormData } from './buildQuery';
+
+declare global {
+  interface Window {
+    polyline?: {
+      decode: (data: string) => [number, number][];
+    };
+    geohash?: {
+      decode: (data: string) => { longitude: number; latitude: number };
+    };
+  }
+}
+
+export interface DeckPathTransformPropsFormData extends DeckPathFormData {
+  js_data_mutator?: string;
+  js_tooltip?: string;
+  js_onclick_href?: string;
+}
+
+interface PathFeature {
+  path: [number, number][];
+  metric?: number;
+  timestamp?: unknown;
+  extraProps?: Record<string, unknown>;
+  [key: string]: unknown;
+}
+
+const decoders = {
+  json: (data: string): [number, number][] => {
+    try {
+      const parsed = JSON.parse(data);
+      return Array.isArray(parsed) ? parsed : [];
+    } catch (error) {
+      return [];
+    }
+  },
+  polyline: (data: string): [number, number][] => {
+    try {
+      if (typeof window !== 'undefined' && window.polyline) {
+        return window.polyline.decode(data);
+      }
+      return [];
+    } catch (error) {
+      return [];
+    }
+  },
+  geohash: (data: string): [number, number][] => {
+    try {
+      if (typeof window !== 'undefined' && window.geohash) {
+        const decoded = window.geohash.decode(data);
+        return [[decoded.longitude, decoded.latitude]];
+      }
+      return [];
+    } catch (error) {
+      return [];
+    }
+  },
+};
+
+function processPathData(
+  records: DataRecord[],
+  lineColumn: string,
+  lineType: 'polyline' | 'json' | 'geohash' = 'json',
+  reverseLongLat: boolean = false,
+  metricLabel?: string,
+  jsColumns?: string[],
+): PathFeature[] {
+  if (!records.length || !lineColumn) {
+    return [];
+  }
+
+  const decoder = decoders[lineType] || decoders.json;
+  const excludeKeys = new Set(
+    [
+      lineType !== 'geohash' ? lineColumn : undefined,
+      'timestamp',
+      DTTM_ALIAS,
+      metricLabel,
+      ...(jsColumns || []),
+    ].filter(Boolean) as string[],
+  );
+
+  return records.map(record => {
+    const lineData = record[lineColumn];
+    let path: [number, number][] = [];
+
+    if (lineData) {
+      path = decoder(String(lineData));
+      if (reverseLongLat && path.length > 0) {
+        path = path.map(([lng, lat]) => [lat, lng]);
+      }
+    }
+
+    let feature: PathFeature = {
+      path,
+      timestamp: record[DTTM_ALIAS],
+      extraProps: {},
+    };
+
+    if (metricLabel && record[metricLabel] != null) {
+      const metricValue = parseMetricValue(record[metricLabel]);
+      if (metricValue !== undefined) {
+        feature.metric = metricValue;
+      }
+    }
+
+    feature = addJsColumnsToExtraProps(feature, record, jsColumns);
+    feature = addPropertiesToFeature(feature, record, excludeKeys);
+    return feature;
+  });
+}
+
+export default function transformProps(chartProps: ChartProps) {
+  const { rawFormData: formData } = chartProps;
+  const {
+    line_column,
+    line_type = 'json',
+    metric,
+    reverse_long_lat = false,
+    js_columns,
+  } = formData as DeckPathTransformPropsFormData;
+
+  const metricLabel = getMetricLabelFromFormData(metric);
+  const records = getRecordsFromQuery(chartProps.queriesData);
+  const features = processPathData(
+    records,
+    line_column || '',
+    line_type,
+    reverse_long_lat,
+    metricLabel,
+    js_columns,
+  ).reverse();
+
+  return createBaseTransformResult(
+    chartProps,
+    features,
+    metricLabel ? [metricLabel] : [],
+  );
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
index 630a2639d0..69c21e9079 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
@@ -118,7 +118,7 @@ export const getLayer: GetLayerType<PolygonLayer> = 
function ({
     fd.fill_color_picker;
   const sc: { r: number; g: number; b: number; a: number } =
     fd.stroke_color_picker;
-  const defaultBreakpointColor = fd.deafult_breakpoint_color;
+  const defaultBreakpointColor = fd.default_breakpoint_color;
   let data = [...payload.data.features];
 
   if (fd.js_data_mutator) {
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/buildQuery.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/buildQuery.ts
new file mode 100644
index 0000000000..257096525c
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/buildQuery.ts
@@ -0,0 +1,111 @@
+/**
+ * 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 {
+  buildQueryContext,
+  ensureIsArray,
+  SqlaFormData,
+  getMetricLabel,
+  QueryObjectFilterClause,
+  QueryObject,
+  QueryFormColumn,
+} from '@superset-ui/core';
+import { addTooltipColumnsToQuery } from '../buildQueryUtils';
+
+export interface DeckPolygonFormData extends SqlaFormData {
+  line_column?: string;
+  line_type?: string;
+  metric?: string;
+  point_radius_fixed?: {
+    value?: string;
+  };
+  reverse_long_lat?: boolean;
+  filter_nulls?: boolean;
+  js_columns?: string[];
+  tooltip_contents?: unknown[];
+  tooltip_template?: string;
+}
+
+export default function buildQuery(formData: DeckPolygonFormData) {
+  const {
+    line_column,
+    metric,
+    point_radius_fixed,
+    filter_nulls = true,
+    js_columns,
+    tooltip_contents,
+  } = formData;
+
+  if (!line_column) {
+    throw new Error('Polygon column is required for Polygon charts');
+  }
+
+  return buildQueryContext(formData, (baseQueryObject: QueryObject) => {
+    let columns: QueryFormColumn[] = [
+      ...ensureIsArray(baseQueryObject.columns || []),
+      line_column,
+    ];
+
+    const jsColumns = ensureIsArray(js_columns || []);
+    jsColumns.forEach((col: string) => {
+      if (!columns.includes(col)) {
+        columns.push(col);
+      }
+    });
+
+    columns = addTooltipColumnsToQuery(columns, tooltip_contents);
+
+    const metrics = [];
+    if (metric) {
+      metrics.push(metric);
+    }
+    if (point_radius_fixed?.value) {
+      metrics.push(point_radius_fixed.value);
+    }
+
+    const filters = ensureIsArray(baseQueryObject.filters || []);
+    if (filter_nulls) {
+      const nullFilters: QueryObjectFilterClause[] = [
+        {
+          col: line_column,
+          op: 'IS NOT NULL',
+        },
+      ];
+
+      if (metric) {
+        nullFilters.push({
+          col: getMetricLabel(metric),
+          op: 'IS NOT NULL',
+        });
+      }
+
+      filters.push(...nullFilters);
+    }
+
+    return [
+      {
+        ...baseQueryObject,
+        columns,
+        metrics,
+        filters,
+        is_timeseries: false,
+        row_limit: baseQueryObject.row_limit,
+      },
+    ];
+  });
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/index.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/index.ts
index 2f793a4539..fa89cc4474 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/index.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/index.ts
@@ -21,7 +21,8 @@ import thumbnail from './images/thumbnail.png';
 import thumbnailDark from './images/thumbnail-dark.png';
 import example from './images/example.png';
 import exampleDark from './images/example-dark.png';
-import transformProps from '../../transformProps';
+import transformProps from './transformProps';
+import buildQuery from './buildQuery';
 import controlPanel from './controlPanel';
 
 const metadata = new ChartMetadata({
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
   thumbnail,
   thumbnailDark,
   exampleGallery: [{ url: example, urlDark: exampleDark }],
-  useLegacyApi: true,
   tags: [t('deckGL'), t('3D'), t('Multi-Dimensions'), t('Geo')],
   behaviors: [Behavior.InteractiveChart],
 });
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
 export default class PolygonChartPlugin extends ChartPlugin {
   constructor() {
     super({
+      buildQuery,
       loadChart: () => import('./Polygon'),
       controlPanel,
       metadata,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts
new file mode 100644
index 0000000000..b1c73b72d5
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts
@@ -0,0 +1,143 @@
+/**
+ * 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 { ChartProps } from '@superset-ui/core';
+import { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils';
+import {
+  createBaseTransformResult,
+  getRecordsFromQuery,
+  getMetricLabelFromFormData,
+  parseMetricValue,
+  addPropertiesToFeature,
+} from '../transformUtils';
+import { DeckPolygonFormData } from './buildQuery';
+
+interface PolygonFeature {
+  polygon?: number[][];
+  name?: string;
+  elevation?: number;
+  extraProps?: Record<string, unknown>;
+  metrics?: Record<string, number | string>;
+}
+
+function processPolygonData(
+  records: DataRecord[],
+  formData: DeckPolygonFormData,
+): PolygonFeature[] {
+  const {
+    line_column,
+    line_type,
+    metric,
+    point_radius_fixed,
+    reverse_long_lat,
+    js_columns,
+  } = formData;
+
+  if (!line_column || !records.length) {
+    return [];
+  }
+
+  const metricLabel = getMetricLabelFromFormData(metric);
+  const elevationLabel = getMetricLabelFromFormData(point_radius_fixed);
+  const excludeKeys = new Set([line_column, ...(js_columns || [])]);
+
+  return records
+    .map(record => {
+      let feature: PolygonFeature = {
+        extraProps: {},
+        metrics: {},
+      };
+
+      feature = addJsColumnsToExtraProps(feature, record, js_columns);
+      const updatedFeature = addPropertiesToFeature(
+        feature as unknown as Record<string, unknown>,
+        record,
+        excludeKeys,
+      );
+      feature = updatedFeature as unknown as PolygonFeature;
+
+      const rawPolygonData = record[line_column];
+      if (!rawPolygonData) {
+        return null;
+      }
+
+      try {
+        let polygonCoords: number[][];
+
+        switch (line_type) {
+          case 'json': {
+            const parsed =
+              typeof rawPolygonData === 'string'
+                ? JSON.parse(rawPolygonData)
+                : rawPolygonData;
+
+            if (parsed.coordinates) {
+              polygonCoords = parsed.coordinates[0] || parsed.coordinates;
+            } else if (Array.isArray(parsed)) {
+              polygonCoords = parsed;
+            } else {
+              return null;
+            }
+            break;
+          }
+          case 'geohash':
+          case 'zipcode':
+          default: {
+            polygonCoords = Array.isArray(rawPolygonData) ? rawPolygonData : 
[];
+            break;
+          }
+        }
+
+        if (reverse_long_lat && polygonCoords.length > 0) {
+          polygonCoords = polygonCoords.map(coord => [coord[1], coord[0]]);
+        }
+
+        feature.polygon = polygonCoords;
+
+        if (elevationLabel && record[elevationLabel] != null) {
+          const elevationValue = parseMetricValue(record[elevationLabel]);
+          if (elevationValue !== undefined) {
+            feature.elevation = elevationValue;
+          }
+        }
+
+        if (metricLabel && record[metricLabel] != null) {
+          const metricValue = record[metricLabel];
+          if (
+            typeof metricValue === 'string' ||
+            typeof metricValue === 'number'
+          ) {
+            feature.metrics![metricLabel] = metricValue;
+          }
+        }
+      } catch {
+        return null;
+      }
+
+      return feature;
+    })
+    .filter((feature): feature is PolygonFeature => feature !== null);
+}
+
+export default function transformProps(chartProps: ChartProps) {
+  const { rawFormData: formData } = chartProps;
+  const records = getRecordsFromQuery(chartProps.queriesData);
+  const features = processPolygonData(records, formData as 
DeckPolygonFormData);
+
+  return createBaseTransformResult(chartProps, features);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.ts
new file mode 100644
index 0000000000..66b7146190
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.ts
@@ -0,0 +1,105 @@
+/**
+ * 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 {
+  buildQueryContext,
+  ensureIsArray,
+  QueryFormOrderBy,
+  SqlaFormData,
+  QueryFormColumn,
+  QueryObject,
+} from '@superset-ui/core';
+import {
+  getSpatialColumns,
+  addSpatialNullFilters,
+  SpatialFormData,
+} from '../spatialUtils';
+import {
+  addJsColumnsToColumns,
+  processMetricsArray,
+  addTooltipColumnsToQuery,
+} from '../buildQueryUtils';
+
+export interface DeckScatterFormData
+  extends Omit<SpatialFormData, 'color_picker'>,
+    SqlaFormData {
+  point_radius_fixed?: {
+    value?: string;
+  };
+  multiplier?: number;
+  point_unit?: string;
+  min_radius?: number;
+  max_radius?: number;
+  color_picker?: { r: number; g: number; b: number; a: number };
+  category_name?: string;
+}
+
+export default function buildQuery(formData: DeckScatterFormData) {
+  const {
+    spatial,
+    point_radius_fixed,
+    category_name,
+    js_columns,
+    tooltip_contents,
+  } = formData;
+
+  if (!spatial) {
+    throw new Error('Spatial configuration is required for Scatter charts');
+  }
+
+  return buildQueryContext(formData, {
+    buildQuery: (baseQueryObject: QueryObject) => {
+      const spatialColumns = getSpatialColumns(spatial);
+      let columns = [...(baseQueryObject.columns || []), ...spatialColumns];
+
+      if (category_name) {
+        columns.push(category_name);
+      }
+
+      const columnStrings = columns.map(col =>
+        typeof col === 'string' ? col : col.label || col.sqlExpression || '',
+      );
+      const withJsColumns = addJsColumnsToColumns(columnStrings, js_columns);
+
+      columns = withJsColumns as QueryFormColumn[];
+      columns = addTooltipColumnsToQuery(columns, tooltip_contents);
+
+      const metrics = processMetricsArray([point_radius_fixed?.value]);
+      const filters = addSpatialNullFilters(
+        spatial,
+        ensureIsArray(baseQueryObject.filters || []),
+      );
+
+      const orderby = point_radius_fixed?.value
+        ? ([[point_radius_fixed.value, false]] as QueryFormOrderBy[])
+        : (baseQueryObject.orderby as QueryFormOrderBy[]) || [];
+
+      return [
+        {
+          ...baseQueryObject,
+          columns,
+          metrics,
+          filters,
+          orderby,
+          is_timeseries: false,
+          row_limit: baseQueryObject.row_limit,
+        },
+      ];
+    },
+  });
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/index.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/index.ts
index 4f32f4a1c7..cef908989a 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/index.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/index.ts
@@ -21,7 +21,8 @@ import thumbnail from './images/thumbnail.png';
 import thumbnailDark from './images/thumbnail-dark.png';
 import example from './images/example.png';
 import exampleDark from './images/example-dark.png';
-import transformProps from '../../transformProps';
+import buildQuery from './buildQuery';
+import transformProps from './transformProps';
 import controlPanel from './controlPanel';
 
 const metadata = new ChartMetadata({
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
   thumbnail,
   thumbnailDark,
   exampleGallery: [{ url: example, urlDark: exampleDark }],
-  useLegacyApi: true,
   tags: [
     t('deckGL'),
     t('Comparison'),
@@ -50,6 +50,7 @@ const metadata = new ChartMetadata({
 export default class ScatterChartPlugin extends ChartPlugin {
   constructor() {
     super({
+      buildQuery,
       loadChart: () => import('./Scatter'),
       controlPanel,
       metadata,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/transformProps.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/transformProps.ts
new file mode 100644
index 0000000000..baadec33c9
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/transformProps.ts
@@ -0,0 +1,116 @@
+/**
+ * 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 { ChartProps } from '@superset-ui/core';
+import { processSpatialData, DataRecord } from '../spatialUtils';
+import {
+  createBaseTransformResult,
+  getRecordsFromQuery,
+  getMetricLabelFromFormData,
+  parseMetricValue,
+  addPropertiesToFeature,
+} from '../transformUtils';
+import { DeckScatterFormData } from './buildQuery';
+
+interface ScatterPoint {
+  position: [number, number];
+  radius?: number;
+  color?: [number, number, number, number];
+  cat_color?: string;
+  metric?: number;
+  extraProps?: Record<string, unknown>;
+  [key: string]: unknown;
+}
+
+function processScatterData(
+  records: DataRecord[],
+  spatial: DeckScatterFormData['spatial'],
+  radiusMetricLabel?: string,
+  categoryColumn?: string,
+  jsColumns?: string[],
+): ScatterPoint[] {
+  if (!spatial || !records.length) {
+    return [];
+  }
+
+  const spatialFeatures = processSpatialData(records, spatial);
+  const excludeKeys = new Set([
+    'position',
+    'weight',
+    'extraProps',
+    ...(spatial
+      ? [
+          spatial.lonCol,
+          spatial.latCol,
+          spatial.lonlatCol,
+          spatial.geohashCol,
+        ].filter(Boolean)
+      : []),
+    radiusMetricLabel,
+    categoryColumn,
+    ...(jsColumns || []),
+  ]);
+
+  return spatialFeatures.map(feature => {
+    let scatterPoint: ScatterPoint = {
+      position: feature.position,
+      extraProps: feature.extraProps || {},
+    };
+
+    if (radiusMetricLabel && feature[radiusMetricLabel] != null) {
+      const radiusValue = parseMetricValue(feature[radiusMetricLabel]);
+      if (radiusValue !== undefined) {
+        scatterPoint.radius = radiusValue;
+        scatterPoint.metric = radiusValue;
+      }
+    }
+
+    if (categoryColumn && feature[categoryColumn] != null) {
+      scatterPoint.cat_color = String(feature[categoryColumn]);
+    }
+
+    scatterPoint = addPropertiesToFeature(
+      scatterPoint,
+      feature as DataRecord,
+      excludeKeys,
+    );
+    return scatterPoint;
+  });
+}
+
+export default function transformProps(chartProps: ChartProps) {
+  const { rawFormData: formData } = chartProps;
+  const { spatial, point_radius_fixed, category_name, js_columns } =
+    formData as DeckScatterFormData;
+
+  const radiusMetricLabel = getMetricLabelFromFormData(point_radius_fixed);
+  const records = getRecordsFromQuery(chartProps.queriesData);
+  const features = processScatterData(
+    records,
+    spatial,
+    radiusMetricLabel,
+    category_name,
+    js_columns,
+  );
+
+  return createBaseTransformResult(
+    chartProps,
+    features,
+    radiusMetricLabel ? [radiusMetricLabel] : [],
+  );
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
index 9ea46cd453..bbade67044 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
@@ -123,7 +123,7 @@ export const getLayer: GetLayerType<ScreenGridLayer> = 
function ({
 
   const colorSchemeType = fd.color_scheme_type as ColorSchemeType & 'default';
   const colorRange = getColorRange({
-    defaultBreakpointsColor: fd.deafult_breakpoint_color,
+    defaultBreakpointsColor: fd.default_breakpoint_color,
     colorBreakpoints: fd.color_breakpoints,
     fixedColor: fd.color_picker,
     colorSchemeType,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/buildQuery.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/buildQuery.ts
new file mode 100644
index 0000000000..94607704ac
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/buildQuery.ts
@@ -0,0 +1,23 @@
+/**
+ * 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 { SpatialFormData, buildSpatialQuery } from '../spatialUtils';
+
+export default function buildQuery(formData: SpatialFormData) {
+  return buildSpatialQuery(formData);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/index.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/index.ts
index 574d50dfff..87758e37cf 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/index.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/index.ts
@@ -21,7 +21,8 @@ import thumbnail from './images/thumbnail.png';
 import thumbnailDark from './images/thumbnail-dark.png';
 import example from './images/example.png';
 import exampleDark from './images/example-dark.png';
-import transformProps from '../../transformProps';
+import buildQuery from './buildQuery';
+import transformProps from './transformProps';
 import controlPanel from './controlPanel';
 
 const metadata = new ChartMetadata({
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
   thumbnail,
   thumbnailDark,
   exampleGallery: [{ url: example, urlDark: exampleDark }],
-  useLegacyApi: true,
   tags: [t('deckGL'), t('Comparison'), t('Intensity'), t('Density')],
   behaviors: [Behavior.InteractiveChart],
 });
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
 export default class ScreengridChartPlugin extends ChartPlugin {
   constructor() {
     super({
+      buildQuery,
       loadChart: () => import('./Screengrid'),
       controlPanel,
       metadata,
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/transformProps.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/transformProps.ts
new file mode 100644
index 0000000000..4b8f437d7f
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/transformProps.ts
@@ -0,0 +1,24 @@
+/**
+ * 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 { ChartProps } from '@superset-ui/core';
+import { transformSpatialProps } from '../spatialUtils';
+
+export default function transformProps(chartProps: ChartProps) {
+  return transformSpatialProps(chartProps);
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/buildQueryUtils.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/buildQueryUtils.ts
new file mode 100644
index 0000000000..f61a71ede7
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/buildQueryUtils.ts
@@ -0,0 +1,142 @@
+/**
+ * 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 {
+  getMetricLabel,
+  QueryObjectFilterClause,
+  QueryFormColumn,
+  getColumnLabel,
+} from '@superset-ui/core';
+
+export function addJsColumnsToColumns(
+  columns: string[],
+  jsColumns?: string[],
+  existingColumns?: string[],
+): string[] {
+  if (!jsColumns?.length) return columns;
+
+  const allExisting = new Set([...columns, ...(existingColumns || [])]);
+  const result = [...columns];
+
+  jsColumns.forEach(col => {
+    if (!allExisting.has(col)) {
+      result.push(col);
+      allExisting.add(col);
+    }
+  });
+
+  return result;
+}
+
+export function addNullFilters(
+  filters: QueryObjectFilterClause[],
+  columnNames: string[],
+): QueryObjectFilterClause[] {
+  const existingFilters = new Set(
+    filters
+      .filter(filter => filter.op === 'IS NOT NULL')
+      .map(filter => filter.col),
+  );
+
+  const nullFilters: QueryObjectFilterClause[] = columnNames
+    .filter(col => !existingFilters.has(col))
+    .map(col => ({
+      col,
+      op: 'IS NOT NULL' as const,
+    }));
+
+  return [...filters, ...nullFilters];
+}
+
+export function addMetricNullFilter(
+  filters: QueryObjectFilterClause[],
+  metric?: string,
+): QueryObjectFilterClause[] {
+  if (!metric) return filters;
+  return addNullFilters(filters, [getMetricLabel(metric)]);
+}
+
+export function ensureColumnsUnique(columns: string[]): string[] {
+  return [...new Set(columns)];
+}
+
+export function addColumnsIfNotExists(
+  baseColumns: string[],
+  newColumns: string[],
+): string[] {
+  const existing = new Set(baseColumns);
+  const result = [...baseColumns];
+
+  newColumns.forEach(col => {
+    if (!existing.has(col)) {
+      result.push(col);
+      existing.add(col);
+    }
+  });
+
+  return result;
+}
+
+export function processMetricsArray(metrics: (string | undefined)[]): string[] 
{
+  return metrics.filter((metric): metric is string => Boolean(metric));
+}
+
+export function extractTooltipColumns(tooltipContents?: unknown[]): string[] {
+  if (!Array.isArray(tooltipContents) || !tooltipContents.length) {
+    return [];
+  }
+
+  const columns: string[] = [];
+
+  tooltipContents.forEach(item => {
+    if (typeof item === 'string') {
+      columns.push(item);
+    } else if (item && typeof item === 'object') {
+      const objItem = item as Record<string, unknown>;
+      if (
+        objItem.item_type === 'column' &&
+        typeof objItem.column_name === 'string'
+      ) {
+        columns.push(objItem.column_name);
+      }
+    }
+  });
+
+  return columns;
+}
+
+export function addTooltipColumnsToQuery(
+  baseColumns: QueryFormColumn[],
+  tooltipContents?: unknown[],
+): QueryFormColumn[] {
+  const tooltipColumns = extractTooltipColumns(tooltipContents);
+
+  const baseColumnLabels = baseColumns.map(getColumnLabel);
+  const existingLabels = new Set(baseColumnLabels);
+
+  const result: QueryFormColumn[] = [...baseColumns];
+
+  tooltipColumns.forEach(col => {
+    if (!existingLabels.has(col)) {
+      result.push(col);
+      existingLabels.add(col);
+    }
+  });
+
+  return result;
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.test.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.test.ts
new file mode 100644
index 0000000000..c169ed4c33
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.test.ts
@@ -0,0 +1,604 @@
+/**
+ * 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 {
+  ChartProps,
+  DatasourceType,
+  QueryObjectFilterClause,
+  SupersetTheme,
+} from '@superset-ui/core';
+import { decode } from 'ngeohash';
+
+import {
+  getSpatialColumns,
+  addSpatialNullFilters,
+  buildSpatialQuery,
+  processSpatialData,
+  transformSpatialProps,
+  SpatialFormData,
+} from './spatialUtils';
+
+jest.mock('ngeohash', () => ({
+  decode: jest.fn(),
+}));
+
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  buildQueryContext: jest.fn(),
+  getMetricLabel: jest.fn(),
+  ensureIsArray: jest.fn(arr => arr || []),
+  normalizeOrderBy: jest.fn(({ orderby }) => ({ orderby })),
+}));
+
+// Mock DOM element for bootstrap data
+const mockBootstrapData = {
+  common: {
+    conf: {
+      MAPBOX_API_KEY: 'test_api_key',
+    },
+  },
+};
+
+Object.defineProperty(document, 'getElementById', {
+  value: jest.fn().mockReturnValue({
+    getAttribute: jest.fn().mockReturnValue(JSON.stringify(mockBootstrapData)),
+  }),
+  writable: true,
+});
+
+const mockDecode = decode as jest.MockedFunction<typeof decode>;
+
+describe('spatialUtils', () => {
+  test('getSpatialColumns returns correct columns for latlong type', () => {
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'latlong',
+      lonCol: 'longitude',
+      latCol: 'latitude',
+    };
+
+    const result = getSpatialColumns(spatial);
+    expect(result).toEqual(['longitude', 'latitude']);
+  });
+
+  test('getSpatialColumns returns correct columns for delimited type', () => {
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'delimited',
+      lonlatCol: 'coordinates',
+    };
+
+    const result = getSpatialColumns(spatial);
+    expect(result).toEqual(['coordinates']);
+  });
+
+  test('getSpatialColumns returns correct columns for geohash type', () => {
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'geohash',
+      geohashCol: 'geohash_code',
+    };
+
+    const result = getSpatialColumns(spatial);
+    expect(result).toEqual(['geohash_code']);
+  });
+
+  test('getSpatialColumns throws error when spatial is null', () => {
+    expect(() => getSpatialColumns(null as any)).toThrow('Bad spatial key');
+  });
+
+  test('getSpatialColumns throws error when spatial type is missing', () => {
+    const spatial = {} as SpatialFormData['spatial'];
+    expect(() => getSpatialColumns(spatial)).toThrow('Bad spatial key');
+  });
+
+  test('getSpatialColumns throws error when latlong columns are missing', () 
=> {
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'latlong',
+    };
+    expect(() => getSpatialColumns(spatial)).toThrow(
+      'Longitude and latitude columns are required for latlong type',
+    );
+  });
+
+  test('getSpatialColumns throws error when delimited column is missing', () 
=> {
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'delimited',
+    };
+    expect(() => getSpatialColumns(spatial)).toThrow(
+      'Longitude/latitude column is required for delimited type',
+    );
+  });
+
+  test('getSpatialColumns throws error when geohash column is missing', () => {
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'geohash',
+    };
+    expect(() => getSpatialColumns(spatial)).toThrow(
+      'Geohash column is required for geohash type',
+    );
+  });
+
+  test('getSpatialColumns throws error for unknown spatial type', () => {
+    const spatial = {
+      type: 'unknown',
+    } as any;
+    expect(() => getSpatialColumns(spatial)).toThrow(
+      'Unknown spatial type: unknown',
+    );
+  });
+
+  test('addSpatialNullFilters adds null filters for spatial columns', () => {
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'latlong',
+      lonCol: 'longitude',
+      latCol: 'latitude',
+    };
+    const existingFilters: QueryObjectFilterClause[] = [
+      { col: 'other_col', op: '==', val: 'test' },
+    ];
+
+    const result = addSpatialNullFilters(spatial, existingFilters);
+
+    expect(result).toEqual([
+      { col: 'other_col', op: '==', val: 'test' },
+      { col: 'longitude', op: 'IS NOT NULL', val: null },
+      { col: 'latitude', op: 'IS NOT NULL', val: null },
+    ]);
+  });
+
+  test('addSpatialNullFilters returns original filters when spatial is null', 
() => {
+    const existingFilters: QueryObjectFilterClause[] = [
+      { col: 'test_col', op: '==', val: 'test' },
+    ];
+
+    const result = addSpatialNullFilters(null as any, existingFilters);
+    expect(result).toBe(existingFilters);
+  });
+
+  test('addSpatialNullFilters works with empty filters array', () => {
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'delimited',
+      lonlatCol: 'coordinates',
+    };
+
+    const result = addSpatialNullFilters(spatial, []);
+
+    expect(result).toEqual([
+      { col: 'coordinates', op: 'IS NOT NULL', val: null },
+    ]);
+  });
+
+  test('buildSpatialQuery throws error when spatial is missing', () => {
+    const formData = {} as SpatialFormData;
+
+    expect(() => buildSpatialQuery(formData)).toThrow(
+      'Spatial configuration is required for this chart',
+    );
+  });
+
+  test('buildSpatialQuery calls buildQueryContext with correct parameters', () 
=> {
+    const mockBuildQueryContext =
+      jest.requireMock('@superset-ui/core').buildQueryContext;
+    const formData: SpatialFormData = {
+      spatial: {
+        type: 'latlong',
+        lonCol: 'longitude',
+        latCol: 'latitude',
+      },
+      size: 'count',
+      js_columns: ['extra_col'],
+    } as SpatialFormData;
+
+    buildSpatialQuery(formData);
+
+    expect(mockBuildQueryContext).toHaveBeenCalledWith(formData, {
+      buildQuery: expect.any(Function),
+    });
+  });
+
+  test('processSpatialData processes latlong data correctly', () => {
+    const records = [
+      { longitude: -122.4, latitude: 37.8, count: 10, extra: 'test1' },
+      { longitude: -122.5, latitude: 37.9, count: 20, extra: 'test2' },
+    ];
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'latlong',
+      lonCol: 'longitude',
+      latCol: 'latitude',
+    };
+    const metricLabel = 'count';
+    const jsColumns = ['extra'];
+
+    const result = processSpatialData(records, spatial, metricLabel, 
jsColumns);
+
+    expect(result).toHaveLength(2);
+    expect(result[0]).toEqual({
+      position: [-122.4, 37.8],
+      weight: 10,
+      extraProps: { extra: 'test1' },
+    });
+    expect(result[1]).toEqual({
+      position: [-122.5, 37.9],
+      weight: 20,
+      extraProps: { extra: 'test2' },
+    });
+  });
+
+  test('processSpatialData processes delimited data correctly', () => {
+    const records = [
+      { coordinates: '-122.4,37.8', count: 15 },
+      { coordinates: '-122.5,37.9', count: 25 },
+    ];
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'delimited',
+      lonlatCol: 'coordinates',
+    };
+
+    const result = processSpatialData(records, spatial, 'count');
+
+    expect(result).toHaveLength(2);
+    expect(result[0]).toEqual({
+      position: [-122.4, 37.8],
+      weight: 15,
+      extraProps: {},
+    });
+  });
+
+  test('processSpatialData processes geohash data correctly', () => {
+    mockDecode.mockReturnValue({
+      latitude: 37.8,
+      longitude: -122.4,
+      error: {
+        latitude: 0,
+        longitude: 0,
+      },
+    });
+
+    const records = [{ geohash: 'dr5regw3p', count: 30 }];
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'geohash',
+      geohashCol: 'geohash',
+    };
+
+    const result = processSpatialData(records, spatial, 'count');
+
+    expect(result).toHaveLength(1);
+    expect(result[0]).toEqual({
+      position: [-122.4, 37.8],
+      weight: 30,
+      extraProps: {},
+    });
+    expect(mockDecode).toHaveBeenCalledWith('dr5regw3p');
+  });
+
+  test('processSpatialData reverses coordinates when reverseCheckbox is true', 
() => {
+    const records = [{ longitude: -122.4, latitude: 37.8, count: 10 }];
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'latlong',
+      lonCol: 'longitude',
+      latCol: 'latitude',
+      reverseCheckbox: true,
+    };
+
+    const result = processSpatialData(records, spatial, 'count');
+
+    expect(result[0].position).toEqual([37.8, -122.4]);
+  });
+
+  test('processSpatialData handles invalid coordinates', () => {
+    const records = [
+      { longitude: 'invalid', latitude: 37.8, count: 10 },
+      { longitude: -122.4, latitude: NaN, count: 20 },
+      // 'latlong' spatial type expects longitude/latitude fields
+      // so records with 'coordinates' should be filtered out
+      { coordinates: 'invalid,coords', count: 30 },
+      { coordinates: '-122.4,invalid', count: 40 },
+    ];
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'latlong',
+      lonCol: 'longitude',
+      latCol: 'latitude',
+    };
+
+    const result = processSpatialData(records, spatial, 'count');
+
+    expect(result).toHaveLength(0);
+  });
+
+  test('processSpatialData handles missing metric values', () => {
+    const records = [
+      { longitude: -122.4, latitude: 37.8, count: null },
+      { longitude: -122.5, latitude: 37.9 },
+      { longitude: -122.6, latitude: 38.0, count: 'invalid' },
+    ];
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'latlong',
+      lonCol: 'longitude',
+      latCol: 'latitude',
+    };
+
+    const result = processSpatialData(records, spatial, 'count');
+
+    expect(result).toHaveLength(3);
+    expect(result[0].weight).toBe(1);
+    expect(result[1].weight).toBe(1);
+    expect(result[2].weight).toBe(1);
+  });
+
+  test('processSpatialData returns empty array for empty records', () => {
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'latlong',
+      lonCol: 'longitude',
+      latCol: 'latitude',
+    };
+
+    const result = processSpatialData([], spatial);
+
+    expect(result).toEqual([]);
+  });
+
+  test('processSpatialData returns empty array when spatial is null', () => {
+    const records = [{ longitude: -122.4, latitude: 37.8 }];
+
+    const result = processSpatialData(records, null as any);
+
+    expect(result).toEqual([]);
+  });
+
+  test('processSpatialData handles delimited coordinate edge cases', () => {
+    const records = [
+      { coordinates: '', count: 10 },
+      { coordinates: null, count: 20 },
+      { coordinates: undefined, count: 30 },
+      { coordinates: '-122.4', count: 40 }, // only one coordinate
+      { coordinates: 'a,b', count: 50 }, // non-numeric
+      { coordinates: '  -122.4  ,  37.8  ', count: 60 }, // with spaces
+    ];
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'delimited',
+      lonlatCol: 'coordinates',
+    };
+
+    const result = processSpatialData(records, spatial, 'count');
+
+    expect(result).toHaveLength(1);
+    expect(result[0]).toEqual({
+      position: [-122.4, 37.8],
+      weight: 60,
+      extraProps: {},
+    });
+  });
+
+  test('processSpatialData copies additional properties correctly', () => {
+    const records = [
+      {
+        longitude: -122.4,
+        latitude: 37.8,
+        count: 10,
+        category: 'A',
+        description: 'Test location',
+        extra_col: 'extra_value',
+      },
+    ];
+    const spatial: SpatialFormData['spatial'] = {
+      type: 'latlong',
+      lonCol: 'longitude',
+      latCol: 'latitude',
+    };
+    const jsColumns = ['extra_col'];
+
+    const result = processSpatialData(records, spatial, 'count', jsColumns);
+
+    expect(result[0]).toEqual({
+      position: [-122.4, 37.8],
+      weight: 10,
+      extraProps: { extra_col: 'extra_value' },
+      category: 'A',
+      description: 'Test location',
+    });
+
+    expect(result[0]).not.toHaveProperty('longitude');
+    expect(result[0]).not.toHaveProperty('latitude');
+    expect(result[0]).not.toHaveProperty('count');
+    expect(result[0]).not.toHaveProperty('extra_col');
+  });
+
+  test('transformSpatialProps transforms chart props correctly', () => {
+    const mockGetMetricLabel =
+      jest.requireMock('@superset-ui/core').getMetricLabel;
+    mockGetMetricLabel.mockReturnValue('count_label');
+
+    const chartProps: ChartProps = {
+      datasource: {
+        id: 1,
+        type: DatasourceType.Table,
+        columns: [],
+        name: '',
+        metrics: [],
+      },
+      height: 400,
+      width: 600,
+      hooks: {
+        onAddFilter: jest.fn(),
+        onContextMenu: jest.fn(),
+        setControlValue: jest.fn(),
+        setDataMask: jest.fn(),
+      },
+      queriesData: [
+        {
+          data: [
+            { longitude: -122.4, latitude: 37.8, count: 10 },
+            { longitude: -122.5, latitude: 37.9, count: 20 },
+          ],
+        },
+      ],
+      rawFormData: {
+        spatial: {
+          type: 'latlong',
+          lonCol: 'longitude',
+          latCol: 'latitude',
+        },
+        size: 'count',
+        js_columns: [],
+        viewport: {
+          zoom: 10,
+          latitude: 37.8,
+          longitude: -122.4,
+        },
+      } as unknown as SpatialFormData,
+      filterState: {},
+      emitCrossFilters: true,
+      annotationData: {},
+      rawDatasource: {},
+      initialValues: {},
+      formData: {
+        spatial: {
+          type: 'latlong',
+          lonCol: 'longitude',
+          latCol: 'latitude',
+        },
+        size: 'count',
+        js_columns: [],
+        viewport: {
+          zoom: 10,
+          latitude: 37.8,
+          longitude: -122.4,
+        },
+      },
+      ownState: {},
+      behaviors: [],
+      theme: {} as unknown as SupersetTheme,
+    };
+
+    const result = transformSpatialProps(chartProps);
+
+    expect(result).toMatchObject({
+      datasource: chartProps.datasource,
+      emitCrossFilters: chartProps.emitCrossFilters,
+      formData: chartProps.rawFormData,
+      height: 400,
+      width: 600,
+      filterState: {},
+      onAddFilter: chartProps.hooks.onAddFilter,
+      onContextMenu: chartProps.hooks.onContextMenu,
+      setControlValue: chartProps.hooks.setControlValue,
+      setDataMask: chartProps.hooks.setDataMask,
+      viewport: {
+        zoom: 10,
+        latitude: 37.8,
+        longitude: -122.4,
+        height: 400,
+        width: 600,
+      },
+    });
+
+    expect(result.payload.data.features).toHaveLength(2);
+    expect(result.payload.data.mapboxApiKey).toBe('test_api_key');
+    expect(result.payload.data.metricLabels).toEqual(['count_label']);
+  });
+
+  test('transformSpatialProps handles missing hooks gracefully', () => {
+    const chartProps: ChartProps = {
+      datasource: {
+        id: 1,
+        type: DatasourceType.Table,
+        columns: [],
+        name: '',
+        metrics: [],
+      },
+      height: 400,
+      width: 600,
+      hooks: {},
+      queriesData: [{ data: [] }],
+      rawFormData: {
+        spatial: {
+          type: 'latlong',
+          lonCol: 'longitude',
+          latCol: 'latitude',
+        },
+      } as SpatialFormData,
+      filterState: {},
+      emitCrossFilters: true,
+      annotationData: {},
+      rawDatasource: {},
+      initialValues: {},
+      formData: {
+        spatial: {
+          type: 'latlong',
+          lonCol: 'longitude',
+          latCol: 'latitude',
+        },
+      },
+      ownState: {},
+      behaviors: [],
+      theme: {} as unknown as SupersetTheme,
+    };
+
+    const result = transformSpatialProps(chartProps);
+
+    expect(typeof result.onAddFilter).toBe('function');
+    expect(typeof result.onContextMenu).toBe('function');
+    expect(typeof result.setControlValue).toBe('function');
+    expect(typeof result.setDataMask).toBe('function');
+    expect(typeof result.setTooltip).toBe('function');
+  });
+
+  test('transformSpatialProps handles missing metric', () => {
+    const mockGetMetricLabel =
+      jest.requireMock('@superset-ui/core').getMetricLabel;
+    mockGetMetricLabel.mockReturnValue(undefined);
+
+    const chartProps: ChartProps = {
+      datasource: {
+        id: 1,
+        type: DatasourceType.Table,
+        columns: [],
+        name: '',
+        metrics: [],
+      },
+      height: 400,
+      width: 600,
+      hooks: {},
+      queriesData: [{ data: [] }],
+      rawFormData: {
+        spatial: {
+          type: 'latlong',
+          lonCol: 'longitude',
+          latCol: 'latitude',
+        },
+      } as SpatialFormData,
+      filterState: {},
+      emitCrossFilters: true,
+      annotationData: {},
+      rawDatasource: {},
+      initialValues: {},
+      formData: {
+        spatial: {
+          type: 'latlong',
+          lonCol: 'longitude',
+          latCol: 'latitude',
+        },
+      },
+      ownState: {},
+      behaviors: [],
+      theme: {} as unknown as SupersetTheme,
+    };
+
+    const result = transformSpatialProps(chartProps);
+
+    expect(result.payload.data.metricLabels).toEqual([]);
+  });
+});
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.ts
new file mode 100644
index 0000000000..28625c5872
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.ts
@@ -0,0 +1,400 @@
+/**
+ * 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 {
+  buildQueryContext,
+  getMetricLabel,
+  QueryFormData,
+  QueryObjectFilterClause,
+  ensureIsArray,
+  ChartProps,
+  normalizeOrderBy,
+} from '@superset-ui/core';
+import { decode } from 'ngeohash';
+import { addTooltipColumnsToQuery } from './buildQueryUtils';
+
+export interface SpatialConfiguration {
+  type: 'latlong' | 'delimited' | 'geohash';
+  lonCol?: string;
+  latCol?: string;
+  lonlatCol?: string;
+  geohashCol?: string;
+  reverseCheckbox?: boolean;
+}
+
+export interface DataRecord {
+  [key: string]: string | number | null | undefined;
+}
+
+export interface BootstrapData {
+  common?: {
+    conf?: {
+      MAPBOX_API_KEY?: string;
+    };
+  };
+}
+
+export interface SpatialFormData extends QueryFormData {
+  spatial: SpatialConfiguration;
+  size?: string;
+  grid_size?: number;
+  js_data_mutator?: string;
+  js_agg_function?: string;
+  js_columns?: string[];
+  color_scheme?: string;
+  color_scheme_type?: string;
+  color_breakpoints?: number[];
+  default_breakpoint_color?: string;
+  tooltip_contents?: unknown[];
+  tooltip_template?: string;
+  color_picker?: string;
+}
+
+export interface SpatialPoint {
+  position: [number, number];
+  weight: number;
+  extraProps?: Record<string, unknown>;
+  [key: string]: unknown;
+}
+
+export function getSpatialColumns(spatial: SpatialConfiguration): string[] {
+  if (!spatial || !spatial.type) {
+    throw new Error('Bad spatial key');
+  }
+
+  switch (spatial.type) {
+    case 'latlong':
+      if (!spatial.lonCol || !spatial.latCol) {
+        throw new Error(
+          'Longitude and latitude columns are required for latlong type',
+        );
+      }
+      return [spatial.lonCol, spatial.latCol];
+    case 'delimited':
+      if (!spatial.lonlatCol) {
+        throw new Error(
+          'Longitude/latitude column is required for delimited type',
+        );
+      }
+      return [spatial.lonlatCol];
+    case 'geohash':
+      if (!spatial.geohashCol) {
+        throw new Error('Geohash column is required for geohash type');
+      }
+      return [spatial.geohashCol];
+    default:
+      throw new Error(`Unknown spatial type: ${spatial.type}`);
+  }
+}
+
+export function addSpatialNullFilters(
+  spatial: SpatialConfiguration,
+  filters: QueryObjectFilterClause[],
+): QueryObjectFilterClause[] {
+  if (!spatial) return filters;
+
+  const spatialColumns = getSpatialColumns(spatial);
+  const nullFilters: QueryObjectFilterClause[] = spatialColumns.map(column => 
({
+    col: column,
+    op: 'IS NOT NULL',
+    val: null,
+  }));
+
+  return [...filters, ...nullFilters];
+}
+
+export function buildSpatialQuery(formData: SpatialFormData) {
+  const { spatial, size: metric, js_columns, tooltip_contents } = formData;
+
+  if (!spatial) {
+    throw new Error(`Spatial configuration is required for this chart`);
+  }
+  return buildQueryContext(formData, {
+    buildQuery: baseQueryObject => {
+      const spatialColumns = getSpatialColumns(spatial);
+      let columns = [...(baseQueryObject.columns || []), ...spatialColumns];
+      const metrics = metric ? [metric] : [];
+
+      if (js_columns?.length) {
+        js_columns.forEach(col => {
+          if (!columns.includes(col)) {
+            columns.push(col);
+          }
+        });
+      }
+
+      columns = addTooltipColumnsToQuery(columns, tooltip_contents);
+
+      const filters = addSpatialNullFilters(
+        spatial,
+        ensureIsArray(baseQueryObject.filters || []),
+      );
+
+      const orderby = metric
+        ? normalizeOrderBy({ orderby: [[metric, false]] }).orderby
+        : baseQueryObject.orderby;
+
+      return [
+        {
+          ...baseQueryObject,
+          columns,
+          metrics,
+          filters,
+          orderby,
+          is_timeseries: false,
+          row_limit: baseQueryObject.row_limit,
+        },
+      ];
+    },
+  });
+}
+
+function parseCoordinates(latlong: string): [number, number] | null {
+  if (!latlong || typeof latlong !== 'string') {
+    return null;
+  }
+
+  try {
+    const coords = latlong.split(',').map(coord => parseFloat(coord.trim()));
+    if (
+      coords.length === 2 &&
+      !Number.isNaN(coords[0]) &&
+      !Number.isNaN(coords[1])
+    ) {
+      return [coords[0], coords[1]];
+    }
+    return null;
+  } catch (error) {
+    return null;
+  }
+}
+
+function reverseGeohashDecode(geohashCode: string): [number, number] | null {
+  if (!geohashCode || typeof geohashCode !== 'string') {
+    return null;
+  }
+
+  try {
+    const { latitude: lat, longitude: lng } = decode(geohashCode);
+    if (
+      Number.isNaN(lat) ||
+      Number.isNaN(lng) ||
+      lat < -90 ||
+      lat > 90 ||
+      lng < -180 ||
+      lng > 180
+    ) {
+      return null;
+    }
+    return [lng, lat];
+  } catch (error) {
+    return null;
+  }
+}
+
+export function addJsColumnsToExtraProps<
+  T extends { extraProps?: Record<string, unknown> },
+>(feature: T, record: DataRecord, jsColumns?: string[]): T {
+  if (!jsColumns?.length) {
+    return feature;
+  }
+
+  const extraProps: Record<string, unknown> = { ...(feature.extraProps ?? {}) 
};
+
+  jsColumns.forEach(col => {
+    if (record[col] !== undefined) {
+      extraProps[col] = record[col];
+    }
+  });
+
+  return { ...feature, extraProps };
+}
+
+export function processSpatialData(
+  records: DataRecord[],
+  spatial: SpatialConfiguration,
+  metricLabel?: string,
+  jsColumns?: string[],
+): SpatialPoint[] {
+  if (!spatial || !records.length) {
+    return [];
+  }
+
+  const features: SpatialPoint[] = [];
+  const spatialColumns = getSpatialColumns(spatial);
+  const jsColumnsSet = jsColumns ? new Set(jsColumns) : null;
+  const spatialColumnsSet = new Set(spatialColumns);
+
+  for (const record of records) {
+    let position: [number, number] | null = null;
+
+    switch (spatial.type) {
+      case 'latlong':
+        if (spatial.lonCol && spatial.latCol) {
+          const lon = parseFloat(String(record[spatial.lonCol] ?? ''));
+          const lat = parseFloat(String(record[spatial.latCol] ?? ''));
+          if (!Number.isNaN(lon) && !Number.isNaN(lat)) {
+            position = [lon, lat];
+          }
+        }
+        break;
+      case 'delimited':
+        if (spatial.lonlatCol) {
+          position = parseCoordinates(String(record[spatial.lonlatCol] ?? ''));
+        }
+        break;
+      case 'geohash':
+        if (spatial.geohashCol) {
+          const geohashValue = record[spatial.geohashCol];
+          if (geohashValue) {
+            position = reverseGeohashDecode(String(geohashValue));
+          }
+        }
+        break;
+      default:
+        continue;
+    }
+
+    if (!position) {
+      continue;
+    }
+
+    if (spatial.reverseCheckbox) {
+      position = [position[1], position[0]];
+    }
+
+    let weight = 1;
+    if (metricLabel && record[metricLabel] != null) {
+      const metricValue = parseFloat(String(record[metricLabel]));
+      if (!Number.isNaN(metricValue)) {
+        weight = metricValue;
+      }
+    }
+
+    let spatialPoint: SpatialPoint = {
+      position,
+      weight,
+      extraProps: {},
+    };
+
+    spatialPoint = addJsColumnsToExtraProps(spatialPoint, record, jsColumns);
+    Object.keys(record).forEach(key => {
+      if (spatialColumnsSet.has(key)) {
+        return;
+      }
+
+      if (key === metricLabel) {
+        return;
+      }
+
+      if (jsColumnsSet?.has(key)) {
+        return;
+      }
+
+      spatialPoint[key] = record[key];
+    });
+
+    features.push(spatialPoint);
+  }
+
+  return features;
+}
+
+const NOOP = () => {};
+
+export function getMapboxApiKey(mapboxApiKey?: string): string {
+  if (mapboxApiKey) {
+    return mapboxApiKey;
+  }
+
+  if (typeof document !== 'undefined') {
+    try {
+      const appContainer = document.getElementById('app');
+      const dataBootstrap = appContainer?.getAttribute('data-bootstrap');
+      if (dataBootstrap) {
+        const bootstrapData: BootstrapData = JSON.parse(dataBootstrap);
+        return bootstrapData?.common?.conf?.MAPBOX_API_KEY || '';
+      }
+    } catch (error) {
+      throw new Error(
+        `Failed to read MAPBOX_API_KEY from bootstrap data: ${error}`,
+      );
+    }
+  }
+
+  return '';
+}
+
+export function transformSpatialProps(chartProps: ChartProps) {
+  const {
+    datasource,
+    height,
+    hooks,
+    queriesData,
+    rawFormData: formData,
+    width,
+    filterState,
+    emitCrossFilters,
+  } = chartProps;
+
+  const {
+    onAddFilter = NOOP,
+    onContextMenu = NOOP,
+    setControlValue = NOOP,
+    setDataMask = NOOP,
+  } = hooks;
+
+  const { spatial, size: metric, js_columns } = formData as SpatialFormData;
+  const metricLabel = metric ? getMetricLabel(metric) : undefined;
+
+  const queryData = queriesData[0];
+  const records = queryData?.data || [];
+  const features = processSpatialData(
+    records,
+    spatial,
+    metricLabel,
+    js_columns,
+  );
+
+  return {
+    datasource,
+    emitCrossFilters,
+    formData,
+    height,
+    onAddFilter,
+    onContextMenu,
+    payload: {
+      ...queryData,
+      data: {
+        features,
+        mapboxApiKey: getMapboxApiKey(),
+        metricLabels: metricLabel ? [metricLabel] : [],
+      },
+    },
+    setControlValue,
+    filterState,
+    viewport: {
+      ...formData.viewport,
+      height,
+      width,
+    },
+    width,
+    setDataMask,
+    setTooltip: () => {},
+  };
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts
new file mode 100644
index 0000000000..6427db900a
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts
@@ -0,0 +1,142 @@
+/**
+ * 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 { ChartProps, getMetricLabel } from '@superset-ui/core';
+import { getMapboxApiKey, DataRecord } from './spatialUtils';
+
+const NOOP = () => {};
+
+export interface BaseHooks {
+  onAddFilter: ChartProps['hooks']['onAddFilter'];
+  onContextMenu: ChartProps['hooks']['onContextMenu'];
+  setControlValue: ChartProps['hooks']['setControlValue'];
+  setDataMask: ChartProps['hooks']['setDataMask'];
+}
+
+export interface BaseTransformPropsResult {
+  datasource: ChartProps['datasource'];
+  emitCrossFilters: ChartProps['emitCrossFilters'];
+  formData: ChartProps['rawFormData'];
+  height: ChartProps['height'];
+  onAddFilter: ChartProps['hooks']['onAddFilter'];
+  onContextMenu: ChartProps['hooks']['onContextMenu'];
+  payload: {
+    data: {
+      features: unknown[];
+      mapboxApiKey: string;
+      metricLabels?: string[];
+    };
+    [key: string]: unknown;
+  };
+  setControlValue: ChartProps['hooks']['setControlValue'];
+  filterState: ChartProps['filterState'];
+  viewport: {
+    height: number;
+    width: number;
+    [key: string]: unknown;
+  };
+  width: ChartProps['width'];
+  setDataMask: ChartProps['hooks']['setDataMask'];
+  setTooltip: () => void;
+}
+
+export function extractHooks(hooks: ChartProps['hooks']): BaseHooks {
+  return {
+    onAddFilter: hooks?.onAddFilter || NOOP,
+    onContextMenu: hooks?.onContextMenu || NOOP,
+    setControlValue: hooks?.setControlValue || NOOP,
+    setDataMask: hooks?.setDataMask || NOOP,
+  };
+}
+
+export function createBaseTransformResult(
+  chartProps: ChartProps,
+  features: unknown[],
+  metricLabels?: string[],
+): BaseTransformPropsResult {
+  const {
+    datasource,
+    height,
+    queriesData,
+    rawFormData: formData,
+    width,
+    filterState,
+    emitCrossFilters,
+  } = chartProps;
+
+  const hooks = extractHooks(chartProps.hooks);
+  const queryData = queriesData[0];
+
+  return {
+    datasource,
+    emitCrossFilters,
+    formData,
+    height,
+    ...hooks,
+    payload: {
+      ...queryData,
+      data: {
+        features,
+        mapboxApiKey: getMapboxApiKey(),
+        metricLabels: metricLabels || [],
+      },
+    },
+    filterState,
+    viewport: {
+      ...formData.viewport,
+      height,
+      width,
+    },
+    width,
+    setTooltip: NOOP,
+  };
+}
+
+export function getRecordsFromQuery(
+  queriesData: ChartProps['queriesData'],
+): DataRecord[] {
+  return queriesData[0]?.data || [];
+}
+
+export function parseMetricValue(value: unknown): number | undefined {
+  if (value == null) return undefined;
+  const parsed = parseFloat(String(value));
+  return Number.isNaN(parsed) ? undefined : parsed;
+}
+
+export function addPropertiesToFeature<T extends Record<string, unknown>>(
+  feature: T,
+  record: DataRecord,
+  excludeKeys: Set<string>,
+): T {
+  const result = { ...feature } as Record<string, unknown>;
+  Object.keys(record).forEach(key => {
+    if (!excludeKeys.has(key)) {
+      result[key] = record[key];
+    }
+  });
+  return result as T;
+}
+
+export function getMetricLabelFromFormData(
+  metric: string | { value?: string } | undefined,
+): string | undefined {
+  if (!metric) return undefined;
+  if (typeof metric === 'string') return getMetricLabel(metric);
+  return metric.value ? getMetricLabel(metric.value) : undefined;
+}
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
index be9359a616..be06ecaa15 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
@@ -615,7 +615,7 @@ export const deckGLColorBreakpointsSelect: 
CustomControlItem = {
 };
 
 export const breakpointsDefaultColor: CustomControlItem = {
-  name: 'deafult_breakpoint_color',
+  name: 'default_breakpoint_color',
   config: {
     label: t('Default color'),
     type: 'ColorPickerControl',
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utils/crossFiltersDataMask.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utils/crossFiltersDataMask.ts
index 0b63a1017f..95a8350d11 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utils/crossFiltersDataMask.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utils/crossFiltersDataMask.ts
@@ -73,7 +73,10 @@ export interface ValidatedPickingData {
   sourcePosition?: [number, number];
   targetPosition?: [number, number];
   path?: string;
-  geometry?: any;
+  geometry?: {
+    type: string;
+    coordinates: number[] | number[][] | number[][][];
+  };
 }
 
 const getFiltersBySpatialType = ({
@@ -96,7 +99,7 @@ const getFiltersBySpatialType = ({
     type,
     delimiter,
   } = spatialData;
-  let values: any[] = [];
+  let values: (string | number | [number, number] | [number, number][])[] = [];
   let filters: QueryObjectFilterClause[] = [];
   let customColumnLabel;
 
diff --git a/superset/views/base.py b/superset/views/base.py
index 1e6e12d2cc..b07d63f5cb 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -120,6 +120,7 @@ FRONTEND_CONF_KEYS = (
     "SQLLAB_QUERY_RESULT_TIMEOUT",
     "SYNC_DB_PERMISSIONS_IN_ASYNC_MODE",
     "TABLE_VIZ_MAX_ROW_SERVER",
+    "MAPBOX_API_KEY",
 )
 
 logger = logging.getLogger(__name__)
diff --git a/superset/views/core.py b/superset/views/core.py
index c495594216..d52a01f356 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -169,7 +169,9 @@ class Superset(BaseSupersetView):
             return json_error_response(payload=payload, status=400)
         return self.json_response(
             {
-                "data": payload["df"].to_dict("records"),
+                "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"),

Reply via email to