This is an automated email from the ASF dual-hosted git repository.
asoare 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 2b411b32ba0 fix(scatter): Fix ad-hoc metric for pointsize (#37669)
2b411b32ba0 is described below
commit 2b411b32ba07f8dd4b11ee23623de81e064ee1fb
Author: Alexandru Soare <[email protected]>
AuthorDate: Mon Feb 9 11:13:06 2026 +0200
fix(scatter): Fix ad-hoc metric for pointsize (#37669)
---
.../src/layers/Scatter/buildQuery.test.ts | 245 ++++++++++++++++++++-
.../src/layers/Scatter/buildQuery.ts | 44 ++--
.../src/layers/buildQueryUtils.ts | 4 -
3 files changed, 270 insertions(+), 23 deletions(-)
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
index 830d2c26b58..007867764cc 100644
---
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
@@ -212,20 +212,27 @@ test('Scatter buildQuery should handle js_columns', () =>
{
expect(query.columns).toContain('custom_col2');
});
-test('Scatter buildQuery should convert numeric metric value to string', () =>
{
+test('Scatter buildQuery should handle adhoc SQL metric for
point_radius_fixed', () => {
+ const adhocMetric = {
+ label: 'count(*) * 1.1',
+ expressionType: 'SQL' as const,
+ sqlExpression: 'count(*) * 1.1',
+ };
const formData: DeckScatterFormData = {
...baseFormData,
point_radius_fixed: {
type: 'metric',
- value: 123, // numeric metric (edge case)
+ value: adhocMetric,
},
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
- expect(query.metrics).toContain('123');
- expect(query.orderby).toEqual([['123', false]]);
+ // Should preserve full adhoc metric object (not just the label string)
+ expect(query.metrics).toContainEqual(adhocMetric);
+ // orderby should use the label string
+ expect(query.orderby).toEqual([['count(*) * 1.1', false]]);
});
test('Scatter buildQuery should set is_timeseries to false', () => {
@@ -310,3 +317,233 @@ test('Scatter buildQuery should deduplicate metrics when
radius metric already e
expect(query.metrics).toEqual(['COUNT(*)', 'AVG(price)']);
expect(query.metrics).toHaveLength(2);
});
+
+// Comprehensive point_radius_fixed tests to prevent regressions
+test('Scatter buildQuery should handle adhoc SIMPLE metric for
point_radius_fixed', () => {
+ const adhocMetric = {
+ label: 'AVG(population)',
+ expressionType: 'SIMPLE' as const,
+ column: { column_name: 'population' },
+ aggregate: 'AVG' as const,
+ };
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ point_radius_fixed: {
+ type: 'metric',
+ value: adhocMetric,
+ },
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Should preserve full adhoc metric object
+ expect(query.metrics).toContainEqual(adhocMetric);
+ expect(query.orderby).toEqual([['AVG(population)', false]]);
+});
+
+test('Scatter buildQuery should deduplicate adhoc metrics with same label', ()
=> {
+ const adhocMetric = {
+ label: 'custom_count',
+ expressionType: 'SQL' as const,
+ sqlExpression: 'count(*) * 2',
+ };
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ metrics: [adhocMetric], // Already has this metric
+ point_radius_fixed: {
+ type: 'metric',
+ value: adhocMetric, // Same metric for radius
+ },
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Should not duplicate the metric
+ expect(query.metrics).toHaveLength(1);
+ expect(query.metrics).toContainEqual(adhocMetric);
+});
+
+test('Scatter buildQuery should handle fixed type with string value
correctly', () => {
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ point_radius_fixed: {
+ type: 'fix',
+ value: '2500',
+ },
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Fixed values should NOT be added to metrics
+ expect(query.metrics).toEqual([]);
+ expect(query.orderby).toEqual([]);
+});
+
+test('Scatter buildQuery should handle undefined value in metric type
gracefully', () => {
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ point_radius_fixed: {
+ type: 'metric',
+ value: undefined,
+ },
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Should not add anything when value is undefined
+ expect(query.metrics).toEqual([]);
+ expect(query.orderby).toEqual([]);
+});
+
+test('Scatter buildQuery should preserve adhoc metric with custom label', ()
=> {
+ const adhocMetric = {
+ label: 'My Custom Metric',
+ expressionType: 'SQL' as const,
+ sqlExpression: 'SUM(revenue) / COUNT(*)',
+ hasCustomLabel: true,
+ };
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ point_radius_fixed: {
+ type: 'metric',
+ value: adhocMetric,
+ },
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Should preserve full metric including hasCustomLabel
+ expect(query.metrics).toContainEqual(adhocMetric);
+ expect(query.orderby).toEqual([['My Custom Metric', false]]);
+});
+
+test('Scatter buildQuery should handle point_radius_fixed with undefined
type', () => {
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ point_radius_fixed: {
+ type: undefined,
+ value: '1000',
+ },
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Without explicit type, should not be treated as metric
+ expect(query.metrics).toEqual([]);
+ expect(query.orderby).toEqual([]);
+});
+
+test('Scatter buildQuery should handle empty point_radius_fixed object', () =>
{
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ point_radius_fixed: {},
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Empty object should not add any metrics
+ expect(query.metrics).toEqual([]);
+ expect(query.orderby).toEqual([]);
+});
+
+test('Scatter buildQuery should handle fix type with undefined value', () => {
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ point_radius_fixed: {
+ type: 'fix',
+ value: undefined,
+ },
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Fix type with no value should not affect metrics
+ expect(query.metrics).toEqual([]);
+ expect(query.orderby).toEqual([]);
+});
+
+test('Scatter buildQuery should add saved metric radius to existing adhoc
metrics', () => {
+ const existingAdhocMetric = {
+ label: 'SUM(sales)',
+ expressionType: 'SQL' as const,
+ sqlExpression: 'SUM(sales)',
+ };
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ metrics: [existingAdhocMetric],
+ point_radius_fixed: {
+ type: 'metric',
+ value: 'AVG(radius)',
+ },
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Should have both the existing adhoc metric and the new saved metric
+ expect(query.metrics).toContainEqual(existingAdhocMetric);
+ expect(query.metrics).toContain('AVG(radius)');
+ expect(query.metrics).toHaveLength(2);
+});
+
+test('Scatter buildQuery should add adhoc metric radius to existing saved
metrics', () => {
+ const radiusAdhocMetric = {
+ label: 'Custom Radius',
+ expressionType: 'SQL' as const,
+ sqlExpression: 'AVG(size) * 100',
+ };
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ metrics: ['COUNT(*)', 'SUM(value)'],
+ point_radius_fixed: {
+ type: 'metric',
+ value: radiusAdhocMetric,
+ },
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Should have existing saved metrics plus the new adhoc metric
+ expect(query.metrics).toContain('COUNT(*)');
+ expect(query.metrics).toContain('SUM(value)');
+ expect(query.metrics).toContainEqual(radiusAdhocMetric);
+ expect(query.metrics).toHaveLength(3);
+});
+
+test('Scatter buildQuery should handle legacy string format for
point_radius_fixed', () => {
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ point_radius_fixed: 'COUNT(*)',
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Legacy string format should be treated as a metric
+ expect(query.metrics).toContain('COUNT(*)');
+ expect(query.orderby).toEqual([['COUNT(*)', false]]);
+});
+
+test('Scatter buildQuery should deduplicate legacy string metric with existing
metrics', () => {
+ const formData: DeckScatterFormData = {
+ ...baseFormData,
+ metrics: ['COUNT(*)', 'SUM(value)'],
+ point_radius_fixed: 'COUNT(*)',
+ };
+
+ const queryContext = buildQuery(formData);
+ const [query] = queryContext.queries;
+
+ // Should not duplicate COUNT(*)
+ expect(query.metrics).toEqual(['COUNT(*)', 'SUM(value)']);
+ 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 e1976c6544f..231e2bf1f79 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
@@ -19,6 +19,8 @@
import {
buildQueryContext,
ensureIsArray,
+ getMetricLabel,
+ QueryFormMetric,
QueryFormOrderBy,
SqlaFormData,
QueryFormColumn,
@@ -31,17 +33,19 @@ import {
} from '../spatialUtils';
import {
addJsColumnsToColumns,
- processMetricsArray,
addTooltipColumnsToQuery,
} from '../buildQueryUtils';
-import { isMetricValue, extractMetricKey } from '../utils/metricUtils';
+import { isMetricValue } from '../utils/metricUtils';
export interface DeckScatterFormData
extends Omit<SpatialFormData, 'color_picker'>, SqlaFormData {
- point_radius_fixed?: {
- type?: 'fix' | 'metric';
- value?: string | number;
- };
+ // 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;
@@ -82,26 +86,36 @@ export default function buildQuery(formData:
DeckScatterFormData) {
// 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;
+ // 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 || [];
- const radiusMetrics = processMetricsArray(
- metricValue ? [metricValue] : [],
+ // Deduplicate metrics using getMetricLabel for comparison
+ const existingLabels = new Set(
+ existingMetrics.map(m => getMetricLabel(m)),
);
- // Deduplicate metrics to avoid adding the same metric twice
- const metricsSet = new Set([...existingMetrics, ...radiusMetrics]);
- const metrics = Array.from(metricsSet);
+ 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
- ? ([[metricValue, false]] as QueryFormOrderBy[])
+ ? ([[getMetricLabel(metricValue), false]] as QueryFormOrderBy[])
: (baseQueryObject.orderby as QueryFormOrderBy[]) || [];
return [
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/buildQueryUtils.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/buildQueryUtils.ts
index f61a71ede7c..7a3c20d2422 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/buildQueryUtils.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/buildQueryUtils.ts
@@ -92,10 +92,6 @@ export function addColumnsIfNotExists(
return result;
}
-export function processMetricsArray(metrics: (string | undefined)[]): string[]
{
- return metrics.filter((metric): metric is string => Boolean(metric));
-}
-
export function extractTooltipColumns(tooltipContents?: unknown[]): string[] {
if (!Array.isArray(tooltipContents) || !tooltipContents.length) {
return [];