From dd1226f8e59aac252ef2faca3d29a3d9002cd616 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 14 May 2026 18:56:52 -0700 Subject: [PATCH] feat(glyph): consolidate deckgl Polygon layer to defineChart() Collapse multi-file plugin into single index.tsx. Polygon keeps its custom buildQuery (line_column required, optional metric/elevation metric handling, null filter) and transformProps (json/geohash/zipcode polygon decoders, fixed/metric elevation, reverse_long_lat). Largest deckgl layer (500+ lines) but follows the same pattern. Polygon.tsx component stays as sibling for Multi. Co-Authored-By: Claude Sonnet 4.6 --- .../src/layers/Polygon/buildQuery.ts | 129 ----- .../src/layers/Polygon/controlPanel.ts | 225 -------- .../src/layers/Polygon/index.ts | 53 -- .../src/layers/Polygon/index.tsx | 521 ++++++++++++++++++ .../src/layers/Polygon/transformProps.ts | 186 ------- 5 files changed, 521 insertions(+), 593 deletions(-) delete mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/buildQuery.ts delete mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/controlPanel.ts delete mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/index.ts create mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/index.tsx delete mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/transformProps.ts diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/buildQuery.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/buildQuery.ts deleted file mode 100644 index ec319a9ae2c..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/buildQuery.ts +++ /dev/null @@ -1,129 +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, - SqlaFormData, - getMetricLabel, - QueryObjectFilterClause, - QueryObject, - QueryFormColumn, - QueryFormMetric, -} from '@superset-ui/core'; -import { addTooltipColumnsToQuery } from '../buildQueryUtils'; - -export interface DeckPolygonFormData extends SqlaFormData { - line_column?: string; - line_type?: string; - metric?: string; - point_radius_fixed?: - | { - value?: string; - } - | { - type: 'fix'; - value: string; - } - | { - type: 'metric'; - value: QueryFormMetric; - }; - reverse_long_lat?: boolean; - filter_nulls?: boolean; - js_columns?: string[]; - tooltip_contents?: unknown[]; - tooltip_template?: string; -} - -export default function buildQuery(formData: DeckPolygonFormData) { - const { - line_column, - metric, - point_radius_fixed, - filter_nulls = true, - js_columns, - tooltip_contents, - } = formData; - - if (!line_column) { - throw new Error('Polygon column is required for Polygon charts'); - } - - return buildQueryContext(formData, (baseQueryObject: QueryObject) => { - let columns: QueryFormColumn[] = [ - ...ensureIsArray(baseQueryObject.columns || []), - line_column, - ]; - - const jsColumns = ensureIsArray(js_columns || []); - jsColumns.forEach((col: string) => { - if (!columns.includes(col)) { - columns.push(col); - } - }); - - columns = addTooltipColumnsToQuery(columns, tooltip_contents); - - const metrics = []; - if (metric) { - metrics.push(metric); - } - - if (point_radius_fixed) { - if ('type' in point_radius_fixed) { - if ( - point_radius_fixed.type === 'metric' && - point_radius_fixed.value != null - ) { - metrics.push(point_radius_fixed.value); - } - } - } - - const filters = ensureIsArray(baseQueryObject.filters || []); - if (filter_nulls) { - const nullFilters: QueryObjectFilterClause[] = [ - { - col: line_column, - op: 'IS NOT NULL', - }, - ]; - - if (metric) { - nullFilters.push({ - col: getMetricLabel(metric), - op: 'IS NOT NULL', - }); - } - - filters.push(...nullFilters); - } - - return [ - { - ...baseQueryObject, - columns, - metrics, - filters, - is_timeseries: false, - row_limit: baseQueryObject.row_limit, - }, - ]; - }); -} diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/controlPanel.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/controlPanel.ts deleted file mode 100644 index 010e079d00f..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/controlPanel.ts +++ /dev/null @@ -1,225 +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, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import timeGrainSqlaAnimationOverrides from '../../utilities/controls'; -import { COLOR_SCHEME_TYPES, formatSelectOptions } from '../../utilities/utils'; -import { - filterNulls, - autozoom, - jsColumns, - jsDataMutator, - jsTooltip, - jsOnclickHref, - legendFormat, - legendPosition, - fillColorPicker, - strokeColorPicker, - filled, - stroked, - extruded, - viewport, - pointRadiusFixed, - multiplier, - lineWidth, - lineType, - reverseLongLat, - mapboxStyle, - maplibreStyle, - mapProvider, - deckGLCategoricalColorSchemeTypeSelect, - deckGLLinearColorSchemeSelect, - deckGLColorBreakpointsSelect, - breakpointsDefaultColor, - tooltipContents, - tooltipTemplate, -} from '../../utilities/Shared_DeckGL'; -import { dndLineColumn } from '../../utilities/sharedDndControls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [ - { - ...dndLineColumn, - config: { - ...dndLineColumn.config, - label: t('Polygon Column'), - }, - }, - ], - [ - { - ...lineType, - config: { - ...lineType.config, - label: t('Polygon Encoding'), - }, - }, - ], - ['adhoc_filters'], - ['metric'], - [ - { - ...pointRadiusFixed, - config: { - ...pointRadiusFixed.config, - label: t('Elevation'), - }, - }, - ], - ['row_limit'], - [reverseLongLat], - [filterNulls], - [tooltipContents], - [tooltipTemplate], - ], - }, - { - label: t('Map'), - expanded: true, - controlSetRows: [ - [mapProvider], - [mapboxStyle], - [maplibreStyle], - [viewport], - [autozoom], - ], - }, - { - label: t('Polygon Settings'), - expanded: true, - controlSetRows: [ - [ - { - ...deckGLCategoricalColorSchemeTypeSelect, - config: { - ...deckGLCategoricalColorSchemeTypeSelect.config, - choices: [ - [COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')], - [COLOR_SCHEME_TYPES.linear_palette, t('Linear palette')], - [COLOR_SCHEME_TYPES.color_breakpoints, t('Color breakpoints')], - ], - default: COLOR_SCHEME_TYPES.linear_palette, - }, - }, - fillColorPicker, - deckGLLinearColorSchemeSelect, - breakpointsDefaultColor, - deckGLColorBreakpointsSelect, - strokeColorPicker, - ], - [filled, stroked], - [extruded], - [multiplier], - [lineWidth], - [ - { - name: 'line_width_unit', - config: { - type: 'SelectControl', - label: t('Line width unit'), - default: 'pixels', - choices: [ - ['meters', t('meters')], - ['pixels', t('pixels')], - ], - renderTrigger: true, - }, - }, - ], - [ - { - name: 'opacity', - config: { - type: 'SliderControl', - label: t('Opacity'), - default: 80, - step: 1, - min: 0, - max: 100, - renderTrigger: true, - description: t('Opacity, expects values between 0 and 100'), - }, - }, - ], - [ - { - name: 'num_buckets', - config: { - type: 'SelectControl', - multi: false, - freeForm: true, - label: t('Number of buckets to group data'), - default: 5, - choices: formatSelectOptions([2, 3, 5, 10]), - description: t('How many buckets should the data be grouped in.'), - renderTrigger: true, - }, - }, - ], - [ - { - name: 'break_points', - config: { - type: 'SelectControl', - multi: true, - freeForm: true, - label: t('Bucket break points'), - choices: formatSelectOptions([]), - description: t( - 'List of n+1 values for bucketing metric into n buckets.', - ), - renderTrigger: true, - }, - }, - ], - [legendPosition], - [legendFormat], - ], - }, - { - label: t('Advanced'), - controlSetRows: [ - [jsColumns], - [jsDataMutator], - [jsTooltip], - [jsOnclickHref], - ], - }, - ], - controlOverrides: { - metric: { - validators: [], - }, - time_grain_sqla: timeGrainSqlaAnimationOverrides, - }, - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/index.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/index.ts deleted file mode 100644 index 0d5a691ab4e..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/index.ts +++ /dev/null @@ -1,53 +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 transformProps from './transformProps'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Map'), - credits: ['https://uber.github.io/deck.gl'], - description: t( - 'Visualizes geographic areas from your data as polygons on a Mapbox rendered map. Polygons can be colored using a metric.', - ), - name: t('deck.gl Polygon'), - thumbnail, - thumbnailDark, - exampleGallery: [{ url: example, urlDark: exampleDark }], - tags: [t('deckGL'), t('3D'), t('Multi-Dimensions'), t('Geo')], - behaviors: [Behavior.InteractiveChart], -}); - -export default class PolygonChartPlugin extends ChartPlugin { - constructor() { - super({ - buildQuery, - loadChart: () => import('./Polygon'), - controlPanel, - metadata, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/index.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/index.tsx new file mode 100644 index 00000000000..2c6155ec805 --- /dev/null +++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/index.tsx @@ -0,0 +1,521 @@ +/** + * 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, + QueryObject, + QueryObjectFilterClause, + SqlaFormData, +} from '@superset-ui/core'; +import { getStandardizedControls } from '@superset-ui/chart-controls'; +import { defineChart } from '@superset-ui/glyph-core'; +import { decode_bbox } from 'ngeohash'; +import PolygonComponent from './Polygon'; +import { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils'; +import { + createBaseTransformResult, + getRecordsFromQuery, + getMetricLabelFromFormData, + parseMetricValue, + addPropertiesToFeature, +} from '../transformUtils'; +import { addTooltipColumnsToQuery } from '../buildQueryUtils'; +import timeGrainSqlaAnimationOverrides from '../../utilities/controls'; +import { COLOR_SCHEME_TYPES, formatSelectOptions } from '../../utilities/utils'; +import { + filterNulls, + autozoom, + jsColumns, + jsDataMutator, + jsTooltip, + jsOnclickHref, + legendFormat, + legendPosition, + fillColorPicker, + strokeColorPicker, + filled, + stroked, + extruded, + viewport, + pointRadiusFixed, + multiplier, + lineWidth, + lineType, + reverseLongLat, + mapboxStyle, + maplibreStyle, + mapProvider, + deckGLCategoricalColorSchemeTypeSelect, + deckGLLinearColorSchemeSelect, + deckGLColorBreakpointsSelect, + breakpointsDefaultColor, + tooltipContents, + tooltipTemplate, +} from '../../utilities/Shared_DeckGL'; +import { dndLineColumn } from '../../utilities/sharedDndControls'; +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 DeckPolygonFormData extends SqlaFormData { + line_column?: string; + line_type?: string; + metric?: string; + point_radius_fixed?: + | { value?: string } + | { type: 'fix'; value: string } + | { type: 'metric'; value: QueryFormMetric }; + reverse_long_lat?: boolean; + filter_nulls?: boolean; + js_columns?: string[]; + tooltip_contents?: unknown[]; + tooltip_template?: string; +} + +interface PolygonFeature { + polygon?: number[][]; + name?: string; + elevation?: number; + extraProps?: Record; + metrics?: Record; +} + +// ─── buildQuery ────────────────────────────────────────────────────────────── + +export function buildQuery(formData: DeckPolygonFormData) { + const { + line_column, + metric, + point_radius_fixed, + filter_nulls = true, + js_columns, + tooltip_contents, + } = formData; + + if (!line_column) { + throw new Error('Polygon column is required for Polygon charts'); + } + + return buildQueryContext(formData, (baseQueryObject: QueryObject) => { + let columns: QueryFormColumn[] = [ + ...ensureIsArray(baseQueryObject.columns || []), + line_column, + ]; + + const jsCols = ensureIsArray(js_columns || []); + jsCols.forEach((col: string) => { + if (!columns.includes(col)) { + columns.push(col); + } + }); + + columns = addTooltipColumnsToQuery(columns, tooltip_contents); + + const metrics = []; + if (metric) { + metrics.push(metric); + } + + if (point_radius_fixed) { + if ('type' in point_radius_fixed) { + if ( + point_radius_fixed.type === 'metric' && + point_radius_fixed.value != null + ) { + metrics.push(point_radius_fixed.value); + } + } + } + + const filters = ensureIsArray(baseQueryObject.filters || []); + if (filter_nulls) { + const nullFilters: QueryObjectFilterClause[] = [ + { col: line_column, op: 'IS NOT NULL' }, + ]; + + if (metric) { + nullFilters.push({ col: getMetricLabel(metric), op: 'IS NOT NULL' }); + } + + filters.push(...nullFilters); + } + + return [ + { + ...baseQueryObject, + columns, + metrics, + filters, + is_timeseries: false, + row_limit: baseQueryObject.row_limit, + }, + ]; + }); +} + +// ─── transformProps ────────────────────────────────────────────────────────── + +function parseElevationValue(value: string): number | undefined { + const parsed = parseFloat(value); + return Number.isNaN(parsed) ? undefined : parsed; +} + +export function processPolygonData( + records: DataRecord[], + formData: DeckPolygonFormData, +): PolygonFeature[] { + const { + line_column, + line_type, + metric, + point_radius_fixed, + reverse_long_lat, + js_columns, + } = formData; + + if (!line_column || !records.length) { + return []; + } + + const metricLabel = getMetricLabelFromFormData(metric); + + let elevationLabel: string | undefined; + let fixedElevationValue: number | undefined; + + if (point_radius_fixed) { + if ('type' in point_radius_fixed) { + if ( + point_radius_fixed.type === 'metric' && + point_radius_fixed.value != null + ) { + elevationLabel = getMetricLabel(point_radius_fixed.value); + } else if ( + point_radius_fixed.type === 'fix' && + point_radius_fixed.value + ) { + fixedElevationValue = parseElevationValue(point_radius_fixed.value); + } + } else if (point_radius_fixed.value) { + fixedElevationValue = parseElevationValue(point_radius_fixed.value); + } + } + + const excludeKeys = new Set([line_column, ...(js_columns || [])]); + + return records + .map(record => { + let feature: PolygonFeature = { + extraProps: {}, + metrics: {}, + }; + + feature = addJsColumnsToExtraProps(feature, record, js_columns); + const updatedFeature = addPropertiesToFeature( + feature as unknown as Record, + record, + excludeKeys, + ); + feature = updatedFeature as unknown as PolygonFeature; + + const rawPolygonData = record[line_column]; + if (!rawPolygonData) { + return null; + } + + try { + let polygonCoords: number[][]; + + switch (line_type) { + case 'json': { + const parsed = + typeof rawPolygonData === 'string' + ? JSON.parse(rawPolygonData) + : rawPolygonData; + + if (parsed.coordinates) { + polygonCoords = parsed.coordinates[0] || parsed.coordinates; + } else if (parsed.geometry?.coordinates) { + polygonCoords = + parsed.geometry.coordinates[0] || parsed.geometry.coordinates; + } else if (Array.isArray(parsed)) { + polygonCoords = parsed; + } else { + return null; + } + break; + } + case 'geohash': + polygonCoords = []; + const decoded = decode_bbox(String(rawPolygonData)); + if (decoded) { + polygonCoords.push([decoded[1], decoded[0]]); + polygonCoords.push([decoded[1], decoded[2]]); + polygonCoords.push([decoded[3], decoded[2]]); + polygonCoords.push([decoded[3], decoded[0]]); + polygonCoords.push([decoded[1], decoded[0]]); + } + break; + case 'zipcode': + default: { + polygonCoords = Array.isArray(rawPolygonData) ? rawPolygonData : []; + break; + } + } + + if (reverse_long_lat && polygonCoords.length > 0) { + polygonCoords = polygonCoords.map(coord => [coord[1], coord[0]]); + } + + feature.polygon = polygonCoords; + + if (fixedElevationValue !== undefined) { + feature.elevation = fixedElevationValue; + } else if (elevationLabel && record[elevationLabel] != null) { + const elevationValue = parseMetricValue(record[elevationLabel]); + if (elevationValue !== undefined) { + feature.elevation = elevationValue; + } + } + + if (metricLabel && record[metricLabel] != null) { + const metricValue = record[metricLabel]; + if ( + typeof metricValue === 'string' || + typeof metricValue === 'number' + ) { + feature.metrics![metricLabel] = metricValue; + } + } + } catch { + return null; + } + + return feature; + }) + .filter((feature): feature is PolygonFeature => feature !== null); +} + +function transformProps(chartProps: ChartProps) { + const { rawFormData: formData } = chartProps; + const records = getRecordsFromQuery(chartProps.queriesData); + const features = processPolygonData(records, formData as DeckPolygonFormData); + + return createBaseTransformResult(chartProps, features); +} + +// ─── Plugin definition ─────────────────────────────────────────────────────── + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default defineChart, any>({ + metadata: { + name: t('deck.gl Polygon'), + description: t( + 'Visualizes geographic areas from your data as polygons on a Mapbox rendered map. Polygons can be colored using a metric.', + ), + category: t('Map'), + credits: ['https://uber.github.io/deck.gl'], + behaviors: [Behavior.InteractiveChart], + tags: [t('deckGL'), t('3D'), t('Multi-Dimensions'), t('Geo')], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example, urlDark: exampleDark }], + }, + arguments: {}, + suppressQuerySection: true, + prependSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: [ + [ + { + ...dndLineColumn, + config: { + ...dndLineColumn.config, + label: t('Polygon Column'), + }, + }, + ], + [ + { + ...lineType, + config: { + ...lineType.config, + label: t('Polygon Encoding'), + }, + }, + ], + ['adhoc_filters'], + ['metric'], + [ + { + ...pointRadiusFixed, + config: { + ...pointRadiusFixed.config, + label: t('Elevation'), + }, + }, + ], + ['row_limit'], + [reverseLongLat], + [filterNulls], + [tooltipContents], + [tooltipTemplate], + ], + }, + { + label: t('Map'), + expanded: true, + controlSetRows: [ + [mapProvider], + [mapboxStyle], + [maplibreStyle], + [viewport], + [autozoom], + ], + }, + { + label: t('Polygon Settings'), + expanded: true, + controlSetRows: [ + [ + { + ...deckGLCategoricalColorSchemeTypeSelect, + config: { + ...deckGLCategoricalColorSchemeTypeSelect.config, + choices: [ + [COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')], + [COLOR_SCHEME_TYPES.linear_palette, t('Linear palette')], + [COLOR_SCHEME_TYPES.color_breakpoints, t('Color breakpoints')], + ], + default: COLOR_SCHEME_TYPES.linear_palette, + }, + }, + fillColorPicker, + deckGLLinearColorSchemeSelect, + breakpointsDefaultColor, + deckGLColorBreakpointsSelect, + strokeColorPicker, + ], + [filled, stroked], + [extruded], + [multiplier], + [lineWidth], + [ + { + name: 'line_width_unit', + config: { + type: 'SelectControl', + label: t('Line width unit'), + default: 'pixels', + choices: [ + ['meters', t('meters')], + ['pixels', t('pixels')], + ], + renderTrigger: true, + }, + }, + ], + [ + { + name: 'opacity', + config: { + type: 'SliderControl', + label: t('Opacity'), + default: 80, + step: 1, + min: 0, + max: 100, + renderTrigger: true, + description: t('Opacity, expects values between 0 and 100'), + }, + }, + ], + [ + { + name: 'num_buckets', + config: { + type: 'SelectControl', + multi: false, + freeForm: true, + label: t('Number of buckets to group data'), + default: 5, + choices: formatSelectOptions([2, 3, 5, 10]), + description: t('How many buckets should the data be grouped in.'), + renderTrigger: true, + }, + }, + ], + [ + { + name: 'break_points', + config: { + type: 'SelectControl', + multi: true, + freeForm: true, + label: t('Bucket break points'), + choices: formatSelectOptions([]), + description: t( + 'List of n+1 values for bucketing metric into n buckets.', + ), + renderTrigger: true, + }, + }, + ], + [legendPosition], + [legendFormat], + ], + }, + { + label: t('Advanced'), + controlSetRows: [ + [jsColumns], + [jsDataMutator], + [jsTooltip], + [jsOnclickHref], + ], + }, + ], + additionalControlOverrides: { + metric: { + validators: [], + }, + time_grain_sqla: timeGrainSqlaAnimationOverrides, + }, + formDataOverrides: formData => ({ + ...formData, + metric: getStandardizedControls().shiftMetric(), + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + buildQuery: (formData: any) => buildQuery(formData as DeckPolygonFormData), + 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/Polygon/transformProps.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/transformProps.ts deleted file mode 100644 index 5a9f71b2a53..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/transformProps.ts +++ /dev/null @@ -1,186 +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, getMetricLabel } from '@superset-ui/core'; -import { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils'; -import { - createBaseTransformResult, - getRecordsFromQuery, - getMetricLabelFromFormData, - parseMetricValue, - addPropertiesToFeature, -} from '../transformUtils'; -import { DeckPolygonFormData } from './buildQuery'; -import { decode_bbox } from 'ngeohash'; - -function parseElevationValue(value: string): number | undefined { - const parsed = parseFloat(value); - return Number.isNaN(parsed) ? undefined : parsed; -} - -interface PolygonFeature { - polygon?: number[][]; - name?: string; - elevation?: number; - extraProps?: Record; - metrics?: Record; -} - -function processPolygonData( - records: DataRecord[], - formData: DeckPolygonFormData, -): PolygonFeature[] { - const { - line_column, - line_type, - metric, - point_radius_fixed, - reverse_long_lat, - js_columns, - } = formData; - - if (!line_column || !records.length) { - return []; - } - - const metricLabel = getMetricLabelFromFormData(metric); - - let elevationLabel: string | undefined; - let fixedElevationValue: number | undefined; - - if (point_radius_fixed) { - if ('type' in point_radius_fixed) { - if ( - point_radius_fixed.type === 'metric' && - point_radius_fixed.value != null - ) { - elevationLabel = getMetricLabel(point_radius_fixed.value); - } else if ( - point_radius_fixed.type === 'fix' && - point_radius_fixed.value - ) { - fixedElevationValue = parseElevationValue(point_radius_fixed.value); - } - } else if (point_radius_fixed.value) { - fixedElevationValue = parseElevationValue(point_radius_fixed.value); - } - } - - const excludeKeys = new Set([line_column, ...(js_columns || [])]); - - return records - .map(record => { - let feature: PolygonFeature = { - extraProps: {}, - metrics: {}, - }; - - feature = addJsColumnsToExtraProps(feature, record, js_columns); - const updatedFeature = addPropertiesToFeature( - feature as unknown as Record, - record, - excludeKeys, - ); - feature = updatedFeature as unknown as PolygonFeature; - - const rawPolygonData = record[line_column]; - if (!rawPolygonData) { - return null; - } - - try { - let polygonCoords: number[][]; - - switch (line_type) { - case 'json': { - const parsed = - typeof rawPolygonData === 'string' - ? JSON.parse(rawPolygonData) - : rawPolygonData; - - if (parsed.coordinates) { - polygonCoords = parsed.coordinates[0] || parsed.coordinates; - } else if (parsed.geometry?.coordinates) { - // Non-standard format with nested geometry - polygonCoords = - parsed.geometry.coordinates[0] || parsed.geometry.coordinates; - } else if (Array.isArray(parsed)) { - polygonCoords = parsed; - } else { - return null; - } - break; - } - case 'geohash': - polygonCoords = []; - const decoded = decode_bbox(String(rawPolygonData)); - if (decoded) { - polygonCoords.push([decoded[1], decoded[0]]); // SW (minLon, minLat) - polygonCoords.push([decoded[1], decoded[2]]); // NW (minLon, maxLat) - polygonCoords.push([decoded[3], decoded[2]]); // NE (maxLon, maxLat) - polygonCoords.push([decoded[3], decoded[0]]); // SE (maxLon, minLat) - polygonCoords.push([decoded[1], decoded[0]]); // SW (close polygon) - } - break; - case 'zipcode': - default: { - polygonCoords = Array.isArray(rawPolygonData) ? rawPolygonData : []; - break; - } - } - - if (reverse_long_lat && polygonCoords.length > 0) { - polygonCoords = polygonCoords.map(coord => [coord[1], coord[0]]); - } - - feature.polygon = polygonCoords; - - if (fixedElevationValue !== undefined) { - feature.elevation = fixedElevationValue; - } else if (elevationLabel && record[elevationLabel] != null) { - const elevationValue = parseMetricValue(record[elevationLabel]); - if (elevationValue !== undefined) { - feature.elevation = elevationValue; - } - } - - if (metricLabel && record[metricLabel] != null) { - const metricValue = record[metricLabel]; - if ( - typeof metricValue === 'string' || - typeof metricValue === 'number' - ) { - feature.metrics![metricLabel] = metricValue; - } - } - } catch { - return null; - } - - return feature; - }) - .filter((feature): feature is PolygonFeature => feature !== null); -} - -export default function transformProps(chartProps: ChartProps) { - const { rawFormData: formData } = chartProps; - const records = getRecordsFromQuery(chartProps.queriesData); - const features = processPolygonData(records, formData as DeckPolygonFormData); - - return createBaseTransformResult(chartProps, features); -}