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/06/16 00:38:51 UTC

(superset) branch master updated: feat(deckgl): add point radius controls for GeoJSON layer (#33247)

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 1311d040bac feat(deckgl): add point radius controls for GeoJSON layer (#33247)
1311d040bac is described below

commit 1311d040bacdb6cb5ec34c2e15c3658d7f3e8965
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Mon Jun 15 17:38:43 2026 -0700

    feat(deckgl): add point radius controls for GeoJSON layer (#33247)
    
    Co-authored-by: Claude Opus 4.5 <no...@anthropic.com>
---
 .../src/layers/Geojson/Geojson.test.tsx            | 162 ++++++++++++++++++++-
 .../src/layers/Geojson/Geojson.tsx                 |  15 +-
 .../src/layers/Geojson/controlPanel.ts             |  49 ++++++-
 3 files changed, 221 insertions(+), 5 deletions(-)

diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.test.tsx
index b47de83ab6b..d30d7fcd14c 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.test.tsx
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.test.tsx
@@ -17,7 +17,11 @@
  * under the License.
  */
 import type { ReactElement } from 'react';
-import type { ControlPanelSectionConfig } from '@superset-ui/chart-controls';
+import type {
+  ControlPanelSectionConfig,
+  CustomControlItem,
+} from '@superset-ui/chart-controls';
+import { isCustomControlItem } from '@superset-ui/chart-controls';
 // eslint-disable-next-line import/no-extraneous-dependencies
 import { render } from '@testing-library/react';
 import { SqlaFormData } from '@superset-ui/core';
@@ -28,6 +32,7 @@ import DeckGLGeoJson, {
   computeGeoJsonIconOptionsFromJsOutput,
   computeGeoJsonIconOptionsFromFormData,
   getPoints,
+  getLayer,
 } from './Geojson';
 import controlPanel from './controlPanel';
 
@@ -295,3 +300,158 @@ test('DeckGLGeoJson falls back to legacy map_style when provider-specific style
     }),
   );
 });
+
+const baseFormData: SqlaFormData = {
+  datasource: 'test_datasource',
+  viz_type: 'deck_geojson',
+  slice_id: 1,
+  fill_color_picker: { r: 0, g: 0, b: 255, a: 1 },
+  stroke_color_picker: { r: 0, g: 0, b: 0, a: 1 },
+};
+
+const baseLayerArgs = {
+  onContextMenu: jest.fn(),
+  filterState: undefined,
+  setDataMask: jest.fn(),
+  payload: { data: { type: 'FeatureCollection', features: [] } },
+  setTooltip: jest.fn(),
+  emitCrossFilters: false,
+};
+
+test('getLayer preserves rendering for existing charts without new point radius fields', () => {
+  // Simulate form data from an existing chart that only has point_radius_scale
+  const legacyFormData = {
+    ...baseFormData,
+    point_radius_scale: 200,
+    // point_radius and point_radius_units intentionally absent
+  };
+
+  const layer = getLayer({ formData: legacyFormData, ...baseLayerArgs });
+  const { props } = layer;
+
+  // Should match deck.gl defaults, NOT the new control panel defaults
+  expect(props.getPointRadius).toBe(1); // deck.gl default, not 10
+  expect(props.pointRadiusUnits).toBe('meters'); // deck.gl default, not 'pixels'
+  expect(props.pointRadiusScale).toBe(200); // user's saved value preserved
+});
+
+test('getLayer uses control panel defaults for new charts', () => {
+  const newChartFormData = {
+    ...baseFormData,
+    point_radius: 10,
+    point_radius_units: 'pixels',
+    point_radius_scale: 1,
+  };
+
+  const layer = getLayer({ formData: newChartFormData, ...baseLayerArgs });
+  const { props } = layer;
+
+  expect(props.getPointRadius).toBe(10);
+  expect(props.pointRadiusUnits).toBe('pixels');
+  expect(props.pointRadiusScale).toBe(1);
+});
+
+test('getLayer falls back to defaults when legacy fields are null', () => {
+  // The old point_radius_scale control had `default: null`, so legacy charts
+  // can have null persisted; it must fall back to 1, not coerce to 0.
+  const nullFormData = {
+    ...baseFormData,
+    point_radius: null,
+    point_radius_scale: null,
+  };
+
+  const layer = getLayer({ formData: nullFormData, ...baseLayerArgs });
+  const { props } = layer;
+
+  expect(props.getPointRadius).toBe(1);
+  expect(props.pointRadiusScale).toBe(1);
+});
+
+test('getLayer preserves an explicit zero radius scale', () => {
+  const zeroFormData = {
+    ...baseFormData,
+    point_radius_scale: 0,
+  };
+
+  const layer = getLayer({ formData: zeroFormData, ...baseLayerArgs });
+  const { props } = layer;
+
+  expect(props.pointRadiusScale).toBe(0);
+});
+
+test('getLayer coerces free-form string radius values to numbers', () => {
+  // Free-form SelectControls can store user-typed values as strings
+  const stringFormData = {
+    ...baseFormData,
+    point_radius: '3',
+    point_radius_scale: '0.25',
+  };
+
+  const layer = getLayer({ formData: stringFormData, ...baseLayerArgs });
+  const { props } = layer;
+
+  expect(props.getPointRadius).toBe(3);
+  expect(props.pointRadiusScale).toBe(0.25);
+});
+
+type ControlConfig = {
+  default?: unknown;
+  validators?: unknown[];
+  choices?: [unknown, unknown][];
+  renderTrigger?: boolean;
+};
+
+const controlItems = controlPanel.controlPanelSections
+  .filter(
+    (s: ControlPanelSectionConfig | null): s is ControlPanelSectionConfig =>
+      s !== null,
+  )
+  .flatMap((section: ControlPanelSectionConfig) => section.controlSetRows)
+  .flat();
+
+const findControlConfig = (name: string): ControlConfig | undefined =>
+  (controlItems.filter(isCustomControlItem) as CustomControlItem[]).find(
+    (item: CustomControlItem) => item.name === name,
+  )?.config as ControlConfig | undefined;
+
+test('controlPanel exposes a Point Radius control defaulting to 10', () => {
+  const config = findControlConfig('point_radius');
+  expect(config).toBeDefined();
+  expect(config?.default).toBe(10);
+  expect(config?.renderTrigger).toBe(true);
+  expect(config?.validators).toHaveLength(1);
+  expect(config?.choices).toEqual(
+    expect.arrayContaining([
+      [1, '1'],
+      [10, '10'],
+      [100, '100'],
+    ]),
+  );
+});
+
+test('controlPanel Point Radius Scale defaults to 1 with fractional choices', () => {
+  const config = findControlConfig('point_radius_scale');
+  expect(config).toBeDefined();
+  expect(config?.default).toBe(1);
+  expect(config?.renderTrigger).toBe(true);
+  expect(config?.validators).toHaveLength(1);
+  expect(config?.choices).toEqual(
+    expect.arrayContaining([
+      [0.1, '0.1'],
+      [1, '1'],
+      [10, '10'],
+    ]),
+  );
+});
+
+test('controlPanel Point Radius Units defaults to pixels', () => {
+  const config = findControlConfig('point_radius_units');
+  expect(config).toBeDefined();
+  expect(config?.default).toBe('pixels');
+  expect(config?.renderTrigger).toBe(true);
+  expect(config?.choices?.map(([value]) => value)).toEqual([
+    'pixels',
+    'meters',
+    'common',
+  ]);
+});
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
index d6a958f4301..5264ecf6d86 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
@@ -254,6 +254,15 @@ export const computeGeoJsonIconOptionsFromFormData = (
   iconSizeUnits: fd.icon_size_unit,
 });
 
+// Free-form SelectControls can yield string values, and legacy charts may have
+// null persisted for these fields, so coerce to a number (falling back to the
+// provided default for null/undefined/NaN input, while preserving an explicit 0)
+// before handing them to deck.gl's numeric layer props.
+const toNumber = (value: unknown, fallback: number) => {
+  const num = Number(value ?? fallback);
+  return Number.isFinite(num) ? num : fallback;
+};
+
 export const getLayer: GetLayerType<GeoJsonLayer> = function ({
   formData,
   onContextMenu,
@@ -328,7 +337,11 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
       getFillColor(feature, filterState?.value),
     getLineColor,
     getLineWidth: fd.line_width || 1,
-    pointRadiusScale: fd.point_radius_scale,
+    // Use deck.gl defaults as fallbacks for backward compatibility with existing charts.
+    // New charts will get control panel defaults (point_radius=10, units='pixels', scale=1).
+    getPointRadius: toNumber(fd.point_radius, 1),
+    pointRadiusUnits: fd.point_radius_units ?? 'meters',
+    pointRadiusScale: toNumber(fd.point_radius_scale, 1),
     lineWidthUnits: fd.line_width_unit,
     pointType,
     ...labelOpts,
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/controlPanel.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/controlPanel.ts
index ba2cb14f7ab..ad8f3fb1c63 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/controlPanel.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/controlPanel.ts
@@ -22,6 +22,8 @@ import {
   legacyValidateInteger,
   isFeatureEnabled,
   FeatureFlag,
+  validateNumber,
+  validateInteger,
 } from '@superset-ui/core';
 import { formatSelectOptions } from '../../utilities/utils';
 import {
@@ -352,15 +354,56 @@ const config: ControlPanelConfig = {
           },
         ],
         [
+          {
+            name: 'point_radius',
+            config: {
+              type: 'SelectControl',
+              freeForm: true,
+              label: t('Point Radius'),
+              description: t(
+                'The radius of point features, in the units specified below. ' +
+                  'The final rendered size is this value multiplied by Point Radius Scale.',
+              ),
+              validators: [validateInteger],
+              default: 10,
+              choices: formatSelectOptions([1, 5, 10, 20, 50, 100]),
+              renderTrigger: true,
+            },
+          },
           {
             name: 'point_radius_scale',
             config: {
               type: 'SelectControl',
               freeForm: true,
               label: t('Point Radius Scale'),
-              validators: [legacyValidateInteger],
-              default: null,
-              choices: formatSelectOptions([0, 100, 200, 300, 500]),
+              description: t(
+                'A multiplier applied to the point radius. ' +
+                  'Use this to uniformly scale all points.',
+              ),
+              validators: [validateNumber],
+              default: 1,
+              choices: formatSelectOptions([0.1, 0.5, 1, 2, 5, 10]),
+              renderTrigger: true,
+            },
+          },
+        ],
+        [
+          {
+            name: 'point_radius_units',
+            config: {
+              type: 'SelectControl',
+              label: t('Point Radius Units'),
+              description: t(
+                'The unit for point radius. Use "pixels" for consistent ' +
+                  'screen-space sizing regardless of zoom level.',
+              ),
+              default: 'pixels',
+              choices: [
+                ['pixels', t('Pixels')],
+                ['meters', t('Meters')],
+                ['common', t('Common (unit per pixel at zoom 0)')],
+              ],
+              renderTrigger: true,
             },
           },
         ],