Skip to content

Commit d0b7258

Browse files
fix(scatter): fix ad-hoc metric for pointsize
1 parent 2dfc770 commit d0b7258

File tree

3 files changed

+131
-20
lines changed

3 files changed

+131
-20
lines changed

superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.test.ts

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,20 +212,27 @@ test('Scatter buildQuery should handle js_columns', () => {
212212
expect(query.columns).toContain('custom_col2');
213213
});
214214

215-
test('Scatter buildQuery should convert numeric metric value to string', () => {
215+
test('Scatter buildQuery should handle adhoc SQL metric for point_radius_fixed', () => {
216+
const adhocMetric = {
217+
label: 'count(*) * 1.1',
218+
expressionType: 'SQL' as const,
219+
sqlExpression: 'count(*) * 1.1',
220+
};
216221
const formData: DeckScatterFormData = {
217222
...baseFormData,
218223
point_radius_fixed: {
219224
type: 'metric',
220-
value: 123, // numeric metric (edge case)
225+
value: adhocMetric,
221226
},
222227
};
223228

224229
const queryContext = buildQuery(formData);
225230
const [query] = queryContext.queries;
226231

227-
expect(query.metrics).toContain('123');
228-
expect(query.orderby).toEqual([['123', false]]);
232+
// Should preserve full adhoc metric object (not just the label string)
233+
expect(query.metrics).toContainEqual(adhocMetric);
234+
// orderby should use the label string
235+
expect(query.orderby).toEqual([['count(*) * 1.1', false]]);
229236
});
230237

231238
test('Scatter buildQuery should set is_timeseries to false', () => {
@@ -310,3 +317,107 @@ test('Scatter buildQuery should deduplicate metrics when radius metric already e
310317
expect(query.metrics).toEqual(['COUNT(*)', 'AVG(price)']);
311318
expect(query.metrics).toHaveLength(2);
312319
});
320+
321+
// Comprehensive point_radius_fixed tests to prevent regressions
322+
test('Scatter buildQuery should handle adhoc SIMPLE metric for point_radius_fixed', () => {
323+
const adhocMetric = {
324+
label: 'AVG(population)',
325+
expressionType: 'SIMPLE' as const,
326+
column: { column_name: 'population' },
327+
aggregate: 'AVG' as const,
328+
};
329+
const formData: DeckScatterFormData = {
330+
...baseFormData,
331+
point_radius_fixed: {
332+
type: 'metric',
333+
value: adhocMetric,
334+
},
335+
};
336+
337+
const queryContext = buildQuery(formData);
338+
const [query] = queryContext.queries;
339+
340+
// Should preserve full adhoc metric object
341+
expect(query.metrics).toContainEqual(adhocMetric);
342+
expect(query.orderby).toEqual([['AVG(population)', false]]);
343+
});
344+
345+
test('Scatter buildQuery should deduplicate adhoc metrics with same label', () => {
346+
const adhocMetric = {
347+
label: 'custom_count',
348+
expressionType: 'SQL' as const,
349+
sqlExpression: 'count(*) * 2',
350+
};
351+
const formData: DeckScatterFormData = {
352+
...baseFormData,
353+
metrics: [adhocMetric], // Already has this metric
354+
point_radius_fixed: {
355+
type: 'metric',
356+
value: adhocMetric, // Same metric for radius
357+
},
358+
};
359+
360+
const queryContext = buildQuery(formData);
361+
const [query] = queryContext.queries;
362+
363+
// Should not duplicate the metric
364+
expect(query.metrics).toHaveLength(1);
365+
expect(query.metrics).toContainEqual(adhocMetric);
366+
});
367+
368+
test('Scatter buildQuery should handle fixed type with string value correctly', () => {
369+
const formData: DeckScatterFormData = {
370+
...baseFormData,
371+
point_radius_fixed: {
372+
type: 'fix',
373+
value: '2500',
374+
},
375+
};
376+
377+
const queryContext = buildQuery(formData);
378+
const [query] = queryContext.queries;
379+
380+
// Fixed values should NOT be added to metrics
381+
expect(query.metrics).toEqual([]);
382+
expect(query.orderby).toEqual([]);
383+
});
384+
385+
test('Scatter buildQuery should handle undefined value in metric type gracefully', () => {
386+
const formData: DeckScatterFormData = {
387+
...baseFormData,
388+
point_radius_fixed: {
389+
type: 'metric',
390+
value: undefined,
391+
},
392+
};
393+
394+
const queryContext = buildQuery(formData);
395+
const [query] = queryContext.queries;
396+
397+
// Should not add anything when value is undefined
398+
expect(query.metrics).toEqual([]);
399+
expect(query.orderby).toEqual([]);
400+
});
401+
402+
test('Scatter buildQuery should preserve adhoc metric with custom label', () => {
403+
const adhocMetric = {
404+
label: 'My Custom Metric',
405+
expressionType: 'SQL' as const,
406+
sqlExpression: 'SUM(revenue) / COUNT(*)',
407+
hasCustomLabel: true,
408+
};
409+
const formData: DeckScatterFormData = {
410+
...baseFormData,
411+
point_radius_fixed: {
412+
type: 'metric',
413+
value: adhocMetric,
414+
},
415+
};
416+
417+
const queryContext = buildQuery(formData);
418+
const [query] = queryContext.queries;
419+
420+
// Should preserve full metric including hasCustomLabel
421+
expect(query.metrics).toContainEqual(adhocMetric);
422+
expect(query.orderby).toEqual([['My Custom Metric', false]]);
423+
});

superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import {
2020
buildQueryContext,
2121
ensureIsArray,
22+
getMetricLabel,
23+
QueryFormMetric,
2224
QueryFormOrderBy,
2325
SqlaFormData,
2426
QueryFormColumn,
@@ -31,16 +33,15 @@ import {
3133
} from '../spatialUtils';
3234
import {
3335
addJsColumnsToColumns,
34-
processMetricsArray,
3536
addTooltipColumnsToQuery,
3637
} from '../buildQueryUtils';
37-
import { isMetricValue, extractMetricKey } from '../utils/metricUtils';
38+
import { isMetricValue } from '../utils/metricUtils';
3839

3940
export interface DeckScatterFormData
4041
extends Omit<SpatialFormData, 'color_picker'>, SqlaFormData {
4142
point_radius_fixed?: {
4243
type?: 'fix' | 'metric';
43-
value?: string | number;
44+
value?: QueryFormMetric;
4445
};
4546
multiplier?: number;
4647
point_unit?: string;
@@ -82,26 +83,29 @@ export default function buildQuery(formData: DeckScatterFormData) {
8283

8384
// Only add metric if point_radius_fixed is a metric type
8485
const isMetric = isMetricValue(point_radius_fixed);
85-
const metricValue = isMetric
86-
? extractMetricKey(point_radius_fixed?.value)
87-
: null;
86+
// Preserve full metric value (string for saved metrics, object for adhoc)
87+
const metricValue = isMetric ? point_radius_fixed?.value : null;
8888

8989
// Preserve existing metrics and only add radius metric if it's metric-based
9090
const existingMetrics = baseQueryObject.metrics || [];
91-
const radiusMetrics = processMetricsArray(
92-
metricValue ? [metricValue] : [],
91+
// Deduplicate metrics using getMetricLabel for comparison
92+
const existingLabels = new Set(
93+
existingMetrics.map(m => getMetricLabel(m)),
9394
);
94-
// Deduplicate metrics to avoid adding the same metric twice
95-
const metricsSet = new Set([...existingMetrics, ...radiusMetrics]);
96-
const metrics = Array.from(metricsSet);
95+
const metrics =
96+
metricValue && !existingLabels.has(getMetricLabel(metricValue))
97+
? [...existingMetrics, metricValue]
98+
: [...existingMetrics];
99+
97100
const filters = addSpatialNullFilters(
98101
spatial,
99102
ensureIsArray(baseQueryObject.filters || []),
100103
);
101104

105+
// orderby needs string label, not the full metric object
102106
const orderby =
103107
isMetric && metricValue
104-
? ([[metricValue, false]] as QueryFormOrderBy[])
108+
? ([[getMetricLabel(metricValue), false]] as QueryFormOrderBy[])
105109
: (baseQueryObject.orderby as QueryFormOrderBy[]) || [];
106110

107111
return [

superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/buildQueryUtils.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,6 @@ export function addColumnsIfNotExists(
9292
return result;
9393
}
9494

95-
export function processMetricsArray(metrics: (string | undefined)[]): string[] {
96-
return metrics.filter((metric): metric is string => Boolean(metric));
97-
}
98-
9995
export function extractTooltipColumns(tooltipContents?: unknown[]): string[] {
10096
if (!Array.isArray(tooltipContents) || !tooltipContents.length) {
10197
return [];

0 commit comments

Comments
 (0)