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,
},
},
],