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

msyavuz 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 c76ddcbbec fix(deck.gl): Fix Scatterplot chart error when using fixed 
point size (#36890)
c76ddcbbec is described below

commit c76ddcbbecc68d457ee339c30259857dfeaf8696
Author: Mehmet Salih Yavuz <[email protected]>
AuthorDate: Mon Jan 5 15:55:43 2026 +0300

    fix(deck.gl): Fix Scatterplot chart error when using fixed point size 
(#36890)
    
    Co-authored-by: Claude <[email protected]>
---
 .../src/layers/Scatter/Scatter.tsx                 |  18 +-
 .../src/layers/Scatter/buildQuery.test.ts          | 312 +++++++++++++++++++++
 .../src/layers/Scatter/buildQuery.ts               |  26 +-
 .../src/layers/Scatter/transformProps.test.ts      | 303 ++++++++++++++++++++
 .../src/layers/Scatter/transformProps.ts           |  19 +-
 .../src/layers/transformUtils.test.ts              | 184 ++++++++++++
 .../src/layers/transformUtils.ts                   |  12 +-
 .../src/layers/utils/metricUtils.test.ts           | 121 ++++++++
 .../src/layers/utils/metricUtils.ts                | 120 ++++++++
 9 files changed, 1100 insertions(+), 15 deletions(-)

diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/Scatter.tsx
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/Scatter.tsx
index 3ecd3eac0c..2b72231626 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/Scatter.tsx
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/Scatter.tsx
@@ -25,6 +25,7 @@ import { createTooltipContent } from 
'../../utilities/tooltipUtils';
 import TooltipRow from '../../TooltipRow';
 import { unitToRadius } from '../../utils/geo';
 import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
+import { isMetricValue, extractMetricKey } from '../utils/metricUtils';
 
 function getMetricLabel(metric: any) {
   if (typeof metric === 'string') {
@@ -44,9 +45,18 @@ function setTooltipContent(
   verboseMap?: Record<string, string>,
 ) {
   const defaultTooltipGenerator = (o: JsonObject) => {
-    const label =
-      verboseMap?.[formData.point_radius_fixed.value] ||
-      getMetricLabel(formData.point_radius_fixed?.value);
+    // Only show metric info if point_radius_fixed is metric-based
+    let metricKey = null;
+    if (isMetricValue(formData.point_radius_fixed)) {
+      metricKey = extractMetricKey(formData.point_radius_fixed?.value);
+    }
+
+    // Normalize metricKey for verboseMap lookup
+    const lookupKey = typeof metricKey === 'string' ? metricKey : null;
+    const label = lookupKey
+      ? verboseMap?.[lookupKey] || getMetricLabel(lookupKey)
+      : null;
+
     return (
       <div className="deckgl-tooltip">
         <TooltipRow
@@ -59,7 +69,7 @@ function setTooltipContent(
             value={`${o.object?.cat_color}`}
           />
         )}
-        {o.object?.metric && (
+        {o.object?.metric && label && (
           <TooltipRow label={`${label}: `} value={`${o.object?.metric}`} />
         )}
       </div>
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.test.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.test.ts
new file mode 100644
index 0000000000..830d2c26b5
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.test.ts
@@ -0,0 +1,312 @@
+/**
+ * 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 buildQuery, { DeckScatterFormData } from './buildQuery';
+
+const baseFormData: DeckScatterFormData = {
+  datasource: '1__table',
+  viz_type: 'deck_scatter',
+  spatial: {
+    type: 'latlong',
+    latCol: 'LATITUDE',
+    lonCol: 'LONGITUDE',
+  },
+  row_limit: 100,
+};
+
+test('Scatter buildQuery should not include metric when point_radius_fixed is 
fixed type', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    point_radius_fixed: {
+      type: 'fix',
+      value: '1000',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.metrics).toEqual([]);
+  expect(query.orderby).toEqual([]);
+});
+
+test('Scatter buildQuery should include metric when point_radius_fixed is 
metric type', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    point_radius_fixed: {
+      type: 'metric',
+      value: 'AVG(radius_value)',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.metrics).toContain('AVG(radius_value)');
+  expect(query.orderby).toEqual([['AVG(radius_value)', false]]);
+});
+
+test('Scatter buildQuery should handle numeric value in fixed type', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    point_radius_fixed: {
+      type: 'fix',
+      value: 500,
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  // Fixed numeric value should not be included as a metric
+  expect(query.metrics).toEqual([]);
+  expect(query.orderby).toEqual([]);
+});
+
+test('Scatter buildQuery should handle missing point_radius_fixed', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    // no point_radius_fixed
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.metrics).toEqual([]);
+  expect(query.orderby).toEqual([]);
+});
+
+test('Scatter buildQuery should include spatial columns in query', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    point_radius_fixed: {
+      type: 'fix',
+      value: '1000',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.columns).toContain('LATITUDE');
+  expect(query.columns).toContain('LONGITUDE');
+});
+
+test('Scatter buildQuery should include dimension column when specified', () 
=> {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    dimension: 'category',
+    point_radius_fixed: {
+      type: 'fix',
+      value: '1000',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.columns).toContain('category');
+});
+
+test('Scatter buildQuery should add spatial null filters', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    point_radius_fixed: {
+      type: 'fix',
+      value: '1000',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  const latFilter = query.filters?.find(
+    f => f.col === 'LATITUDE' && f.op === 'IS NOT NULL',
+  );
+  const lonFilter = query.filters?.find(
+    f => f.col === 'LONGITUDE' && f.op === 'IS NOT NULL',
+  );
+
+  expect(latFilter).toBeDefined();
+  expect(lonFilter).toBeDefined();
+});
+
+test('Scatter buildQuery should throw error when spatial is missing', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    spatial: undefined,
+  };
+
+  expect(() => buildQuery(formData)).toThrow(
+    'Spatial configuration is required for Scatter charts',
+  );
+});
+
+test('Scatter buildQuery should handle geohash spatial type', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    spatial: {
+      type: 'geohash',
+      geohashCol: 'geohash_column',
+    },
+    point_radius_fixed: {
+      type: 'metric',
+      value: 'COUNT(*)',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.columns).toContain('geohash_column');
+  expect(query.metrics).toContain('COUNT(*)');
+});
+
+test('Scatter buildQuery should handle tooltip_contents', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    tooltip_contents: ['name', 'description'],
+    point_radius_fixed: {
+      type: 'fix',
+      value: '1000',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.columns).toContain('name');
+  expect(query.columns).toContain('description');
+});
+
+test('Scatter buildQuery should handle js_columns', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    js_columns: ['custom_col1', 'custom_col2'],
+    point_radius_fixed: {
+      type: 'fix',
+      value: '1000',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.columns).toContain('custom_col1');
+  expect(query.columns).toContain('custom_col2');
+});
+
+test('Scatter buildQuery should convert numeric metric value to string', () => 
{
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    point_radius_fixed: {
+      type: 'metric',
+      value: 123, // numeric metric (edge case)
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.metrics).toContain('123');
+  expect(query.orderby).toEqual([['123', false]]);
+});
+
+test('Scatter buildQuery should set is_timeseries to false', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    point_radius_fixed: {
+      type: 'fix',
+      value: '1000',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.is_timeseries).toBe(false);
+});
+
+test('Scatter buildQuery should preserve row_limit', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    row_limit: 5000,
+    point_radius_fixed: {
+      type: 'fix',
+      value: '1000',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.row_limit).toBe(5000);
+});
+
+test('Scatter buildQuery should preserve existing metrics when adding radius 
metric', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    metrics: ['COUNT(*)'],
+    point_radius_fixed: {
+      type: 'metric',
+      value: 'AVG(radius_value)',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.metrics).toContain('COUNT(*)');
+  expect(query.metrics).toContain('AVG(radius_value)');
+  expect(query.metrics).toHaveLength(2);
+});
+
+test('Scatter buildQuery should not modify existing metrics for fixed radius', 
() => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    metrics: ['COUNT(*)', 'SUM(value)'],
+    point_radius_fixed: {
+      type: 'fix',
+      value: '1000',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  expect(query.metrics).toEqual(['COUNT(*)', 'SUM(value)']);
+});
+
+test('Scatter buildQuery should deduplicate metrics when radius metric already 
exists', () => {
+  const formData: DeckScatterFormData = {
+    ...baseFormData,
+    metrics: ['COUNT(*)', 'AVG(price)'],
+    point_radius_fixed: {
+      type: 'metric',
+      value: 'AVG(price)',
+    },
+  };
+
+  const queryContext = buildQuery(formData);
+  const [query] = queryContext.queries;
+
+  // Should not have duplicate AVG(price)
+  expect(query.metrics).toEqual(['COUNT(*)', 'AVG(price)']);
+  expect(query.metrics).toHaveLength(2);
+});
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
index 598bbfdce9..e1976c6544 100644
--- 
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
@@ -34,11 +34,13 @@ import {
   processMetricsArray,
   addTooltipColumnsToQuery,
 } from '../buildQueryUtils';
+import { isMetricValue, extractMetricKey } from '../utils/metricUtils';
 
 export interface DeckScatterFormData
   extends Omit<SpatialFormData, 'color_picker'>, SqlaFormData {
   point_radius_fixed?: {
-    value?: string;
+    type?: 'fix' | 'metric';
+    value?: string | number;
   };
   multiplier?: number;
   point_unit?: string;
@@ -78,15 +80,29 @@ export default function buildQuery(formData: 
DeckScatterFormData) {
       columns = withJsColumns as QueryFormColumn[];
       columns = addTooltipColumnsToQuery(columns, tooltip_contents);
 
-      const metrics = processMetricsArray([point_radius_fixed?.value]);
+      // Only add metric if point_radius_fixed is a metric type
+      const isMetric = isMetricValue(point_radius_fixed);
+      const metricValue = isMetric
+        ? extractMetricKey(point_radius_fixed?.value)
+        : null;
+
+      // Preserve existing metrics and only add radius metric if it's 
metric-based
+      const existingMetrics = baseQueryObject.metrics || [];
+      const radiusMetrics = processMetricsArray(
+        metricValue ? [metricValue] : [],
+      );
+      // Deduplicate metrics to avoid adding the same metric twice
+      const metricsSet = new Set([...existingMetrics, ...radiusMetrics]);
+      const metrics = Array.from(metricsSet);
       const filters = addSpatialNullFilters(
         spatial,
         ensureIsArray(baseQueryObject.filters || []),
       );
 
-      const orderby = point_radius_fixed?.value
-        ? ([[point_radius_fixed.value, false]] as QueryFormOrderBy[])
-        : (baseQueryObject.orderby as QueryFormOrderBy[]) || [];
+      const orderby =
+        isMetric && metricValue
+          ? ([[metricValue, false]] as QueryFormOrderBy[])
+          : (baseQueryObject.orderby as QueryFormOrderBy[]) || [];
 
       return [
         {
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts
new file mode 100644
index 0000000000..bbdf7ced7b
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts
@@ -0,0 +1,303 @@
+/**
+ * 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 } from '@superset-ui/core';
+import transformProps from './transformProps';
+
+interface ScatterFeature {
+  position: [number, number];
+  radius?: number;
+  metric?: number;
+  cat_color?: string;
+  extraProps?: Record<string, unknown>;
+}
+
+jest.mock('../spatialUtils', () => ({
+  ...jest.requireActual('../spatialUtils'),
+  getMapboxApiKey: jest.fn(() => 'mock-mapbox-key'),
+}));
+
+const mockChartProps: Partial<ChartProps> = {
+  rawFormData: {
+    spatial: {
+      type: 'latlong',
+      latCol: 'LATITUDE',
+      lonCol: 'LONGITUDE',
+    },
+    viewport: {},
+  },
+  queriesData: [
+    {
+      data: [
+        {
+          LATITUDE: 37.8,
+          LONGITUDE: -122.4,
+          population: 50000,
+          'AVG(radius_value)': 100,
+        },
+        {
+          LATITUDE: 37.9,
+          LONGITUDE: -122.3,
+          population: 75000,
+          'AVG(radius_value)': 200,
+        },
+      ],
+    },
+  ],
+  datasource: {
+    type: DatasourceType.Table,
+    id: 1,
+    name: 'test_datasource',
+    columns: [],
+    metrics: [],
+  },
+  height: 400,
+  width: 600,
+  hooks: {},
+  filterState: {},
+  emitCrossFilters: false,
+};
+
+test('Scatter transformProps should use fixed radius value when 
point_radius_fixed type is "fix"', () => {
+  const fixedProps = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      point_radius_fixed: {
+        type: 'fix',
+        value: '1000',
+      },
+    },
+  };
+
+  const result = transformProps(fixedProps as ChartProps);
+  const features = result.payload.data.features as ScatterFeature[];
+
+  expect(features).toHaveLength(2);
+  expect(features[0]?.radius).toBe(1000);
+  expect(features[1]?.radius).toBe(1000);
+  // metric should not be set for fixed radius
+  expect(features[0]?.metric).toBeUndefined();
+  expect(features[1]?.metric).toBeUndefined();
+});
+
+test('Scatter transformProps should use metric value for radius when 
point_radius_fixed type is "metric"', () => {
+  const metricProps = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      point_radius_fixed: {
+        type: 'metric',
+        value: 'AVG(radius_value)',
+      },
+    },
+  };
+
+  const result = transformProps(metricProps as ChartProps);
+  const features = result.payload.data.features as ScatterFeature[];
+
+  expect(features).toHaveLength(2);
+  expect(features[0]?.radius).toBe(100);
+  expect(features[0]?.metric).toBe(100);
+  expect(features[1]?.radius).toBe(200);
+  expect(features[1]?.metric).toBe(200);
+});
+
+test('Scatter transformProps should handle numeric fixed radius value', () => {
+  const fixedProps = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      point_radius_fixed: {
+        type: 'fix',
+        value: 500, // numeric value
+      },
+    },
+  };
+
+  const result = transformProps(fixedProps as ChartProps);
+  const features = result.payload.data.features as ScatterFeature[];
+
+  expect(features).toHaveLength(2);
+  expect(features[0]?.radius).toBe(500);
+  expect(features[1]?.radius).toBe(500);
+});
+
+test('Scatter transformProps should handle missing point_radius_fixed', () => {
+  const propsWithoutRadius = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      // no point_radius_fixed
+    },
+  };
+
+  const result = transformProps(propsWithoutRadius as ChartProps);
+  const features = result.payload.data.features as ScatterFeature[];
+
+  expect(features).toHaveLength(2);
+  // radius should not be set
+  expect(features[0]?.radius).toBeUndefined();
+  expect(features[1]?.radius).toBeUndefined();
+});
+
+test('Scatter transformProps should handle dimension for category colors', () 
=> {
+  const propsWithDimension = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      dimension: 'category',
+      point_radius_fixed: {
+        type: 'fix',
+        value: '1000',
+      },
+    },
+    queriesData: [
+      {
+        data: [
+          {
+            LATITUDE: 37.8,
+            LONGITUDE: -122.4,
+            category: 'A',
+          },
+          {
+            LATITUDE: 37.9,
+            LONGITUDE: -122.3,
+            category: 'B',
+          },
+        ],
+      },
+    ],
+  };
+
+  const result = transformProps(propsWithDimension as ChartProps);
+  const features = result.payload.data.features as ScatterFeature[];
+
+  expect(features).toHaveLength(2);
+  expect(features[0]?.cat_color).toBe('A');
+  expect(features[1]?.cat_color).toBe('B');
+});
+
+test('Scatter transformProps should not include metric labels for fixed 
radius', () => {
+  const fixedProps = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      point_radius_fixed: {
+        type: 'fix',
+        value: '1000',
+      },
+    },
+  };
+
+  const result = transformProps(fixedProps as ChartProps);
+
+  // metricLabels should be empty for fixed radius
+  expect(result.payload.data.metricLabels).toEqual([]);
+});
+
+test('Scatter transformProps should include metric labels for metric-based 
radius', () => {
+  const metricProps = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      point_radius_fixed: {
+        type: 'metric',
+        value: 'AVG(radius_value)',
+      },
+    },
+  };
+
+  const result = transformProps(metricProps as ChartProps);
+
+  // metricLabels should include the metric name
+  expect(result.payload.data.metricLabels).toContain('AVG(radius_value)');
+});
+
+test('Scatter transformProps should handle no records', () => {
+  const noDataProps = {
+    ...mockChartProps,
+    queriesData: [{ data: [] }],
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      point_radius_fixed: {
+        type: 'fix',
+        value: '1000',
+      },
+    },
+  };
+
+  const result = transformProps(noDataProps as ChartProps);
+  const features = result.payload.data.features as ScatterFeature[];
+
+  expect(features).toHaveLength(0);
+});
+
+test('Scatter transformProps should handle missing spatial configuration 
gracefully', () => {
+  const noSpatialProps = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      spatial: undefined,
+      point_radius_fixed: {
+        type: 'fix',
+        value: '1000',
+      },
+    },
+  };
+
+  const result = transformProps(noSpatialProps as ChartProps);
+  const features = result.payload.data.features as ScatterFeature[];
+
+  expect(features).toHaveLength(0);
+});
+
+test('Scatter transformProps should preserve extra properties from records', 
() => {
+  const propsWithExtraData = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      point_radius_fixed: {
+        type: 'fix',
+        value: '1000',
+      },
+    },
+    queriesData: [
+      {
+        data: [
+          {
+            LATITUDE: 37.8,
+            LONGITUDE: -122.4,
+            custom_field: 'value1',
+            another_field: 123,
+          },
+        ],
+      },
+    ],
+  };
+
+  const result = transformProps(propsWithExtraData as ChartProps);
+  const features = result.payload.data.features as ScatterFeature[];
+
+  expect(features).toHaveLength(1);
+  expect(features[0]).toMatchObject({
+    custom_field: 'value1',
+    another_field: 123,
+  });
+});
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
index 7f4a300886..a168bd821e 100644
--- 
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
@@ -26,6 +26,7 @@ import {
   addPropertiesToFeature,
 } from '../transformUtils';
 import { DeckScatterFormData } from './buildQuery';
+import { isFixedValue, getFixedValue } from '../utils/metricUtils';
 
 interface ScatterPoint {
   position: [number, number];
@@ -43,6 +44,7 @@ function processScatterData(
   radiusMetricLabel?: string,
   categoryColumn?: string,
   jsColumns?: string[],
+  fixedRadiusValue?: number | string | null,
 ): ScatterPoint[] {
   if (!spatial || !records.length) {
     return [];
@@ -72,7 +74,15 @@ function processScatterData(
       extraProps: feature.extraProps || {},
     };
 
-    if (radiusMetricLabel && feature[radiusMetricLabel] != null) {
+    // Handle radius: either from metric or fixed value
+    if (fixedRadiusValue != null) {
+      // Use fixed radius value for all points
+      const parsedFixedRadius = parseMetricValue(fixedRadiusValue);
+      if (parsedFixedRadius !== undefined) {
+        scatterPoint.radius = parsedFixedRadius;
+      }
+    } else if (radiusMetricLabel && feature[radiusMetricLabel] != null) {
+      // Use metric value for radius
       const radiusValue = parseMetricValue(feature[radiusMetricLabel]);
       if (radiusValue !== undefined) {
         scatterPoint.radius = radiusValue;
@@ -98,14 +108,21 @@ export default function transformProps(chartProps: 
ChartProps) {
   const { spatial, point_radius_fixed, dimension, js_columns } =
     formData as DeckScatterFormData;
 
+  // Check if this is a fixed value or metric
+  const fixedRadiusValue = isFixedValue(point_radius_fixed)
+    ? getFixedValue(point_radius_fixed)
+    : null;
+
   const radiusMetricLabel = getMetricLabelFromFormData(point_radius_fixed);
   const records = getRecordsFromQuery(chartProps.queriesData);
+
   const features = processScatterData(
     records,
     spatial,
     radiusMetricLabel,
     dimension,
     js_columns,
+    fixedRadiusValue,
   );
 
   return createBaseTransformResult(
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.test.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.test.ts
new file mode 100644
index 0000000000..211bd9e6fd
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.test.ts
@@ -0,0 +1,184 @@
+/**
+ * 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 } from '@superset-ui/core';
+import { getMetricLabelFromFormData, parseMetricValue } from 
'./transformUtils';
+
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  getMetricLabel: jest.fn((metric: string) => metric),
+}));
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+test('getMetricLabelFromFormData should return undefined for undefined input', 
() => {
+  const result = getMetricLabelFromFormData(undefined);
+  expect(result).toBeUndefined();
+});
+
+test('getMetricLabelFromFormData should return undefined for null input', () 
=> {
+  const result = getMetricLabelFromFormData(null as any);
+  expect(result).toBeUndefined();
+});
+
+test('getMetricLabelFromFormData should handle string metric directly', () => {
+  const result = getMetricLabelFromFormData('AVG(value)');
+  expect(result).toBe('AVG(value)');
+  expect(getMetricLabel).toHaveBeenCalledWith('AVG(value)');
+});
+
+test('getMetricLabelFromFormData should return undefined for fixed type', () 
=> {
+  const result = getMetricLabelFromFormData({
+    type: 'fix',
+    value: '1000',
+  });
+  expect(result).toBeUndefined();
+  expect(getMetricLabel).not.toHaveBeenCalled();
+});
+
+test('getMetricLabelFromFormData should return undefined for fixed type with 
numeric value', () => {
+  const result = getMetricLabelFromFormData({
+    type: 'fix',
+    value: 1000,
+  });
+  expect(result).toBeUndefined();
+  expect(getMetricLabel).not.toHaveBeenCalled();
+});
+
+test('getMetricLabelFromFormData should return metric label for metric type 
with string value', () => {
+  const result = getMetricLabelFromFormData({
+    type: 'metric',
+    value: 'SUM(amount)',
+  });
+  expect(result).toBe('SUM(amount)');
+  expect(getMetricLabel).toHaveBeenCalledWith('SUM(amount)');
+});
+
+test('getMetricLabelFromFormData should handle object metric values', () => {
+  const result = getMetricLabelFromFormData({
+    type: 'metric',
+    value: {
+      label: 'Total Sales',
+      sqlExpression: 'SUM(sales)',
+    },
+  });
+  expect(result).toBe('Total Sales');
+  expect(getMetricLabel).toHaveBeenCalledWith('Total Sales');
+});
+
+test('getMetricLabelFromFormData should use sqlExpression if label is 
missing', () => {
+  const result = getMetricLabelFromFormData({
+    type: 'metric',
+    value: {
+      sqlExpression: 'COUNT(*)',
+    },
+  });
+  expect(result).toBe('COUNT(*)');
+  expect(getMetricLabel).toHaveBeenCalledWith('COUNT(*)');
+});
+
+test('getMetricLabelFromFormData should use value field as fallback', () => {
+  const result = getMetricLabelFromFormData({
+    type: 'metric',
+    value: {
+      value: 'AVG(price)',
+    },
+  });
+  expect(result).toBe('AVG(price)');
+  expect(getMetricLabel).toHaveBeenCalledWith('AVG(price)');
+});
+
+test('getMetricLabelFromFormData should handle metric type with numeric 
value', () => {
+  const result = getMetricLabelFromFormData({
+    type: 'metric',
+    value: 123,
+  });
+  expect(result).toBe('123');
+  expect(getMetricLabel).toHaveBeenCalledWith('123');
+});
+
+test('getMetricLabelFromFormData should return undefined for object without 
type', () => {
+  const result = getMetricLabelFromFormData({
+    value: 'AVG(value)',
+  });
+  expect(result).toBeUndefined();
+});
+
+test('getMetricLabelFromFormData should return undefined for empty object', () 
=> {
+  const result = getMetricLabelFromFormData({});
+  expect(result).toBeUndefined();
+});
+
+test('getMetricLabelFromFormData should return undefined for metric type 
without value', () => {
+  const result = getMetricLabelFromFormData({
+    type: 'metric',
+  });
+  expect(result).toBeUndefined();
+});
+
+test('parseMetricValue should parse numeric strings', () => {
+  expect(parseMetricValue('123')).toBe(123);
+  expect(parseMetricValue('123.45')).toBe(123.45);
+  expect(parseMetricValue('0')).toBe(0);
+  expect(parseMetricValue('-123')).toBe(-123);
+});
+
+test('parseMetricValue should handle numbers directly', () => {
+  expect(parseMetricValue(123)).toBe(123);
+  expect(parseMetricValue(123.45)).toBe(123.45);
+  expect(parseMetricValue(0)).toBe(0);
+  expect(parseMetricValue(-123)).toBe(-123);
+});
+
+test('parseMetricValue should return undefined for null', () => {
+  expect(parseMetricValue(null)).toBeUndefined();
+});
+
+test('parseMetricValue should return undefined for undefined', () => {
+  expect(parseMetricValue(undefined)).toBeUndefined();
+});
+
+test('parseMetricValue should return undefined for non-numeric strings', () => 
{
+  expect(parseMetricValue('abc')).toBeUndefined();
+  expect(parseMetricValue('12a34')).toBe(12); // parseFloat returns 12
+  expect(parseMetricValue('')).toBeUndefined();
+});
+
+test('parseMetricValue should handle edge cases', () => {
+  expect(parseMetricValue('Infinity')).toBe(Infinity);
+  expect(parseMetricValue('-Infinity')).toBe(-Infinity);
+  expect(parseMetricValue('NaN')).toBeUndefined();
+});
+
+test('parseMetricValue should handle boolean values', () => {
+  expect(parseMetricValue(true as any)).toBeUndefined();
+  expect(parseMetricValue(false as any)).toBeUndefined();
+});
+
+test('parseMetricValue should handle objects', () => {
+  expect(parseMetricValue({} as any)).toBeUndefined();
+  expect(parseMetricValue({ value: 123 } as any)).toBeUndefined();
+});
+
+test('parseMetricValue should handle arrays', () => {
+  expect(parseMetricValue([] as any)).toBeUndefined();
+  expect(parseMetricValue([123] as any)).toBe(123); // String([123]) = '123'
+});
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
index 6427db900a..7cea61183a 100644
--- 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts
@@ -16,8 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ChartProps, getMetricLabel } from '@superset-ui/core';
+import { ChartProps } from '@superset-ui/core';
 import { getMapboxApiKey, DataRecord } from './spatialUtils';
+import {
+  getMetricLabelFromValue,
+  FixedOrMetricValue,
+} from './utils/metricUtils';
 
 const NOOP = () => {};
 
@@ -134,9 +138,7 @@ export function addPropertiesToFeature<T extends 
Record<string, unknown>>(
 }
 
 export function getMetricLabelFromFormData(
-  metric: string | { value?: string } | undefined,
+  metric: string | FixedOrMetricValue | undefined | null,
 ): string | undefined {
-  if (!metric) return undefined;
-  if (typeof metric === 'string') return getMetricLabel(metric);
-  return metric.value ? getMetricLabel(metric.value) : undefined;
+  return getMetricLabelFromValue(metric);
 }
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/utils/metricUtils.test.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/utils/metricUtils.test.ts
new file mode 100644
index 0000000000..dfd5878e54
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/utils/metricUtils.test.ts
@@ -0,0 +1,121 @@
+/**
+ * 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 } from '@superset-ui/core';
+import {
+  isMetricValue,
+  isFixedValue,
+  extractMetricKey,
+  getMetricLabelFromValue,
+  getFixedValue,
+} from './metricUtils';
+
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  getMetricLabel: jest.fn((metric: string) => metric),
+}));
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+test('isMetricValue should identify metric values correctly', () => {
+  expect(isMetricValue({ type: 'metric', value: 'COUNT(*)' })).toBe(true);
+  expect(isMetricValue({ type: 'fix', value: '1000' })).toBe(false);
+  expect(isMetricValue('AVG(value)')).toBe(true); // legacy string format
+  expect(isMetricValue(undefined)).toBe(false);
+  expect(isMetricValue(null)).toBe(false);
+});
+
+test('isFixedValue should identify fixed values correctly', () => {
+  expect(isFixedValue({ type: 'fix', value: '1000' })).toBe(true);
+  expect(isFixedValue({ type: 'metric', value: 'COUNT(*)' })).toBe(false);
+  expect(isFixedValue('AVG(value)')).toBe(false); // legacy string format
+  expect(isFixedValue(undefined)).toBe(false);
+  expect(isFixedValue(null)).toBe(false);
+});
+
+test('extractMetricKey should handle string values', () => {
+  expect(extractMetricKey('COUNT(*)')).toBe('COUNT(*)');
+  expect(extractMetricKey('')).toBe('');
+});
+
+test('extractMetricKey should handle number values', () => {
+  expect(extractMetricKey(123)).toBe('123');
+  expect(extractMetricKey(0)).toBe('0');
+});
+
+test('extractMetricKey should extract from object properties', () => {
+  expect(extractMetricKey({ label: 'Total Sales' })).toBe('Total Sales');
+  expect(extractMetricKey({ sqlExpression: 'SUM(sales)' })).toBe('SUM(sales)');
+  expect(extractMetricKey({ value: 'AVG(price)' })).toBe('AVG(price)');
+  expect(
+    extractMetricKey({ label: 'Label', sqlExpression: 'SQL', value: 'Value' }),
+  ).toBe('Label'); // priority order
+});
+
+test('extractMetricKey should handle null/undefined', () => {
+  expect(extractMetricKey(null)).toBeUndefined();
+  expect(extractMetricKey(undefined)).toBeUndefined();
+  expect(extractMetricKey({})).toBeUndefined();
+});
+
+test('getMetricLabelFromValue should return label for metric values', () => {
+  getMetricLabelFromValue({ type: 'metric', value: 'COUNT(*)' });
+  expect(getMetricLabel).toHaveBeenCalledWith('COUNT(*)');
+
+  getMetricLabelFromValue({ type: 'metric', value: { label: 'Total Sales' } });
+  expect(getMetricLabel).toHaveBeenCalledWith('Total Sales');
+});
+
+test('getMetricLabelFromValue should return undefined for fixed values', () => 
{
+  const result = getMetricLabelFromValue({ type: 'fix', value: '1000' });
+  expect(result).toBeUndefined();
+  expect(getMetricLabel).not.toHaveBeenCalled();
+});
+
+test('getMetricLabelFromValue should handle legacy string format', () => {
+  getMetricLabelFromValue('AVG(value)');
+  expect(getMetricLabel).toHaveBeenCalledWith('AVG(value)');
+});
+
+test('getFixedValue should return value for fixed types', () => {
+  expect(getFixedValue({ type: 'fix', value: '1000' })).toBe('1000');
+  expect(getFixedValue({ type: 'fix', value: 500 })).toBe(500);
+});
+
+test('getFixedValue should return undefined for metric types', () => {
+  expect(getFixedValue({ type: 'metric', value: 'COUNT(*)' })).toBeUndefined();
+});
+
+test('getFixedValue should return undefined for string values', () => {
+  expect(getFixedValue('AVG(value)')).toBeUndefined();
+});
+
+test('getFixedValue should handle object values', () => {
+  expect(
+    getFixedValue({ type: 'fix', value: { label: 'object' } }),
+  ).toBeUndefined();
+});
+
+test('getFixedValue should handle missing values', () => {
+  expect(getFixedValue({ type: 'fix' })).toBeUndefined();
+  expect(getFixedValue(undefined)).toBeUndefined();
+  expect(getFixedValue(null)).toBeUndefined();
+});
diff --git 
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/utils/metricUtils.ts
 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/utils/metricUtils.ts
new file mode 100644
index 0000000000..688ef78e08
--- /dev/null
+++ 
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/utils/metricUtils.ts
@@ -0,0 +1,120 @@
+/**
+ * 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 } from '@superset-ui/core';
+
+export type MetricFormValue =
+  | string
+  | number
+  | { label?: string; sqlExpression?: string; value?: string }
+  | undefined
+  | null;
+
+export interface FixedOrMetricValue {
+  type?: 'fix' | 'metric';
+  value?: MetricFormValue;
+}
+
+/**
+ * Checks if a value is configured as a metric (vs fixed value)
+ */
+export function isMetricValue(
+  fixedOrMetric: string | FixedOrMetricValue | undefined | null,
+): boolean {
+  if (!fixedOrMetric) return false;
+  if (typeof fixedOrMetric === 'string') return true;
+  return fixedOrMetric.type === 'metric';
+}
+
+/**
+ * Checks if a value is configured as a fixed value (vs metric)
+ */
+export function isFixedValue(
+  fixedOrMetric: string | FixedOrMetricValue | undefined | null,
+): boolean {
+  if (!fixedOrMetric) return false;
+  if (typeof fixedOrMetric === 'string') return false;
+  return fixedOrMetric.type === 'fix';
+}
+
+/**
+ * Extracts the metric key from a metric value object
+ * Handles label, sqlExpression, and value properties
+ */
+export function extractMetricKey(value: MetricFormValue): string | undefined {
+  if (value == null) return undefined;
+  if (typeof value === 'string') return value;
+  if (typeof value === 'number') return String(value);
+
+  // Handle object metrics (adhoc or saved metrics)
+  const metricObj = value as {
+    label?: string;
+    sqlExpression?: string;
+    value?: string;
+  };
+  return metricObj.label || metricObj.sqlExpression || metricObj.value;
+}
+
+/**
+ * Gets the metric label from a fixed/metric form value
+ * Returns undefined for fixed values, metric label for metric values
+ */
+export function getMetricLabelFromValue(
+  fixedOrMetric: string | FixedOrMetricValue | undefined | null,
+): string | undefined {
+  if (!fixedOrMetric) return undefined;
+
+  // Legacy string format - treat as metric
+  if (typeof fixedOrMetric === 'string') {
+    return getMetricLabel(fixedOrMetric);
+  }
+
+  // Only return metric label if it's a metric type, not a fixed value
+  if (isMetricValue(fixedOrMetric) && fixedOrMetric.value) {
+    const metricKey = extractMetricKey(fixedOrMetric.value);
+    if (metricKey && typeof metricKey === 'string') {
+      return getMetricLabel(metricKey);
+    }
+  }
+
+  return undefined;
+}
+
+/**
+ * Gets the fixed value from a fixed/metric form value
+ * Returns the value for fixed types, undefined for metrics
+ */
+export function getFixedValue(
+  fixedOrMetric: string | FixedOrMetricValue | undefined | null,
+): string | number | undefined {
+  if (!fixedOrMetric || typeof fixedOrMetric === 'string') {
+    return undefined;
+  }
+
+  if (isFixedValue(fixedOrMetric) && fixedOrMetric.value) {
+    if (
+      typeof fixedOrMetric.value === 'string' ||
+      typeof fixedOrMetric.value === 'number'
+    ) {
+      return fixedOrMetric.value;
+    }
+  }
+
+  return undefined;
+}


Reply via email to