diff --git a/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx b/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx index 7814e97a938..4cf1c35647a 100644 --- a/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx +++ b/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx @@ -265,6 +265,14 @@ export interface ChartDefinition< */ formDataOverrides?: (formData: QueryFormData) => QueryFormData; + /** + * onInit hook - called once when the chart's controls are first initialized. + * Use to reset/override control values that should not persist from the + * dataset's defaults (e.g., clearing time_grain_sqla on charts that don't + * use it). + */ + onInit?: ControlPanelConfig['onInit']; + /** * Custom buildQuery function - use for charts that need post-processing operators * If not provided, a default query builder is generated from arguments @@ -627,6 +635,7 @@ function generateControlPanel( controlOverrides?: ChartDefinition['controlOverrides'], additionalControlOverrides?: ChartDefinition['additionalControlOverrides'], formDataOverrides?: ChartDefinition['formDataOverrides'], + onInit?: ChartDefinition['onInit'], additionalSections?: ControlPanelSectionConfig[], prependSections?: ControlPanelSectionConfig[], chartOptionsTabOverride?: 'customize' | 'data', @@ -753,6 +762,10 @@ function generateControlPanel( config.formDataOverrides = formDataOverrides; } + if (onInit) { + config.onInit = onInit; + } + // Store raw glyph args for native rendering (bypasses expandControlConfig pipeline) config._glyphArgs = args; @@ -973,6 +986,7 @@ export function defineChart< controlOverrides, additionalControlOverrides, formDataOverrides, + onInit, buildQuery: customBuildQuery, transform, render, @@ -989,6 +1003,7 @@ export function defineChart< controlOverrides, additionalControlOverrides, formDataOverrides, + onInit, additionalSections, prependSections, chartOptionsTabOverride, diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.ts deleted file mode 100644 index 231e2bf1f79..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/buildQuery.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * 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 { - buildQueryContext, - ensureIsArray, - getMetricLabel, - QueryFormMetric, - QueryFormOrderBy, - SqlaFormData, - QueryFormColumn, - QueryObject, -} from '@superset-ui/core'; -import { - getSpatialColumns, - addSpatialNullFilters, - SpatialFormData, -} from '../spatialUtils'; -import { - addJsColumnsToColumns, - addTooltipColumnsToQuery, -} from '../buildQueryUtils'; -import { isMetricValue } from '../utils/metricUtils'; - -export interface DeckScatterFormData - extends Omit, SqlaFormData { - // 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; - max_radius?: number; - color_picker?: { r: number; g: number; b: number; a: number }; - dimension?: string; -} - -export default function buildQuery(formData: DeckScatterFormData) { - const { - spatial, - point_radius_fixed, - dimension, - js_columns, - tooltip_contents, - } = formData; - - if (!spatial) { - throw new Error('Spatial configuration is required for Scatter charts'); - } - - return buildQueryContext(formData, { - buildQuery: (baseQueryObject: QueryObject) => { - const spatialColumns = getSpatialColumns(spatial); - let columns = [...(baseQueryObject.columns || []), ...spatialColumns]; - - if (dimension) { - columns.push(dimension); - } - - const columnStrings = columns.map(col => - typeof col === 'string' ? col : col.label || col.sqlExpression || '', - ); - const withJsColumns = addJsColumnsToColumns(columnStrings, js_columns); - - columns = withJsColumns as QueryFormColumn[]; - columns = addTooltipColumnsToQuery(columns, tooltip_contents); - - // Only add metric if point_radius_fixed is a metric type - const isMetric = isMetricValue(point_radius_fixed); - // 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 || []; - // Deduplicate metrics using getMetricLabel for comparison - const existingLabels = new Set( - existingMetrics.map(m => getMetricLabel(m)), - ); - 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 - ? ([[getMetricLabel(metricValue), false]] as QueryFormOrderBy[]) - : (baseQueryObject.orderby as QueryFormOrderBy[]) || []; - - return [ - { - ...baseQueryObject, - columns, - metrics, - filters, - orderby, - is_timeseries: false, - row_limit: baseQueryObject.row_limit, - }, - ]; - }, - }); -} diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.ts deleted file mode 100644 index 20a788c82a3..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/controlPanel.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * 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 { ControlPanelConfig } from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { validateNonEmpty } from '@superset-ui/core'; -import timeGrainSqlaAnimationOverrides from '../../utilities/controls'; -import { - filterNulls, - autozoom, - jsColumns, - jsDataMutator, - jsTooltip, - jsOnclickHref, - legendFormat, - legendPosition, - viewport, - spatial, - pointRadiusFixed, - multiplier, - mapboxStyle, - maplibreStyle, - mapProvider, - generateDeckGLColorSchemeControls, - tooltipContents, - tooltipTemplate, -} from '../../utilities/Shared_DeckGL'; - -const config: ControlPanelConfig = { - onInit: controlState => ({ - ...controlState, - time_grain_sqla: { - ...controlState.time_grain_sqla, - value: null, - }, - granularity: { - ...controlState.granularity, - value: null, - }, - }), - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [spatial, null], - ['row_limit', filterNulls], - ['adhoc_filters'], - [tooltipContents], - [tooltipTemplate], - ], - }, - { - label: t('Map'), - expanded: true, - controlSetRows: [ - [mapProvider], - [mapboxStyle], - [maplibreStyle], - [autozoom, viewport], - ], - }, - { - label: t('Point Size'), - controlSetRows: [ - [pointRadiusFixed], - [ - { - name: 'point_unit', - config: { - type: 'SelectControl', - label: t('Point Unit'), - default: 'square_m', - clearable: false, - choices: [ - ['square_m', t('Square meters')], - ['square_km', t('Square kilometers')], - ['square_miles', t('Square miles')], - ['radius_m', t('Radius in meters')], - ['radius_km', t('Radius in kilometers')], - ['radius_miles', t('Radius in miles')], - ], - description: t( - 'The unit of measure for the specified point radius', - ), - }, - }, - ], - [ - { - name: 'min_radius', - config: { - type: 'TextControl', - label: t('Minimum Radius'), - isFloat: true, - validators: [validateNonEmpty], - renderTrigger: true, - default: 2, - description: t( - 'Minimum radius size of the circle, in pixels. As the zoom level changes, this ' + - 'insures that the circle respects this minimum radius.', - ), - }, - }, - { - name: 'max_radius', - config: { - type: 'TextControl', - label: t('Maximum Radius'), - isFloat: true, - validators: [validateNonEmpty], - renderTrigger: true, - default: 250, - description: t( - 'Maximum radius size of the circle, in pixels. As the zoom level changes, this ' + - 'insures that the circle respects this maximum radius.', - ), - }, - }, - ], - [multiplier, null], - ], - }, - { - label: t('Point Color'), - controlSetRows: [ - [legendPosition], - [legendFormat], - ...generateDeckGLColorSchemeControls({}), - ], - }, - { - label: t('Advanced'), - controlSetRows: [ - [jsColumns], - [jsDataMutator], - [jsTooltip], - [jsOnclickHref], - ], - }, - ], - controlOverrides: { - size: { - validators: [], - }, - time_grain_sqla: timeGrainSqlaAnimationOverrides, - }, -}; - -export default config; diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.ts deleted file mode 100644 index ee43b4a2749..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * 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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example from './images/example.png'; -import exampleDark from './images/example-dark.png'; -import buildQuery from './buildQuery'; -import transformProps from './transformProps'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Map'), - credits: ['https://uber.github.io/deck.gl'], - description: t( - 'A map that takes rendering circles with a variable radius at latitude/longitude coordinates', - ), - name: t('deck.gl Scatterplot'), - thumbnail, - thumbnailDark, - exampleGallery: [{ url: example, urlDark: exampleDark }], - tags: [ - t('deckGL'), - t('Comparison'), - t('Scatter'), - t('2D'), - t('Geo'), - t('Intensity'), - t('Density'), - ], - behaviors: [Behavior.InteractiveChart], -}); - -export default class ScatterChartPlugin extends ChartPlugin { - constructor() { - super({ - buildQuery, - loadChart: () => import('./Scatter'), - controlPanel, - metadata, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.tsx new file mode 100644 index 00000000000..596da9f8c48 --- /dev/null +++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/index.tsx @@ -0,0 +1,438 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { + Behavior, + buildQueryContext, + ChartProps, + ensureIsArray, + getMetricLabel, + QueryFormColumn, + QueryFormMetric, + QueryFormOrderBy, + QueryObject, + SqlaFormData, + validateNonEmpty, +} from '@superset-ui/core'; +import { defineChart } from '@superset-ui/glyph-core'; +import ScatterComponent from './Scatter'; +import { + processSpatialData, + getSpatialColumns, + addSpatialNullFilters, + SpatialFormData, + DataRecord, +} from '../spatialUtils'; +import { + createBaseTransformResult, + getRecordsFromQuery, + getMetricLabelFromFormData, + parseMetricValue, + addPropertiesToFeature, +} from '../transformUtils'; +import { + addJsColumnsToColumns, + addTooltipColumnsToQuery, +} from '../buildQueryUtils'; +import { + isMetricValue, + isFixedValue, + getFixedValue, +} from '../utils/metricUtils'; +import timeGrainSqlaAnimationOverrides from '../../utilities/controls'; +import { + filterNulls, + autozoom, + jsColumns, + jsDataMutator, + jsTooltip, + jsOnclickHref, + legendFormat, + legendPosition, + viewport, + spatial, + pointRadiusFixed, + multiplier, + mapboxStyle, + maplibreStyle, + mapProvider, + generateDeckGLColorSchemeControls, + tooltipContents, + tooltipTemplate, +} from '../../utilities/Shared_DeckGL'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example from './images/example.png'; +import exampleDark from './images/example-dark.png'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +export interface DeckScatterFormData + extends Omit, + SqlaFormData { + // 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; + max_radius?: number; + color_picker?: { r: number; g: number; b: number; a: number }; + dimension?: string; +} + +interface ScatterPoint { + position: [number, number]; + radius?: number; + color?: [number, number, number, number]; + cat_color?: string; + metric?: number; + extraProps?: Record; + [key: string]: unknown; +} + +// ─── buildQuery ────────────────────────────────────────────────────────────── + +export function buildQuery(formData: DeckScatterFormData) { + const { + spatial: spatialCfg, + point_radius_fixed, + dimension, + js_columns, + tooltip_contents, + } = formData; + + if (!spatialCfg) { + throw new Error('Spatial configuration is required for Scatter charts'); + } + + return buildQueryContext(formData, { + buildQuery: (baseQueryObject: QueryObject) => { + const spatialColumns = getSpatialColumns(spatialCfg); + let columns = [...(baseQueryObject.columns || []), ...spatialColumns]; + + if (dimension) { + columns.push(dimension); + } + + const columnStrings = columns.map(col => + typeof col === 'string' ? col : col.label || col.sqlExpression || '', + ); + const withJsColumns = addJsColumnsToColumns(columnStrings, js_columns); + + columns = withJsColumns as QueryFormColumn[]; + columns = addTooltipColumnsToQuery(columns, tooltip_contents); + + const isMetric = isMetricValue(point_radius_fixed); + 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; + + const existingMetrics = baseQueryObject.metrics || []; + const existingLabels = new Set( + existingMetrics.map(m => getMetricLabel(m)), + ); + const metrics: QueryFormMetric[] = + metricValue && !existingLabels.has(getMetricLabel(metricValue)) + ? [...existingMetrics, metricValue] + : existingMetrics; + + const filters = addSpatialNullFilters( + spatialCfg, + ensureIsArray(baseQueryObject.filters || []), + ); + + const orderby = + isMetric && metricValue + ? ([[getMetricLabel(metricValue), false]] as QueryFormOrderBy[]) + : (baseQueryObject.orderby as QueryFormOrderBy[]) || []; + + return [ + { + ...baseQueryObject, + columns, + metrics, + filters, + orderby, + is_timeseries: false, + row_limit: baseQueryObject.row_limit, + }, + ]; + }, + }); +} + +// ─── transformProps ────────────────────────────────────────────────────────── + +export function processScatterData( + records: DataRecord[], + spatialCfg: DeckScatterFormData['spatial'], + radiusMetricLabel?: string, + categoryColumn?: string, + jsCols?: string[], + fixedRadiusValue?: number | string | null, +): ScatterPoint[] { + if (!spatialCfg || !records.length) { + return []; + } + + const spatialFeatures = processSpatialData(records, spatialCfg); + const excludeKeys = new Set([ + 'position', + 'weight', + 'extraProps', + ...(spatialCfg + ? [ + spatialCfg.lonCol, + spatialCfg.latCol, + spatialCfg.lonlatCol, + spatialCfg.geohashCol, + ].filter(Boolean) + : []), + radiusMetricLabel, + categoryColumn, + ...(jsCols || []), + ]); + + return spatialFeatures.map(feature => { + let scatterPoint: ScatterPoint = { + position: feature.position, + extraProps: feature.extraProps || {}, + }; + + if (fixedRadiusValue != null) { + const parsedFixedRadius = parseMetricValue(fixedRadiusValue); + if (parsedFixedRadius !== undefined) { + scatterPoint.radius = parsedFixedRadius; + } + } else if (radiusMetricLabel && feature[radiusMetricLabel] != null) { + const radiusValue = parseMetricValue(feature[radiusMetricLabel]); + if (radiusValue !== undefined) { + scatterPoint.radius = radiusValue; + scatterPoint.metric = radiusValue; + } + } + + if (categoryColumn && feature[categoryColumn] != null) { + scatterPoint.cat_color = String(feature[categoryColumn]); + } + + scatterPoint = addPropertiesToFeature( + scatterPoint, + feature as DataRecord, + excludeKeys, + ); + return scatterPoint; + }); +} + +function transformProps(chartProps: ChartProps) { + const { rawFormData: formData } = chartProps; + const { + spatial: spatialCfg, + point_radius_fixed, + dimension, + js_columns, + } = formData as DeckScatterFormData; + + const fixedRadiusValue = isFixedValue(point_radius_fixed) + ? getFixedValue(point_radius_fixed) + : null; + + const radiusMetricLabel = getMetricLabelFromFormData(point_radius_fixed); + const records = getRecordsFromQuery(chartProps.queriesData); + + const features = processScatterData( + records, + spatialCfg, + radiusMetricLabel, + dimension, + js_columns, + fixedRadiusValue, + ); + + return createBaseTransformResult( + chartProps, + features, + radiusMetricLabel ? [radiusMetricLabel] : [], + ); +} + +// ─── Plugin definition ─────────────────────────────────────────────────────── + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default defineChart, any>({ + metadata: { + name: t('deck.gl Scatterplot'), + description: t( + 'A map that takes rendering circles with a variable radius at latitude/longitude coordinates', + ), + category: t('Map'), + credits: ['https://uber.github.io/deck.gl'], + behaviors: [Behavior.InteractiveChart], + tags: [ + t('deckGL'), + t('Comparison'), + t('Scatter'), + t('2D'), + t('Geo'), + t('Intensity'), + t('Density'), + ], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example, urlDark: exampleDark }], + }, + arguments: {}, + suppressQuerySection: true, + onInit: controlState => ({ + ...controlState, + time_grain_sqla: { + ...controlState.time_grain_sqla, + value: null, + }, + granularity: { + ...controlState.granularity, + value: null, + }, + }), + prependSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: [ + [spatial, null], + ['row_limit', filterNulls], + ['adhoc_filters'], + [tooltipContents], + [tooltipTemplate], + ], + }, + { + label: t('Map'), + expanded: true, + controlSetRows: [ + [mapProvider], + [mapboxStyle], + [maplibreStyle], + [autozoom, viewport], + ], + }, + { + label: t('Point Size'), + controlSetRows: [ + [pointRadiusFixed], + [ + { + name: 'point_unit', + config: { + type: 'SelectControl', + label: t('Point Unit'), + default: 'square_m', + clearable: false, + choices: [ + ['square_m', t('Square meters')], + ['square_km', t('Square kilometers')], + ['square_miles', t('Square miles')], + ['radius_m', t('Radius in meters')], + ['radius_km', t('Radius in kilometers')], + ['radius_miles', t('Radius in miles')], + ], + description: t( + 'The unit of measure for the specified point radius', + ), + }, + }, + ], + [ + { + name: 'min_radius', + config: { + type: 'TextControl', + label: t('Minimum Radius'), + isFloat: true, + validators: [validateNonEmpty], + renderTrigger: true, + default: 2, + description: t( + 'Minimum radius size of the circle, in pixels. As the zoom level changes, this ' + + 'insures that the circle respects this minimum radius.', + ), + }, + }, + { + name: 'max_radius', + config: { + type: 'TextControl', + label: t('Maximum Radius'), + isFloat: true, + validators: [validateNonEmpty], + renderTrigger: true, + default: 250, + description: t( + 'Maximum radius size of the circle, in pixels. As the zoom level changes, this ' + + 'insures that the circle respects this maximum radius.', + ), + }, + }, + ], + [multiplier, null], + ], + }, + { + label: t('Point Color'), + controlSetRows: [ + [legendPosition], + [legendFormat], + ...generateDeckGLColorSchemeControls({}), + ], + }, + { + label: t('Advanced'), + controlSetRows: [ + [jsColumns], + [jsDataMutator], + [jsTooltip], + [jsOnclickHref], + ], + }, + ], + additionalControlOverrides: { + size: { + validators: [], + }, + time_grain_sqla: timeGrainSqlaAnimationOverrides, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + buildQuery: (formData: any) => buildQuery(formData as DeckScatterFormData), + transform: chartProps => transformProps(chartProps), + render: ({ transformedProps }) => ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + + ), +}); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts deleted file mode 100644 index a168bd821eb..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * 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 { ChartProps } from '@superset-ui/core'; -import { processSpatialData, DataRecord } from '../spatialUtils'; -import { - createBaseTransformResult, - getRecordsFromQuery, - getMetricLabelFromFormData, - parseMetricValue, - addPropertiesToFeature, -} from '../transformUtils'; -import { DeckScatterFormData } from './buildQuery'; -import { isFixedValue, getFixedValue } from '../utils/metricUtils'; - -interface ScatterPoint { - position: [number, number]; - radius?: number; - color?: [number, number, number, number]; - cat_color?: string; - metric?: number; - extraProps?: Record; - [key: string]: unknown; -} - -function processScatterData( - records: DataRecord[], - spatial: DeckScatterFormData['spatial'], - radiusMetricLabel?: string, - categoryColumn?: string, - jsColumns?: string[], - fixedRadiusValue?: number | string | null, -): ScatterPoint[] { - if (!spatial || !records.length) { - return []; - } - - const spatialFeatures = processSpatialData(records, spatial); - const excludeKeys = new Set([ - 'position', - 'weight', - 'extraProps', - ...(spatial - ? [ - spatial.lonCol, - spatial.latCol, - spatial.lonlatCol, - spatial.geohashCol, - ].filter(Boolean) - : []), - radiusMetricLabel, - categoryColumn, - ...(jsColumns || []), - ]); - - return spatialFeatures.map(feature => { - let scatterPoint: ScatterPoint = { - position: feature.position, - extraProps: feature.extraProps || {}, - }; - - // Handle radius: either from metric or fixed value - if (fixedRadiusValue != null) { - // Use fixed radius value for all points - const parsedFixedRadius = parseMetricValue(fixedRadiusValue); - if (parsedFixedRadius !== undefined) { - scatterPoint.radius = parsedFixedRadius; - } - } else if (radiusMetricLabel && feature[radiusMetricLabel] != null) { - // Use metric value for radius - const radiusValue = parseMetricValue(feature[radiusMetricLabel]); - if (radiusValue !== undefined) { - scatterPoint.radius = radiusValue; - scatterPoint.metric = radiusValue; - } - } - - if (categoryColumn && feature[categoryColumn] != null) { - scatterPoint.cat_color = String(feature[categoryColumn]); - } - - scatterPoint = addPropertiesToFeature( - scatterPoint, - feature as DataRecord, - excludeKeys, - ); - return scatterPoint; - }); -} - -export default function transformProps(chartProps: ChartProps) { - const { rawFormData: formData } = chartProps; - const { spatial, point_radius_fixed, dimension, js_columns } = - formData as DeckScatterFormData; - - // Check if this is a fixed value or metric - const fixedRadiusValue = isFixedValue(point_radius_fixed) - ? getFixedValue(point_radius_fixed) - : null; - - const radiusMetricLabel = getMetricLabelFromFormData(point_radius_fixed); - const records = getRecordsFromQuery(chartProps.queriesData); - - const features = processScatterData( - records, - spatial, - radiusMetricLabel, - dimension, - js_columns, - fixedRadiusValue, - ); - - return createBaseTransformResult( - chartProps, - features, - radiusMetricLabel ? [radiusMetricLabel] : [], - ); -}