This is an automated email from the ASF dual-hosted git repository.
enzomartellucci 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 675a4c7a66d fix(charts): numerical column for the Point Radius field
in mapbox (#36962)
675a4c7a66d is described below
commit 675a4c7a66dfbc37c23786cfc4f286ba2f13730f
Author: Felipe López <[email protected]>
AuthorDate: Thu Jan 29 06:50:10 2026 -0300
fix(charts): numerical column for the Point Radius field in mapbox (#36962)
---
.../src/ScatterPlotGlowOverlay.jsx | 64 ++++
.../src/transformProps.js | 4 +-
.../test/ScatterPlotGlowOverlay.test.tsx | 346 +++++++++++++++++++++
3 files changed, 413 insertions(+), 1 deletion(-)
diff --git
a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx
b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx
index 739204c134a..e70862f1d9c 100644
---
a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx
+++
b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx
@@ -145,6 +145,26 @@ class ScatterPlotGlowOverlay extends PureComponent {
const maxLabel = Math.max(...clusterLabelMap.filter(v =>
!Number.isNaN(v)));
+ // Calculate min/max radius values for Pixels mode scaling
+ let minRadiusValue = Infinity;
+ let maxRadiusValue = -Infinity;
+ if (pointRadiusUnit === 'Pixels') {
+ locations.forEach(location => {
+ // Accept both null and undefined as "no value" and coerce potential
numeric strings
+ if (
+ !location.properties.cluster &&
+ location.properties.radius != null
+ ) {
+ const radiusValueRaw = location.properties.radius;
+ const radiusValue = Number(radiusValueRaw);
+ if (Number.isFinite(radiusValue)) {
+ minRadiusValue = Math.min(minRadiusValue, radiusValue);
+ maxRadiusValue = Math.max(maxRadiusValue, radiusValue);
+ }
+ }
+ });
+ }
+
ctx.clearRect(0, 0, width, height);
ctx.globalCompositeOperation = compositeOperation;
@@ -232,6 +252,50 @@ class ScatterPlotGlowOverlay extends PureComponent {
pointLatitude,
zoom,
);
+ } else if (pointRadiusUnit === 'Pixels') {
+ // Scale pixel values to a reasonable range (radius/6 to
radius/3)
+ // This ensures points are visible and proportional to their
values
+ const MIN_POINT_RADIUS = radius / 6;
+ const MAX_POINT_RADIUS = radius / 3;
+
+ if (
+ Number.isFinite(minRadiusValue) &&
+ Number.isFinite(maxRadiusValue) &&
+ maxRadiusValue > minRadiusValue
+ ) {
+ // Normalize the value to 0-1 range, then scale to pixel
range
+ const numericPointRadius = Number(pointRadius);
+ if (!Number.isFinite(numericPointRadius)) {
+ // fallback to minimum visible size when the value is not
a finite number
+ pointRadius = MIN_POINT_RADIUS;
+ } else {
+ const normalizedValueRaw =
+ (numericPointRadius - minRadiusValue) /
+ (maxRadiusValue - minRadiusValue);
+ const normalizedValue = Math.max(
+ 0,
+ Math.min(1, normalizedValueRaw),
+ );
+ pointRadius =
+ MIN_POINT_RADIUS +
+ normalizedValue * (MAX_POINT_RADIUS - MIN_POINT_RADIUS);
+ }
+ pointLabel = `${roundDecimal(radiusProperty, 2)}`;
+ } else if (
+ Number.isFinite(minRadiusValue) &&
+ minRadiusValue === maxRadiusValue
+ ) {
+ // All values are the same, use a fixed medium size
+ pointRadius = (MIN_POINT_RADIUS + MAX_POINT_RADIUS) / 2;
+ pointLabel = `${roundDecimal(radiusProperty, 2)}`;
+ } else {
+ // Use raw pixel values if they're already in a reasonable
range
+ pointRadius = Math.max(
+ MIN_POINT_RADIUS,
+ Math.min(pointRadius, MAX_POINT_RADIUS),
+ );
+ pointLabel = `${roundDecimal(radiusProperty, 2)}`;
+ }
}
}
diff --git
a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.js
b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.js
index de2da2a7357..14a5581926b 100644
---
a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.js
+++
b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.js
@@ -90,7 +90,9 @@ export default function transformProps(chartProps) {
setControlValue('viewport_latitude', latitude);
setControlValue('viewport_zoom', zoom);
},
- pointRadius: pointRadius === 'Auto' ? DEFAULT_POINT_RADIUS : pointRadius,
+ // Always use DEFAULT_POINT_RADIUS as the base radius for cluster sizing
+ // Individual point radii come from geoJSON properties.radius
+ pointRadius: DEFAULT_POINT_RADIUS,
pointRadiusUnit,
renderWhileDragging,
rgb,
diff --git
a/superset-frontend/plugins/legacy-plugin-chart-map-box/test/ScatterPlotGlowOverlay.test.tsx
b/superset-frontend/plugins/legacy-plugin-chart-map-box/test/ScatterPlotGlowOverlay.test.tsx
new file mode 100644
index 00000000000..27dffc2ed2f
--- /dev/null
+++
b/superset-frontend/plugins/legacy-plugin-chart-map-box/test/ScatterPlotGlowOverlay.test.tsx
@@ -0,0 +1,346 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import ScatterPlotGlowOverlay from '../src/ScatterPlotGlowOverlay';
+
+// Mock react-map-gl's CanvasOverlay
+jest.mock('react-map-gl', () => ({
+ CanvasOverlay: ({ redraw }: { redraw: Function }) => {
+ // Store the redraw function so tests can call it
+ (global as any).mockRedraw = redraw;
+ return <div data-testid="canvas-overlay" />;
+ },
+}));
+
+// Mock utility functions
+jest.mock('../src/utils/luminanceFromRGB', () => ({
+ __esModule: true,
+ default: jest.fn(() => 150), // Return a value above the dark threshold
+}));
+
+// Test helpers
+const createMockCanvas = () => {
+ const ctx: any = {
+ clearRect: jest.fn(),
+ beginPath: jest.fn(),
+ arc: jest.fn(),
+ fill: jest.fn(),
+ fillText: jest.fn(),
+ measureText: jest.fn(() => ({ width: 10 })),
+ createRadialGradient: jest.fn(() => ({
+ addColorStop: jest.fn(),
+ })),
+ globalCompositeOperation: '',
+ fillStyle: '',
+ font: '',
+ textAlign: '',
+ textBaseline: '',
+ shadowBlur: 0,
+ shadowColor: '',
+ };
+
+ return ctx;
+};
+
+const createMockRedrawParams = (overrides = {}) => ({
+ width: 800,
+ height: 600,
+ ctx: createMockCanvas(),
+ isDragging: false,
+ project: (lngLat: [number, number]) => lngLat,
+ ...overrides,
+});
+
+const createLocation = (
+ coordinates: [number, number],
+ properties: Record<string, any>,
+) => ({
+ geometry: { coordinates },
+ properties,
+});
+
+const defaultProps = {
+ lngLatAccessor: (loc: any) => loc.geometry.coordinates,
+ dotRadius: 60,
+ rgb: ['', 255, 0, 0] as any,
+ globalOpacity: 1,
+};
+
+test('renders map with varying radius values in Pixels mode', () => {
+ const locations = [
+ createLocation([100, 100], { radius: 10, cluster: false }),
+ createLocation([200, 200], { radius: 50, cluster: false }),
+ createLocation([300, 300], { radius: 100, cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('handles dataset with uniform radius values', () => {
+ const locations = [
+ createLocation([100, 100], { radius: 50, cluster: false }),
+ createLocation([200, 200], { radius: 50, cluster: false }),
+ createLocation([300, 300], { radius: 50, cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('renders successfully when data contains non-finite values', () => {
+ const locations = [
+ createLocation([100, 100], { radius: 10, cluster: false }),
+ createLocation([200, 200], { radius: NaN, cluster: false }),
+ createLocation([300, 300], { radius: 100, cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('handles radius values provided as strings', () => {
+ const locations = [
+ createLocation([100, 100], { radius: '10', cluster: false }),
+ createLocation([200, 200], { radius: '50', cluster: false }),
+ createLocation([300, 300], { radius: '100', cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('renders points when radius values are missing', () => {
+ const locations = [
+ createLocation([100, 100], { radius: null, cluster: false }),
+ createLocation([200, 200], { radius: undefined, cluster: false }),
+ createLocation([300, 300], { cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('renders both cluster and non-cluster points correctly', () => {
+ const locations = [
+ createLocation([100, 100], { radius: 10, cluster: false }),
+ createLocation([200, 200], {
+ radius: 999,
+ cluster: true,
+ point_count: 5,
+ sum: 100,
+ }),
+ createLocation([300, 300], { radius: 100, cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('renders map with multiple points with different radius values', () => {
+ const locations = [
+ createLocation([100, 100], { radius: 10, cluster: false }),
+ createLocation([200, 200], { radius: 42.567, cluster: false }),
+ createLocation([300, 300], { radius: 100, cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('renders map with Kilometers mode', () => {
+ const locations = [
+ createLocation([100, 50], { radius: 10, cluster: false }),
+ createLocation([200, 50], { radius: 5, cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Kilometers"
+ zoom={10}
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('renders map with Miles mode', () => {
+ const locations = [
+ createLocation([100, 50], { radius: 5, cluster: false }),
+ createLocation([200, 50], { radius: 10, cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Miles"
+ zoom={10}
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('displays metric property labels on points', () => {
+ const locations = [
+ createLocation([100, 100], { radius: 50, metric: 123.456, cluster: false
}),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('handles empty dataset without errors', () => {
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={[]}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('handles extreme outlier radius values without breaking', () => {
+ const locations = [
+ createLocation([100, 100], { radius: 1, cluster: false }),
+ createLocation([200, 200], { radius: 50, cluster: false }),
+ createLocation([300, 300], { radius: 999999, cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});
+
+test('renders successfully with mixed extreme and negative radius values', ()
=> {
+ const locations = [
+ createLocation([100, 100], { radius: 0.001, cluster: false }),
+ createLocation([150, 150], { radius: 5, cluster: false }),
+ createLocation([200, 200], { radius: 100, cluster: false }),
+ createLocation([250, 250], { radius: 50000, cluster: false }),
+ createLocation([300, 300], { radius: -10, cluster: false }),
+ ];
+
+ expect(() => {
+ render(
+ <ScatterPlotGlowOverlay
+ {...defaultProps}
+ locations={locations}
+ pointRadiusUnit="Pixels"
+ />,
+ );
+ }).not.toThrow();
+
+ expect(() => {
+ const redrawParams = createMockRedrawParams();
+ (global as any).mockRedraw(redrawParams);
+ }).not.toThrow();
+});