You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/05/15 02:01:42 UTC

(superset) branch feat/glyph-single-file updated: feat(glyph): consolidate deckgl Scatter layer to defineChart()

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

rusackas pushed a commit to branch feat/glyph-single-file
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/feat/glyph-single-file by this push:
     new 857ba7f855e feat(glyph): consolidate deckgl Scatter layer to defineChart()
857ba7f855e is described below

commit 857ba7f855e8cfd44e770aa5b43ff18846ef66d9
Author: Evan Rusackas <ev...@rusackas.com>
AuthorDate: Thu May 14 19:01:33 2026 -0700

    feat(glyph): consolidate deckgl Scatter layer to defineChart()
    
    Collapse multi-file plugin into single index.tsx. Scatter keeps its
    custom buildQuery (spatial config required, metric-or-fixed radius
    handling) and transformProps (radius from metric/fix value, category
    column, js columns).
    
    Also wires `onInit?: ControlPanelConfig['onInit']` through to
    defineChart so Scatter can clear time_grain_sqla and granularity on
    chart initialization. The onInit hook is now an optional metadata-level
    override surfaced in the generated ControlPanelConfig.
    
    Scatter.tsx component stays as sibling for Multi.
    
    Co-Authored-By: Claude Sonnet 4.6 <no...@anthropic.com>
---
 .../superset-ui-glyph-core/src/defineChart.tsx     |  15 +
 .../src/layers/Scatter/buildQuery.ts               | 134 -------
 .../src/layers/Scatter/controlPanel.ts             | 165 --------
 .../src/layers/Scatter/index.ts                    |  61 ---
 .../src/layers/Scatter/index.tsx                   | 438 +++++++++++++++++++++
 .../src/layers/Scatter/transformProps.ts           | 133 -------
 6 files changed, 453 insertions(+), 493 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx b/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx
index 7814e97a938..4cf1c35647a 100644
--- a/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx
+++ b/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx
@@ -265,6 +265,14 @@ export interface ChartDefinition<
    */
   formDataOverrides?: (formData: QueryFormData) => QueryFormData;
 
+  /**
+   * onInit hook - called once when the chart's controls are first initialized.
+   * Use to reset/override control values that should not persist from the
+   * dataset's defaults (e.g., clearing time_grain_sqla on charts that don't
+   * use it).
+   */
+  onInit?: ControlPanelConfig['onInit'];
+
   /**
    * Custom buildQuery function - use for charts that need post-processing operators
    * If not provided, a default query builder is generated from arguments
@@ -627,6 +635,7 @@ function generateControlPanel<TArgs extends ChartArguments>(
   controlOverrides?: ChartDefinition<TArgs>['controlOverrides'],
   additionalControlOverrides?: ChartDefinition<TArgs>['additionalControlOverrides'],
   formDataOverrides?: ChartDefinition<TArgs>['formDataOverrides'],
+  onInit?: ChartDefinition<TArgs>['onInit'],
   additionalSections?: ControlPanelSectionConfig[],
   prependSections?: ControlPanelSectionConfig[],
   chartOptionsTabOverride?: 'customize' | 'data',
@@ -753,6 +762,10 @@ function generateControlPanel<TArgs extends ChartArguments>(
     config.formDataOverrides = formDataOverrides;
   }
 
+  if (onInit) {
+    config.onInit = onInit;
+  }
+
   // Store raw glyph args for native rendering (bypasses expandControlConfig pipeline)
   config._glyphArgs = args;
 
@@ -973,6 +986,7 @@ export function defineChart<
     controlOverrides,
     additionalControlOverrides,
     formDataOverrides,
+    onInit,
     buildQuery: customBuildQuery,
     transform,
     render,
@@ -989,6 +1003,7 @@ export function defineChart<
     controlOverrides,
     additionalControlOverrides,
     formDataOverrides,
+    onInit,
     additionalSections,
     prependSections,
     chartOptionsTabOverride,
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.ts
deleted file mode 100644
index 231e2bf1f79..00000000000
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import {
-  buildQueryContext,
-  ensureIsArray,
-  getMetricLabel,
-  QueryFormMetric,
-  QueryFormOrderBy,
-  SqlaFormData,
-  QueryFormColumn,
-  QueryObject,
-} from '@superset-ui/core';
-import {
-  getSpatialColumns,
-  addSpatialNullFilters,
-  SpatialFormData,
-} from '../spatialUtils';
-import {
-  addJsColumnsToColumns,
-  addTooltipColumnsToQuery,
-} from '../buildQueryUtils';
-import { isMetricValue } from '../utils/metricUtils';
-
-export interface DeckScatterFormData
-  extends Omit<SpatialFormData, 'color_picker'>, SqlaFormData {
-  // Can be a string (legacy format) or an object with type and value
-  point_radius_fixed?:
-    | string // Legacy format: metric name directly
-    | {
-        type?: 'fix' | 'metric';
-        value?: QueryFormMetric | number;
-      };
-  multiplier?: number;
-  point_unit?: string;
-  min_radius?: number;
-  max_radius?: number;
-  color_picker?: { r: number; g: number; b: number; a: number };
-  dimension?: string;
-}
-
-export default function buildQuery(formData: DeckScatterFormData) {
-  const {
-    spatial,
-    point_radius_fixed,
-    dimension,
-    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 (dimension) {
-        columns.push(dimension);
-      }
-
-      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);
-
-      // Only add metric if point_radius_fixed is a metric type
-      const isMetric = isMetricValue(point_radius_fixed);
-      // Extract metric value: legacy string format or object with metric value
-      const rawValue =
-        typeof point_radius_fixed === 'string'
-          ? point_radius_fixed
-          : point_radius_fixed?.value;
-      const metricValue: QueryFormMetric | null =
-        isMetric && rawValue !== undefined && typeof rawValue !== 'number'
-          ? (rawValue as QueryFormMetric)
-          : null;
-
-      // Preserve existing metrics and only add radius metric if it's metric-based
-      const existingMetrics = baseQueryObject.metrics || [];
-      // Deduplicate metrics using getMetricLabel for comparison
-      const existingLabels = new Set(
-        existingMetrics.map(m => getMetricLabel(m)),
-      );
-      const metrics: QueryFormMetric[] =
-        metricValue && !existingLabels.has(getMetricLabel(metricValue))
-          ? [...existingMetrics, metricValue]
-          : existingMetrics;
-
-      const filters = addSpatialNullFilters(
-        spatial,
-        ensureIsArray(baseQueryObject.filters || []),
-      );
-
-      // orderby needs string label, not the full metric object
-      const orderby =
-        isMetric && metricValue
-          ? ([[getMetricLabel(metricValue), 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/preset-chart-deckgl/src/layers/Scatter/controlPanel.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.ts
deleted file mode 100644
index 20a788c82a3..00000000000
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.ts
+++ /dev/null
@@ -1,165 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { ControlPanelConfig } from '@superset-ui/chart-controls';
-import { t } from '@apache-superset/core/translation';
-import { validateNonEmpty } from '@superset-ui/core';
-import timeGrainSqlaAnimationOverrides from '../../utilities/controls';
-import {
-  filterNulls,
-  autozoom,
-  jsColumns,
-  jsDataMutator,
-  jsTooltip,
-  jsOnclickHref,
-  legendFormat,
-  legendPosition,
-  viewport,
-  spatial,
-  pointRadiusFixed,
-  multiplier,
-  mapboxStyle,
-  maplibreStyle,
-  mapProvider,
-  generateDeckGLColorSchemeControls,
-  tooltipContents,
-  tooltipTemplate,
-} from '../../utilities/Shared_DeckGL';
-
-const config: ControlPanelConfig = {
-  onInit: controlState => ({
-    ...controlState,
-    time_grain_sqla: {
-      ...controlState.time_grain_sqla,
-      value: null,
-    },
-    granularity: {
-      ...controlState.granularity,
-      value: null,
-    },
-  }),
-  controlPanelSections: [
-    {
-      label: t('Query'),
-      expanded: true,
-      controlSetRows: [
-        [spatial, null],
-        ['row_limit', filterNulls],
-        ['adhoc_filters'],
-        [tooltipContents],
-        [tooltipTemplate],
-      ],
-    },
-    {
-      label: t('Map'),
-      expanded: true,
-      controlSetRows: [
-        [mapProvider],
-        [mapboxStyle],
-        [maplibreStyle],
-        [autozoom, viewport],
-      ],
-    },
-    {
-      label: t('Point Size'),
-      controlSetRows: [
-        [pointRadiusFixed],
-        [
-          {
-            name: 'point_unit',
-            config: {
-              type: 'SelectControl',
-              label: t('Point Unit'),
-              default: 'square_m',
-              clearable: false,
-              choices: [
-                ['square_m', t('Square meters')],
-                ['square_km', t('Square kilometers')],
-                ['square_miles', t('Square miles')],
-                ['radius_m', t('Radius in meters')],
-                ['radius_km', t('Radius in kilometers')],
-                ['radius_miles', t('Radius in miles')],
-              ],
-              description: t(
-                'The unit of measure for the specified point radius',
-              ),
-            },
-          },
-        ],
-        [
-          {
-            name: 'min_radius',
-            config: {
-              type: 'TextControl',
-              label: t('Minimum Radius'),
-              isFloat: true,
-              validators: [validateNonEmpty],
-              renderTrigger: true,
-              default: 2,
-              description: t(
-                'Minimum radius size of the circle, in pixels. As the zoom level changes, this ' +
-                  'insures that the circle respects this minimum radius.',
-              ),
-            },
-          },
-          {
-            name: 'max_radius',
-            config: {
-              type: 'TextControl',
-              label: t('Maximum Radius'),
-              isFloat: true,
-              validators: [validateNonEmpty],
-              renderTrigger: true,
-              default: 250,
-              description: t(
-                'Maximum radius size of the circle, in pixels. As the zoom level changes, this ' +
-                  'insures that the circle respects this maximum radius.',
-              ),
-            },
-          },
-        ],
-        [multiplier, null],
-      ],
-    },
-    {
-      label: t('Point Color'),
-      controlSetRows: [
-        [legendPosition],
-        [legendFormat],
-        ...generateDeckGLColorSchemeControls({}),
-      ],
-    },
-    {
-      label: t('Advanced'),
-      controlSetRows: [
-        [jsColumns],
-        [jsDataMutator],
-        [jsTooltip],
-        [jsOnclickHref],
-      ],
-    },
-  ],
-  controlOverrides: {
-    size: {
-      validators: [],
-    },
-    time_grain_sqla: timeGrainSqlaAnimationOverrides,
-  },
-};
-
-export default config;
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.ts
deleted file mode 100644
index ee43b4a2749..00000000000
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { t } from '@apache-superset/core/translation';
-import { ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
-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'),
-  credits: ['https://uber.github.io/deck.gl'],
-  description: t(
-    'A map that takes rendering circles with a variable radius at latitude/longitude coordinates',
-  ),
-  name: t('deck.gl Scatterplot'),
-  thumbnail,
-  thumbnailDark,
-  exampleGallery: [{ url: example, urlDark: exampleDark }],
-  tags: [
-    t('deckGL'),
-    t('Comparison'),
-    t('Scatter'),
-    t('2D'),
-    t('Geo'),
-    t('Intensity'),
-    t('Density'),
-  ],
-  behaviors: [Behavior.InteractiveChart],
-});
-
-export default class ScatterChartPlugin extends ChartPlugin {
-  constructor() {
-    super({
-      buildQuery,
-      loadChart: () => import('./Scatter'),
-      controlPanel,
-      metadata,
-      transformProps,
-    });
-  }
-}
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.tsx
new file mode 100644
index 00000000000..596da9f8c48
--- /dev/null
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.tsx
@@ -0,0 +1,438 @@
+/**
+ * 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 { t } from '@apache-superset/core/translation';
+import {
+  Behavior,
+  buildQueryContext,
+  ChartProps,
+  ensureIsArray,
+  getMetricLabel,
+  QueryFormColumn,
+  QueryFormMetric,
+  QueryFormOrderBy,
+  QueryObject,
+  SqlaFormData,
+  validateNonEmpty,
+} from '@superset-ui/core';
+import { defineChart } from '@superset-ui/glyph-core';
+import ScatterComponent from './Scatter';
+import {
+  processSpatialData,
+  getSpatialColumns,
+  addSpatialNullFilters,
+  SpatialFormData,
+  DataRecord,
+} from '../spatialUtils';
+import {
+  createBaseTransformResult,
+  getRecordsFromQuery,
+  getMetricLabelFromFormData,
+  parseMetricValue,
+  addPropertiesToFeature,
+} from '../transformUtils';
+import {
+  addJsColumnsToColumns,
+  addTooltipColumnsToQuery,
+} from '../buildQueryUtils';
+import {
+  isMetricValue,
+  isFixedValue,
+  getFixedValue,
+} from '../utils/metricUtils';
+import timeGrainSqlaAnimationOverrides from '../../utilities/controls';
+import {
+  filterNulls,
+  autozoom,
+  jsColumns,
+  jsDataMutator,
+  jsTooltip,
+  jsOnclickHref,
+  legendFormat,
+  legendPosition,
+  viewport,
+  spatial,
+  pointRadiusFixed,
+  multiplier,
+  mapboxStyle,
+  maplibreStyle,
+  mapProvider,
+  generateDeckGLColorSchemeControls,
+  tooltipContents,
+  tooltipTemplate,
+} from '../../utilities/Shared_DeckGL';
+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';
+
+// ─── Types ───────────────────────────────────────────────────────────────────
+
+export interface DeckScatterFormData
+  extends Omit<SpatialFormData, 'color_picker'>,
+    SqlaFormData {
+  // Can be a string (legacy format) or an object with type and value
+  point_radius_fixed?:
+    | string // Legacy format: metric name directly
+    | {
+        type?: 'fix' | 'metric';
+        value?: QueryFormMetric | number;
+      };
+  multiplier?: number;
+  point_unit?: string;
+  min_radius?: number;
+  max_radius?: number;
+  color_picker?: { r: number; g: number; b: number; a: number };
+  dimension?: string;
+}
+
+interface ScatterPoint {
+  position: [number, number];
+  radius?: number;
+  color?: [number, number, number, number];
+  cat_color?: string;
+  metric?: number;
+  extraProps?: Record<string, unknown>;
+  [key: string]: unknown;
+}
+
+// ─── buildQuery ──────────────────────────────────────────────────────────────
+
+export function buildQuery(formData: DeckScatterFormData) {
+  const {
+    spatial: spatialCfg,
+    point_radius_fixed,
+    dimension,
+    js_columns,
+    tooltip_contents,
+  } = formData;
+
+  if (!spatialCfg) {
+    throw new Error('Spatial configuration is required for Scatter charts');
+  }
+
+  return buildQueryContext(formData, {
+    buildQuery: (baseQueryObject: QueryObject) => {
+      const spatialColumns = getSpatialColumns(spatialCfg);
+      let columns = [...(baseQueryObject.columns || []), ...spatialColumns];
+
+      if (dimension) {
+        columns.push(dimension);
+      }
+
+      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 isMetric = isMetricValue(point_radius_fixed);
+      const rawValue =
+        typeof point_radius_fixed === 'string'
+          ? point_radius_fixed
+          : point_radius_fixed?.value;
+      const metricValue: QueryFormMetric | null =
+        isMetric && rawValue !== undefined && typeof rawValue !== 'number'
+          ? (rawValue as QueryFormMetric)
+          : null;
+
+      const existingMetrics = baseQueryObject.metrics || [];
+      const existingLabels = new Set(
+        existingMetrics.map(m => getMetricLabel(m)),
+      );
+      const metrics: QueryFormMetric[] =
+        metricValue && !existingLabels.has(getMetricLabel(metricValue))
+          ? [...existingMetrics, metricValue]
+          : existingMetrics;
+
+      const filters = addSpatialNullFilters(
+        spatialCfg,
+        ensureIsArray(baseQueryObject.filters || []),
+      );
+
+      const orderby =
+        isMetric && metricValue
+          ? ([[getMetricLabel(metricValue), false]] as QueryFormOrderBy[])
+          : (baseQueryObject.orderby as QueryFormOrderBy[]) || [];
+
+      return [
+        {
+          ...baseQueryObject,
+          columns,
+          metrics,
+          filters,
+          orderby,
+          is_timeseries: false,
+          row_limit: baseQueryObject.row_limit,
+        },
+      ];
+    },
+  });
+}
+
+// ─── transformProps ──────────────────────────────────────────────────────────
+
+export function processScatterData(
+  records: DataRecord[],
+  spatialCfg: DeckScatterFormData['spatial'],
+  radiusMetricLabel?: string,
+  categoryColumn?: string,
+  jsCols?: string[],
+  fixedRadiusValue?: number | string | null,
+): ScatterPoint[] {
+  if (!spatialCfg || !records.length) {
+    return [];
+  }
+
+  const spatialFeatures = processSpatialData(records, spatialCfg);
+  const excludeKeys = new Set([
+    'position',
+    'weight',
+    'extraProps',
+    ...(spatialCfg
+      ? [
+          spatialCfg.lonCol,
+          spatialCfg.latCol,
+          spatialCfg.lonlatCol,
+          spatialCfg.geohashCol,
+        ].filter(Boolean)
+      : []),
+    radiusMetricLabel,
+    categoryColumn,
+    ...(jsCols || []),
+  ]);
+
+  return spatialFeatures.map(feature => {
+    let scatterPoint: ScatterPoint = {
+      position: feature.position,
+      extraProps: feature.extraProps || {},
+    };
+
+    if (fixedRadiusValue != null) {
+      const parsedFixedRadius = parseMetricValue(fixedRadiusValue);
+      if (parsedFixedRadius !== undefined) {
+        scatterPoint.radius = parsedFixedRadius;
+      }
+    } else 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;
+  });
+}
+
+function transformProps(chartProps: ChartProps) {
+  const { rawFormData: formData } = chartProps;
+  const {
+    spatial: spatialCfg,
+    point_radius_fixed,
+    dimension,
+    js_columns,
+  } = formData as DeckScatterFormData;
+
+  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,
+    spatialCfg,
+    radiusMetricLabel,
+    dimension,
+    js_columns,
+    fixedRadiusValue,
+  );
+
+  return createBaseTransformResult(
+    chartProps,
+    features,
+    radiusMetricLabel ? [radiusMetricLabel] : [],
+  );
+}
+
+// ─── Plugin definition ───────────────────────────────────────────────────────
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export default defineChart<Record<string, never>, any>({
+  metadata: {
+    name: t('deck.gl Scatterplot'),
+    description: t(
+      'A map that takes rendering circles with a variable radius at latitude/longitude coordinates',
+    ),
+    category: t('Map'),
+    credits: ['https://uber.github.io/deck.gl'],
+    behaviors: [Behavior.InteractiveChart],
+    tags: [
+      t('deckGL'),
+      t('Comparison'),
+      t('Scatter'),
+      t('2D'),
+      t('Geo'),
+      t('Intensity'),
+      t('Density'),
+    ],
+    thumbnail,
+    thumbnailDark,
+    exampleGallery: [{ url: example, urlDark: exampleDark }],
+  },
+  arguments: {},
+  suppressQuerySection: true,
+  onInit: controlState => ({
+    ...controlState,
+    time_grain_sqla: {
+      ...controlState.time_grain_sqla,
+      value: null,
+    },
+    granularity: {
+      ...controlState.granularity,
+      value: null,
+    },
+  }),
+  prependSections: [
+    {
+      label: t('Query'),
+      expanded: true,
+      controlSetRows: [
+        [spatial, null],
+        ['row_limit', filterNulls],
+        ['adhoc_filters'],
+        [tooltipContents],
+        [tooltipTemplate],
+      ],
+    },
+    {
+      label: t('Map'),
+      expanded: true,
+      controlSetRows: [
+        [mapProvider],
+        [mapboxStyle],
+        [maplibreStyle],
+        [autozoom, viewport],
+      ],
+    },
+    {
+      label: t('Point Size'),
+      controlSetRows: [
+        [pointRadiusFixed],
+        [
+          {
+            name: 'point_unit',
+            config: {
+              type: 'SelectControl',
+              label: t('Point Unit'),
+              default: 'square_m',
+              clearable: false,
+              choices: [
+                ['square_m', t('Square meters')],
+                ['square_km', t('Square kilometers')],
+                ['square_miles', t('Square miles')],
+                ['radius_m', t('Radius in meters')],
+                ['radius_km', t('Radius in kilometers')],
+                ['radius_miles', t('Radius in miles')],
+              ],
+              description: t(
+                'The unit of measure for the specified point radius',
+              ),
+            },
+          },
+        ],
+        [
+          {
+            name: 'min_radius',
+            config: {
+              type: 'TextControl',
+              label: t('Minimum Radius'),
+              isFloat: true,
+              validators: [validateNonEmpty],
+              renderTrigger: true,
+              default: 2,
+              description: t(
+                'Minimum radius size of the circle, in pixels. As the zoom level changes, this ' +
+                  'insures that the circle respects this minimum radius.',
+              ),
+            },
+          },
+          {
+            name: 'max_radius',
+            config: {
+              type: 'TextControl',
+              label: t('Maximum Radius'),
+              isFloat: true,
+              validators: [validateNonEmpty],
+              renderTrigger: true,
+              default: 250,
+              description: t(
+                'Maximum radius size of the circle, in pixels. As the zoom level changes, this ' +
+                  'insures that the circle respects this maximum radius.',
+              ),
+            },
+          },
+        ],
+        [multiplier, null],
+      ],
+    },
+    {
+      label: t('Point Color'),
+      controlSetRows: [
+        [legendPosition],
+        [legendFormat],
+        ...generateDeckGLColorSchemeControls({}),
+      ],
+    },
+    {
+      label: t('Advanced'),
+      controlSetRows: [
+        [jsColumns],
+        [jsDataMutator],
+        [jsTooltip],
+        [jsOnclickHref],
+      ],
+    },
+  ],
+  additionalControlOverrides: {
+    size: {
+      validators: [],
+    },
+    time_grain_sqla: timeGrainSqlaAnimationOverrides,
+  },
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  buildQuery: (formData: any) => buildQuery(formData as DeckScatterFormData),
+  transform: chartProps => transformProps(chartProps),
+  render: ({ transformedProps }) => (
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    <ScatterComponent {...(transformedProps as any)} />
+  ),
+});
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts
deleted file mode 100644
index a168bd821eb..00000000000
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { ChartProps } from '@superset-ui/core';
-import { processSpatialData, DataRecord } from '../spatialUtils';
-import {
-  createBaseTransformResult,
-  getRecordsFromQuery,
-  getMetricLabelFromFormData,
-  parseMetricValue,
-  addPropertiesToFeature,
-} from '../transformUtils';
-import { DeckScatterFormData } from './buildQuery';
-import { isFixedValue, getFixedValue } from '../utils/metricUtils';
-
-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[],
-  fixedRadiusValue?: number | string | null,
-): 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 || {},
-    };
-
-    // 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;
-        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, 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(
-    chartProps,
-    features,
-    radiusMetricLabel ? [radiusMetricLabel] : [],
-  );
-}