This is an automated email from the ASF dual-hosted git repository.
rusackas 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 dce74014da refactor(deckgl): update deck.gl charts to use new api
(#34859)
dce74014da is described below
commit dce74014dad2361a9a4ab7cd7b88efaedd33dc0a
Author: Damian Pendrak <[email protected]>
AuthorDate: Tue Sep 23 19:42:28 2025 +0200
refactor(deckgl): update deck.gl charts to use new api (#34859)
---
.../src/CategoricalDeckGLContainer.tsx | 10 +-
.../src/layers/Arc/buildQuery.ts | 96 ++++
.../src/layers/Arc/index.ts | 5 +-
.../src/layers/Arc/transformProps.ts | 108 ++++
.../src/layers/Contour/buildQuery.ts | 34 ++
.../src/layers/Contour/index.ts | 7 +-
.../src/layers/Contour/transformProps.ts | 21 +
.../src/layers/Grid/Grid.tsx | 2 +-
.../src/layers/Grid/buildQuery.ts | 27 +
.../src/layers/Grid/index.ts | 5 +-
.../src/layers/Grid/transformProps.ts | 24 +
.../src/layers/Heatmap/Heatmap.tsx | 2 +-
.../src/layers/Heatmap/buildQuery.ts | 23 +
.../src/layers/Heatmap/index.ts | 7 +-
.../src/layers/Heatmap/transformProps.ts | 24 +
.../src/layers/Hex/Hex.tsx | 2 +-
.../src/layers/Hex/buildQuery.ts | 29 +
.../src/layers/Hex/index.ts | 5 +-
.../src/layers/Hex/transformProps.ts | 24 +
.../src/layers/Path/buildQuery.ts | 95 ++++
.../src/layers/Path/index.ts | 5 +-
.../src/layers/Path/transformProps.ts | 166 ++++++
.../src/layers/Polygon/Polygon.tsx | 2 +-
.../src/layers/Polygon/buildQuery.ts | 111 ++++
.../src/layers/Polygon/index.ts | 5 +-
.../src/layers/Polygon/transformProps.ts | 143 +++++
.../src/layers/Scatter/buildQuery.ts | 105 ++++
.../src/layers/Scatter/index.ts | 5 +-
.../src/layers/Scatter/transformProps.ts | 116 ++++
.../src/layers/Screengrid/Screengrid.tsx | 2 +-
.../src/layers/Screengrid/buildQuery.ts | 23 +
.../src/layers/Screengrid/index.ts | 5 +-
.../src/layers/Screengrid/transformProps.ts | 24 +
.../src/layers/buildQueryUtils.ts | 142 +++++
.../src/layers/spatialUtils.test.ts | 604 +++++++++++++++++++++
.../src/layers/spatialUtils.ts | 400 ++++++++++++++
.../src/layers/transformUtils.ts | 142 +++++
.../src/utilities/Shared_DeckGL.tsx | 2 +-
.../src/utils/crossFiltersDataMask.ts | 7 +-
superset/views/base.py | 1 +
superset/views/core.py | 4 +-
41 files changed, 2530 insertions(+), 34 deletions(-)
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
index 1757a7b77e..1400a15133 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/CategoricalDeckGLContainer.tsx
@@ -169,12 +169,12 @@ const CategoricalDeckGLContainer = (props:
CategoricalDeckGLContainerProps) => {
}));
}
case COLOR_SCHEME_TYPES.color_breakpoints: {
- const defaultBreakpointColor = fd.deafult_breakpoint_color
+ const defaultBreakpointColor = fd.default_breakpoint_color
? [
- fd.deafult_breakpoint_color.r,
- fd.deafult_breakpoint_color.g,
- fd.deafult_breakpoint_color.b,
- fd.deafult_breakpoint_color.a * 255,
+ fd.default_breakpoint_color.r,
+ fd.default_breakpoint_color.g,
+ fd.default_breakpoint_color.b,
+ fd.default_breakpoint_color.a * 255,
]
: [
DEFAULT_DECKGL_COLOR.r,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/buildQuery.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/buildQuery.ts
new file mode 100644
index 0000000000..6e5a714d12
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/buildQuery.ts
@@ -0,0 +1,96 @@
+/**
+ * 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,
+} from '@superset-ui/core';
+import {
+ getSpatialColumns,
+ addSpatialNullFilters,
+ SpatialFormData,
+} from '../spatialUtils';
+import { addTooltipColumnsToQuery } from '../buildQueryUtils';
+
+export interface DeckArcFormData extends SqlaFormData {
+ start_spatial: SpatialFormData['spatial'];
+ end_spatial: SpatialFormData['spatial'];
+ dimension?: string;
+ js_columns?: string[];
+ tooltip_contents?: unknown[];
+ tooltip_template?: string;
+}
+
+export default function buildQuery(formData: DeckArcFormData) {
+ const {
+ start_spatial,
+ end_spatial,
+ dimension,
+ js_columns,
+ tooltip_contents,
+ } = formData;
+
+ if (!start_spatial || !end_spatial) {
+ throw new Error(
+ 'Start and end spatial configurations are required for Arc charts',
+ );
+ }
+
+ return buildQueryContext(formData, baseQueryObject => {
+ const startSpatialColumns = getSpatialColumns(start_spatial);
+ const endSpatialColumns = getSpatialColumns(end_spatial);
+
+ let columns = [
+ ...(baseQueryObject.columns || []),
+ ...startSpatialColumns,
+ ...endSpatialColumns,
+ ];
+
+ if (dimension) {
+ columns = [...columns, dimension];
+ }
+
+ const jsColumns = ensureIsArray(js_columns || []);
+ jsColumns.forEach(col => {
+ if (!columns.includes(col)) {
+ columns.push(col);
+ }
+ });
+
+ columns = addTooltipColumnsToQuery(columns, tooltip_contents);
+
+ let filters = addSpatialNullFilters(
+ start_spatial,
+ ensureIsArray(baseQueryObject.filters || []),
+ );
+ filters = addSpatialNullFilters(end_spatial, filters);
+
+ const isTimeseries = !!formData.time_grain_sqla;
+
+ return [
+ {
+ ...baseQueryObject,
+ columns,
+ filters,
+ is_timeseries: isTimeseries,
+ row_limit: baseQueryObject.row_limit,
+ },
+ ];
+ });
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/index.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/index.ts
index 60a2c1db07..364e57c469 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/index.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/index.ts
@@ -21,7 +21,8 @@ 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 transformProps from './transformProps';
+import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
@@ -39,13 +40,13 @@ const metadata = new ChartMetadata({
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
- useLegacyApi: true,
tags: [t('deckGL'), t('Geo'), t('3D'), t('Relational'), t('Web')],
});
export default class ArcChartPlugin extends ChartPlugin {
constructor() {
super({
+ buildQuery,
loadChart: () => import('./Arc'),
controlPanel,
metadata,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/transformProps.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/transformProps.ts
new file mode 100644
index 0000000000..85df90585d
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Arc/transformProps.ts
@@ -0,0 +1,108 @@
+/**
+ * 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,
+ addJsColumnsToExtraProps,
+ DataRecord,
+} from '../spatialUtils';
+import {
+ createBaseTransformResult,
+ getRecordsFromQuery,
+ addPropertiesToFeature,
+} from '../transformUtils';
+import { DeckArcFormData } from './buildQuery';
+
+interface ArcPoint {
+ sourcePosition: [number, number];
+ targetPosition: [number, number];
+ cat_color?: string;
+ __timestamp?: number;
+ extraProps?: Record<string, unknown>;
+ [key: string]: unknown;
+}
+
+function processArcData(
+ records: DataRecord[],
+ startSpatial: DeckArcFormData['start_spatial'],
+ endSpatial: DeckArcFormData['end_spatial'],
+ dimension?: string,
+ jsColumns?: string[],
+): ArcPoint[] {
+ if (!startSpatial || !endSpatial || !records.length) {
+ return [];
+ }
+
+ const startFeatures = processSpatialData(records, startSpatial);
+ const endFeatures = processSpatialData(records, endSpatial);
+ const excludeKeys = new Set(
+ ['__timestamp', dimension, ...(jsColumns || [])].filter(
+ (key): key is string => key != null,
+ ),
+ );
+
+ return records
+ .map((record, index) => {
+ const startFeature = startFeatures[index];
+ const endFeature = endFeatures[index];
+
+ if (!startFeature || !endFeature) {
+ return null;
+ }
+
+ let arcPoint: ArcPoint = {
+ sourcePosition: startFeature.position,
+ targetPosition: endFeature.position,
+ extraProps: {},
+ };
+
+ arcPoint = addJsColumnsToExtraProps(arcPoint, record, jsColumns);
+
+ if (dimension && record[dimension] != null) {
+ arcPoint.cat_color = String(record[dimension]);
+ }
+
+ // eslint-disable-next-line no-underscore-dangle
+ if (record.__timestamp != null) {
+ // eslint-disable-next-line no-underscore-dangle
+ arcPoint.__timestamp = Number(record.__timestamp);
+ }
+
+ arcPoint = addPropertiesToFeature(arcPoint, record, excludeKeys);
+ return arcPoint;
+ })
+ .filter((point): point is ArcPoint => point !== null);
+}
+
+export default function transformProps(chartProps: ChartProps) {
+ const { rawFormData: formData } = chartProps;
+ const { start_spatial, end_spatial, dimension, js_columns } =
+ formData as DeckArcFormData;
+
+ const records = getRecordsFromQuery(chartProps.queriesData);
+ const features = processArcData(
+ records,
+ start_spatial,
+ end_spatial,
+ dimension,
+ js_columns,
+ );
+
+ return createBaseTransformResult(chartProps, features);
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/buildQuery.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/buildQuery.ts
new file mode 100644
index 0000000000..294a0f997b
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/buildQuery.ts
@@ -0,0 +1,34 @@
+/**
+ * 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 { SpatialFormData, buildSpatialQuery } from '../spatialUtils';
+
+export interface DeckContourFormData extends SpatialFormData {
+ cellSize?: string;
+ aggregation?: string;
+ contours?: Array<{
+ color: { r: number; g: number; b: number };
+ lowerThreshold: number;
+ upperThreshold?: number;
+ strokeWidth?: number;
+ }>;
+}
+
+export default function buildQuery(formData: DeckContourFormData) {
+ return buildSpatialQuery(formData);
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts
index 60b0f122fb..7d220b1ac0 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/index.ts
@@ -17,12 +17,13 @@
* under the License.
*/
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
-import transformProps from '../../transformProps';
-import controlPanel from './controlPanel';
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'),
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
name: t('deck.gl Contour'),
thumbnail,
thumbnailDark,
- useLegacyApi: true,
tags: [t('deckGL'), t('Spatial'), t('Comparison')],
behaviors: [Behavior.InteractiveChart],
});
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
export default class ContourChartPlugin extends ChartPlugin {
constructor() {
super({
+ buildQuery,
loadChart: () => import('./Contour'),
controlPanel,
metadata,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/transformProps.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/transformProps.ts
new file mode 100644
index 0000000000..1ca8144242
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Contour/transformProps.ts
@@ -0,0 +1,21 @@
+/**
+ * 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 { transformSpatialProps } from '../spatialUtils';
+
+export default transformSpatialProps;
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx
index da1367a189..72faceb32a 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx
@@ -76,7 +76,7 @@ export const getLayer: GetLayerType<GridLayer> = function ({
const colorSchemeType = fd.color_scheme_type;
const colorRange = getColorRange({
- defaultBreakpointsColor: fd.deafult_breakpoint_color,
+ defaultBreakpointsColor: fd.default_breakpoint_color,
colorSchemeType,
colorScale,
colorBreakpoints,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/buildQuery.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/buildQuery.ts
new file mode 100644
index 0000000000..fdd73b1964
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/buildQuery.ts
@@ -0,0 +1,27 @@
+/**
+ * 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 { SpatialFormData, buildSpatialQuery } from '../spatialUtils';
+
+export interface DeckGridFormData extends SpatialFormData {
+ extruded?: boolean;
+}
+
+export default function buildQuery(formData: DeckGridFormData) {
+ return buildSpatialQuery(formData);
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/index.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/index.ts
index 7570121deb..18144934ac 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/index.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/index.ts
@@ -21,7 +21,8 @@ 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 transformProps from './transformProps';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
- useLegacyApi: true,
tags: [t('deckGL'), t('3D'), t('Comparison')],
behaviors: [Behavior.InteractiveChart],
});
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
export default class GridChartPlugin extends ChartPlugin {
constructor() {
super({
+ buildQuery,
loadChart: () => import('./Grid'),
controlPanel,
metadata,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/transformProps.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/transformProps.ts
new file mode 100644
index 0000000000..4b8f437d7f
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/transformProps.ts
@@ -0,0 +1,24 @@
+/**
+ * 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 { transformSpatialProps } from '../spatialUtils';
+
+export default function transformProps(chartProps: ChartProps) {
+ return transformSpatialProps(chartProps);
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/Heatmap.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/Heatmap.tsx
index 68bdfac85d..03e163ea00 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/Heatmap.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/Heatmap.tsx
@@ -126,7 +126,7 @@ export const getLayer: GetLayerType<HeatmapLayer> = ({
const colorSchemeType = fd.color_scheme_type;
const colorRange = getColorRange({
- defaultBreakpointsColor: fd.deafult_breakpoint_color,
+ defaultBreakpointsColor: fd.default_breakpoint_color,
colorBreakpoints: fd.color_breakpoints,
fixedColor: fd.color_picker,
colorSchemeType,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/buildQuery.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/buildQuery.ts
new file mode 100644
index 0000000000..94607704ac
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/buildQuery.ts
@@ -0,0 +1,23 @@
+/**
+ * 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 { SpatialFormData, buildSpatialQuery } from '../spatialUtils';
+
+export default function buildQuery(formData: SpatialFormData) {
+ return buildSpatialQuery(formData);
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/index.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/index.ts
index 418e08daa4..23fc2ad58a 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/index.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/index.ts
@@ -17,12 +17,13 @@
* under the License.
*/
import { t, ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
-import transformProps from '../../transformProps';
-import controlPanel from './controlPanel';
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'),
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
name: t('deck.gl Heatmap'),
thumbnail,
thumbnailDark,
- useLegacyApi: true,
tags: [t('deckGL'), t('Spatial'), t('Comparison')],
behaviors: [Behavior.InteractiveChart],
});
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
export default class HeatmapChartPlugin extends ChartPlugin {
constructor() {
super({
+ buildQuery,
loadChart: () => import('./Heatmap'),
controlPanel,
metadata,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/transformProps.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/transformProps.ts
new file mode 100644
index 0000000000..4b8f437d7f
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Heatmap/transformProps.ts
@@ -0,0 +1,24 @@
+/**
+ * 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 { transformSpatialProps } from '../spatialUtils';
+
+export default function transformProps(chartProps: ChartProps) {
+ return transformSpatialProps(chartProps);
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx
index 9377ee75b1..1f1e35f3dc 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx
@@ -75,7 +75,7 @@ export const getLayer: GetLayerType<HexagonLayer> = function
({
const colorSchemeType = fd.color_scheme_type;
const colorRange = getColorRange({
- defaultBreakpointsColor: fd.deafult_breakpoint_color,
+ defaultBreakpointsColor: fd.default_breakpoint_color,
colorBreakpoints: fd.color_breakpoints,
fixedColor: fd.color_picker,
colorSchemeType,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/buildQuery.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/buildQuery.ts
new file mode 100644
index 0000000000..d5b9a56a13
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/buildQuery.ts
@@ -0,0 +1,29 @@
+/**
+ * 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 { SpatialFormData, buildSpatialQuery } from '../spatialUtils';
+
+export interface DeckHexFormData extends SpatialFormData {
+ extruded?: boolean;
+ js_agg_function?: string;
+ grid_size?: number;
+}
+
+export default function buildQuery(formData: DeckHexFormData) {
+ return buildSpatialQuery(formData);
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/index.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/index.ts
index 2712e847db..fda3cff9b4 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/index.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/index.ts
@@ -21,7 +21,8 @@ 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 transformProps from './transformProps';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
name: t('deck.gl 3D Hexagon'),
thumbnail,
thumbnailDark,
- useLegacyApi: true,
tags: [t('deckGL'), t('3D'), t('Geo'), t('Comparison')],
behaviors: [Behavior.InteractiveChart],
});
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
export default class HexChartPlugin extends ChartPlugin {
constructor() {
super({
+ buildQuery,
loadChart: () => import('./Hex'),
controlPanel,
metadata,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/transformProps.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/transformProps.ts
new file mode 100644
index 0000000000..4b8f437d7f
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/transformProps.ts
@@ -0,0 +1,24 @@
+/**
+ * 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 { transformSpatialProps } from '../spatialUtils';
+
+export default function transformProps(chartProps: ChartProps) {
+ return transformSpatialProps(chartProps);
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/buildQuery.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/buildQuery.ts
new file mode 100644
index 0000000000..b22ef0b6ee
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/buildQuery.ts
@@ -0,0 +1,95 @@
+/**
+ * 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,
+ QueryFormColumn,
+} from '@superset-ui/core';
+import { addNullFilters, addTooltipColumnsToQuery } from '../buildQueryUtils';
+
+export interface DeckPathFormData extends SqlaFormData {
+ line_column?: string;
+ line_type?: 'polyline' | 'json' | 'geohash';
+ metric?: string;
+ reverse_long_lat?: boolean;
+ js_columns?: string[];
+ tooltip_contents?: unknown[];
+ tooltip_template?: string;
+}
+
+export default function buildQuery(formData: DeckPathFormData) {
+ const { line_column, metric, js_columns, tooltip_contents } = formData;
+
+ if (!line_column) {
+ throw new Error('Line column is required for Path charts');
+ }
+
+ return buildQueryContext(formData, {
+ buildQuery: baseQueryObject => {
+ const columns = ensureIsArray(
+ baseQueryObject.columns || [],
+ ) as QueryFormColumn[];
+ const metrics = ensureIsArray(baseQueryObject.metrics || []);
+ const groupby = ensureIsArray(
+ baseQueryObject.groupby || [],
+ ) as QueryFormColumn[];
+ const jsColumns = ensureIsArray(js_columns || []);
+
+ if (baseQueryObject.metrics?.length || metric) {
+ if (metric && !metrics.includes(metric)) {
+ metrics.push(metric);
+ }
+ if (!groupby.includes(line_column)) {
+ groupby.push(line_column);
+ }
+ } else if (!columns.includes(line_column)) {
+ columns.push(line_column);
+ }
+
+ jsColumns.forEach(col => {
+ if (!columns.includes(col) && !groupby.includes(col)) {
+ columns.push(col);
+ }
+ });
+
+ const finalColumns = addTooltipColumnsToQuery(columns, tooltip_contents);
+ const finalGroupby = addTooltipColumnsToQuery(groupby, tooltip_contents);
+
+ const filters = addNullFilters(
+ ensureIsArray(baseQueryObject.filters || []),
+ [line_column],
+ );
+
+ const isTimeseries = Boolean(formData.time_grain_sqla);
+
+ return [
+ {
+ ...baseQueryObject,
+ columns: finalColumns,
+ metrics,
+ groupby: finalGroupby,
+ filters,
+ is_timeseries: isTimeseries,
+ row_limit: baseQueryObject.row_limit,
+ },
+ ];
+ },
+ });
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/index.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/index.ts
index fd930a2780..0c5ea4b421 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/index.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/index.ts
@@ -21,7 +21,8 @@ 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 transformProps from './transformProps';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
@@ -32,7 +33,6 @@ const metadata = new ChartMetadata({
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
- useLegacyApi: true,
tags: [t('deckGL'), t('Web')],
behaviors: [Behavior.InteractiveChart],
});
@@ -40,6 +40,7 @@ const metadata = new ChartMetadata({
export default class PathChartPlugin extends ChartPlugin {
constructor() {
super({
+ buildQuery,
loadChart: () => import('./Path'),
controlPanel,
metadata,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/transformProps.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/transformProps.ts
new file mode 100644
index 0000000000..702ca92b4a
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Path/transformProps.ts
@@ -0,0 +1,166 @@
+/**
+ * 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, DTTM_ALIAS } from '@superset-ui/core';
+import { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils';
+import {
+ createBaseTransformResult,
+ getRecordsFromQuery,
+ getMetricLabelFromFormData,
+ parseMetricValue,
+ addPropertiesToFeature,
+} from '../transformUtils';
+import { DeckPathFormData } from './buildQuery';
+
+declare global {
+ interface Window {
+ polyline?: {
+ decode: (data: string) => [number, number][];
+ };
+ geohash?: {
+ decode: (data: string) => { longitude: number; latitude: number };
+ };
+ }
+}
+
+export interface DeckPathTransformPropsFormData extends DeckPathFormData {
+ js_data_mutator?: string;
+ js_tooltip?: string;
+ js_onclick_href?: string;
+}
+
+interface PathFeature {
+ path: [number, number][];
+ metric?: number;
+ timestamp?: unknown;
+ extraProps?: Record<string, unknown>;
+ [key: string]: unknown;
+}
+
+const decoders = {
+ json: (data: string): [number, number][] => {
+ try {
+ const parsed = JSON.parse(data);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch (error) {
+ return [];
+ }
+ },
+ polyline: (data: string): [number, number][] => {
+ try {
+ if (typeof window !== 'undefined' && window.polyline) {
+ return window.polyline.decode(data);
+ }
+ return [];
+ } catch (error) {
+ return [];
+ }
+ },
+ geohash: (data: string): [number, number][] => {
+ try {
+ if (typeof window !== 'undefined' && window.geohash) {
+ const decoded = window.geohash.decode(data);
+ return [[decoded.longitude, decoded.latitude]];
+ }
+ return [];
+ } catch (error) {
+ return [];
+ }
+ },
+};
+
+function processPathData(
+ records: DataRecord[],
+ lineColumn: string,
+ lineType: 'polyline' | 'json' | 'geohash' = 'json',
+ reverseLongLat: boolean = false,
+ metricLabel?: string,
+ jsColumns?: string[],
+): PathFeature[] {
+ if (!records.length || !lineColumn) {
+ return [];
+ }
+
+ const decoder = decoders[lineType] || decoders.json;
+ const excludeKeys = new Set(
+ [
+ lineType !== 'geohash' ? lineColumn : undefined,
+ 'timestamp',
+ DTTM_ALIAS,
+ metricLabel,
+ ...(jsColumns || []),
+ ].filter(Boolean) as string[],
+ );
+
+ return records.map(record => {
+ const lineData = record[lineColumn];
+ let path: [number, number][] = [];
+
+ if (lineData) {
+ path = decoder(String(lineData));
+ if (reverseLongLat && path.length > 0) {
+ path = path.map(([lng, lat]) => [lat, lng]);
+ }
+ }
+
+ let feature: PathFeature = {
+ path,
+ timestamp: record[DTTM_ALIAS],
+ extraProps: {},
+ };
+
+ if (metricLabel && record[metricLabel] != null) {
+ const metricValue = parseMetricValue(record[metricLabel]);
+ if (metricValue !== undefined) {
+ feature.metric = metricValue;
+ }
+ }
+
+ feature = addJsColumnsToExtraProps(feature, record, jsColumns);
+ feature = addPropertiesToFeature(feature, record, excludeKeys);
+ return feature;
+ });
+}
+
+export default function transformProps(chartProps: ChartProps) {
+ const { rawFormData: formData } = chartProps;
+ const {
+ line_column,
+ line_type = 'json',
+ metric,
+ reverse_long_lat = false,
+ js_columns,
+ } = formData as DeckPathTransformPropsFormData;
+
+ const metricLabel = getMetricLabelFromFormData(metric);
+ const records = getRecordsFromQuery(chartProps.queriesData);
+ const features = processPathData(
+ records,
+ line_column || '',
+ line_type,
+ reverse_long_lat,
+ metricLabel,
+ js_columns,
+ ).reverse();
+
+ return createBaseTransformResult(
+ chartProps,
+ features,
+ metricLabel ? [metricLabel] : [],
+ );
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
index 630a2639d0..69c21e9079 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx
@@ -118,7 +118,7 @@ export const getLayer: GetLayerType<PolygonLayer> =
function ({
fd.fill_color_picker;
const sc: { r: number; g: number; b: number; a: number } =
fd.stroke_color_picker;
- const defaultBreakpointColor = fd.deafult_breakpoint_color;
+ const defaultBreakpointColor = fd.default_breakpoint_color;
let data = [...payload.data.features];
if (fd.js_data_mutator) {
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/buildQuery.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/buildQuery.ts
new file mode 100644
index 0000000000..257096525c
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/buildQuery.ts
@@ -0,0 +1,111 @@
+/**
+ * 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,
+} 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;
+ };
+ 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?.value) {
+ 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/legacy-preset-chart-deckgl/src/layers/Polygon/index.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/index.ts
index 2f793a4539..fa89cc4474 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/index.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/index.ts
@@ -21,7 +21,8 @@ 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 transformProps from './transformProps';
+import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
- useLegacyApi: true,
tags: [t('deckGL'), t('3D'), t('Multi-Dimensions'), t('Geo')],
behaviors: [Behavior.InteractiveChart],
});
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
export default class PolygonChartPlugin extends ChartPlugin {
constructor() {
super({
+ buildQuery,
loadChart: () => import('./Polygon'),
controlPanel,
metadata,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts
new file mode 100644
index 0000000000..b1c73b72d5
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts
@@ -0,0 +1,143 @@
+/**
+ * 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 { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils';
+import {
+ createBaseTransformResult,
+ getRecordsFromQuery,
+ getMetricLabelFromFormData,
+ parseMetricValue,
+ addPropertiesToFeature,
+} from '../transformUtils';
+import { DeckPolygonFormData } from './buildQuery';
+
+interface PolygonFeature {
+ polygon?: number[][];
+ name?: string;
+ elevation?: number;
+ extraProps?: Record<string, unknown>;
+ metrics?: Record<string, number | string>;
+}
+
+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);
+ const elevationLabel = getMetricLabelFromFormData(point_radius_fixed);
+ 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<string, unknown>,
+ 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 (Array.isArray(parsed)) {
+ polygonCoords = parsed;
+ } else {
+ return null;
+ }
+ break;
+ }
+ case 'geohash':
+ 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 (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);
+}
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
new file mode 100644
index 0000000000..66b7146190
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/buildQuery.ts
@@ -0,0 +1,105 @@
+/**
+ * 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,
+ QueryFormOrderBy,
+ SqlaFormData,
+ QueryFormColumn,
+ QueryObject,
+} from '@superset-ui/core';
+import {
+ getSpatialColumns,
+ addSpatialNullFilters,
+ SpatialFormData,
+} from '../spatialUtils';
+import {
+ addJsColumnsToColumns,
+ processMetricsArray,
+ addTooltipColumnsToQuery,
+} from '../buildQueryUtils';
+
+export interface DeckScatterFormData
+ extends Omit<SpatialFormData, 'color_picker'>,
+ SqlaFormData {
+ point_radius_fixed?: {
+ value?: string;
+ };
+ multiplier?: number;
+ point_unit?: string;
+ min_radius?: number;
+ max_radius?: number;
+ color_picker?: { r: number; g: number; b: number; a: number };
+ category_name?: string;
+}
+
+export default function buildQuery(formData: DeckScatterFormData) {
+ const {
+ spatial,
+ point_radius_fixed,
+ category_name,
+ 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 (category_name) {
+ columns.push(category_name);
+ }
+
+ 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 metrics = processMetricsArray([point_radius_fixed?.value]);
+ const filters = addSpatialNullFilters(
+ spatial,
+ ensureIsArray(baseQueryObject.filters || []),
+ );
+
+ const orderby = point_radius_fixed?.value
+ ? ([[point_radius_fixed.value, 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/legacy-preset-chart-deckgl/src/layers/Scatter/index.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/index.ts
index 4f32f4a1c7..cef908989a 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/index.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/index.ts
@@ -21,7 +21,8 @@ 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 transformProps from './transformProps';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
- useLegacyApi: true,
tags: [
t('deckGL'),
t('Comparison'),
@@ -50,6 +50,7 @@ const metadata = new ChartMetadata({
export default class ScatterChartPlugin extends ChartPlugin {
constructor() {
super({
+ buildQuery,
loadChart: () => import('./Scatter'),
controlPanel,
metadata,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/transformProps.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/transformProps.ts
new file mode 100644
index 0000000000..baadec33c9
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Scatter/transformProps.ts
@@ -0,0 +1,116 @@
+/**
+ * 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';
+
+interface ScatterPoint {
+ position: [number, number];
+ radius?: number;
+ color?: [number, number, number, number];
+ cat_color?: string;
+ metric?: number;
+ extraProps?: Record<string, unknown>;
+ [key: string]: unknown;
+}
+
+function processScatterData(
+ records: DataRecord[],
+ spatial: DeckScatterFormData['spatial'],
+ radiusMetricLabel?: string,
+ categoryColumn?: string,
+ jsColumns?: string[],
+): 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 || {},
+ };
+
+ 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;
+ });
+}
+
+export default function transformProps(chartProps: ChartProps) {
+ const { rawFormData: formData } = chartProps;
+ const { spatial, point_radius_fixed, category_name, js_columns } =
+ formData as DeckScatterFormData;
+
+ const radiusMetricLabel = getMetricLabelFromFormData(point_radius_fixed);
+ const records = getRecordsFromQuery(chartProps.queriesData);
+ const features = processScatterData(
+ records,
+ spatial,
+ radiusMetricLabel,
+ category_name,
+ js_columns,
+ );
+
+ return createBaseTransformResult(
+ chartProps,
+ features,
+ radiusMetricLabel ? [radiusMetricLabel] : [],
+ );
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
index 9ea46cd453..bbade67044 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/Screengrid.tsx
@@ -123,7 +123,7 @@ export const getLayer: GetLayerType<ScreenGridLayer> =
function ({
const colorSchemeType = fd.color_scheme_type as ColorSchemeType & 'default';
const colorRange = getColorRange({
- defaultBreakpointsColor: fd.deafult_breakpoint_color,
+ defaultBreakpointsColor: fd.default_breakpoint_color,
colorBreakpoints: fd.color_breakpoints,
fixedColor: fd.color_picker,
colorSchemeType,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/buildQuery.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/buildQuery.ts
new file mode 100644
index 0000000000..94607704ac
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/buildQuery.ts
@@ -0,0 +1,23 @@
+/**
+ * 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 { SpatialFormData, buildSpatialQuery } from '../spatialUtils';
+
+export default function buildQuery(formData: SpatialFormData) {
+ return buildSpatialQuery(formData);
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/index.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/index.ts
index 574d50dfff..87758e37cf 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/index.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/index.ts
@@ -21,7 +21,8 @@ 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 transformProps from './transformProps';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
@@ -34,7 +35,6 @@ const metadata = new ChartMetadata({
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
- useLegacyApi: true,
tags: [t('deckGL'), t('Comparison'), t('Intensity'), t('Density')],
behaviors: [Behavior.InteractiveChart],
});
@@ -42,6 +42,7 @@ const metadata = new ChartMetadata({
export default class ScreengridChartPlugin extends ChartPlugin {
constructor() {
super({
+ buildQuery,
loadChart: () => import('./Screengrid'),
controlPanel,
metadata,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/transformProps.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/transformProps.ts
new file mode 100644
index 0000000000..4b8f437d7f
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Screengrid/transformProps.ts
@@ -0,0 +1,24 @@
+/**
+ * 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 { transformSpatialProps } from '../spatialUtils';
+
+export default function transformProps(chartProps: ChartProps) {
+ return transformSpatialProps(chartProps);
+}
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
new file mode 100644
index 0000000000..f61a71ede7
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/buildQueryUtils.ts
@@ -0,0 +1,142 @@
+/**
+ * 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 {
+ getMetricLabel,
+ QueryObjectFilterClause,
+ QueryFormColumn,
+ getColumnLabel,
+} from '@superset-ui/core';
+
+export function addJsColumnsToColumns(
+ columns: string[],
+ jsColumns?: string[],
+ existingColumns?: string[],
+): string[] {
+ if (!jsColumns?.length) return columns;
+
+ const allExisting = new Set([...columns, ...(existingColumns || [])]);
+ const result = [...columns];
+
+ jsColumns.forEach(col => {
+ if (!allExisting.has(col)) {
+ result.push(col);
+ allExisting.add(col);
+ }
+ });
+
+ return result;
+}
+
+export function addNullFilters(
+ filters: QueryObjectFilterClause[],
+ columnNames: string[],
+): QueryObjectFilterClause[] {
+ const existingFilters = new Set(
+ filters
+ .filter(filter => filter.op === 'IS NOT NULL')
+ .map(filter => filter.col),
+ );
+
+ const nullFilters: QueryObjectFilterClause[] = columnNames
+ .filter(col => !existingFilters.has(col))
+ .map(col => ({
+ col,
+ op: 'IS NOT NULL' as const,
+ }));
+
+ return [...filters, ...nullFilters];
+}
+
+export function addMetricNullFilter(
+ filters: QueryObjectFilterClause[],
+ metric?: string,
+): QueryObjectFilterClause[] {
+ if (!metric) return filters;
+ return addNullFilters(filters, [getMetricLabel(metric)]);
+}
+
+export function ensureColumnsUnique(columns: string[]): string[] {
+ return [...new Set(columns)];
+}
+
+export function addColumnsIfNotExists(
+ baseColumns: string[],
+ newColumns: string[],
+): string[] {
+ const existing = new Set(baseColumns);
+ const result = [...baseColumns];
+
+ newColumns.forEach(col => {
+ if (!existing.has(col)) {
+ result.push(col);
+ existing.add(col);
+ }
+ });
+
+ 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 [];
+ }
+
+ const columns: string[] = [];
+
+ tooltipContents.forEach(item => {
+ if (typeof item === 'string') {
+ columns.push(item);
+ } else if (item && typeof item === 'object') {
+ const objItem = item as Record<string, unknown>;
+ if (
+ objItem.item_type === 'column' &&
+ typeof objItem.column_name === 'string'
+ ) {
+ columns.push(objItem.column_name);
+ }
+ }
+ });
+
+ return columns;
+}
+
+export function addTooltipColumnsToQuery(
+ baseColumns: QueryFormColumn[],
+ tooltipContents?: unknown[],
+): QueryFormColumn[] {
+ const tooltipColumns = extractTooltipColumns(tooltipContents);
+
+ const baseColumnLabels = baseColumns.map(getColumnLabel);
+ const existingLabels = new Set(baseColumnLabels);
+
+ const result: QueryFormColumn[] = [...baseColumns];
+
+ tooltipColumns.forEach(col => {
+ if (!existingLabels.has(col)) {
+ result.push(col);
+ existingLabels.add(col);
+ }
+ });
+
+ return result;
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.test.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.test.ts
new file mode 100644
index 0000000000..c169ed4c33
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.test.ts
@@ -0,0 +1,604 @@
+/**
+ * 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,
+ DatasourceType,
+ QueryObjectFilterClause,
+ SupersetTheme,
+} from '@superset-ui/core';
+import { decode } from 'ngeohash';
+
+import {
+ getSpatialColumns,
+ addSpatialNullFilters,
+ buildSpatialQuery,
+ processSpatialData,
+ transformSpatialProps,
+ SpatialFormData,
+} from './spatialUtils';
+
+jest.mock('ngeohash', () => ({
+ decode: jest.fn(),
+}));
+
+jest.mock('@superset-ui/core', () => ({
+ ...jest.requireActual('@superset-ui/core'),
+ buildQueryContext: jest.fn(),
+ getMetricLabel: jest.fn(),
+ ensureIsArray: jest.fn(arr => arr || []),
+ normalizeOrderBy: jest.fn(({ orderby }) => ({ orderby })),
+}));
+
+// Mock DOM element for bootstrap data
+const mockBootstrapData = {
+ common: {
+ conf: {
+ MAPBOX_API_KEY: 'test_api_key',
+ },
+ },
+};
+
+Object.defineProperty(document, 'getElementById', {
+ value: jest.fn().mockReturnValue({
+ getAttribute: jest.fn().mockReturnValue(JSON.stringify(mockBootstrapData)),
+ }),
+ writable: true,
+});
+
+const mockDecode = decode as jest.MockedFunction<typeof decode>;
+
+describe('spatialUtils', () => {
+ test('getSpatialColumns returns correct columns for latlong type', () => {
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ };
+
+ const result = getSpatialColumns(spatial);
+ expect(result).toEqual(['longitude', 'latitude']);
+ });
+
+ test('getSpatialColumns returns correct columns for delimited type', () => {
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'delimited',
+ lonlatCol: 'coordinates',
+ };
+
+ const result = getSpatialColumns(spatial);
+ expect(result).toEqual(['coordinates']);
+ });
+
+ test('getSpatialColumns returns correct columns for geohash type', () => {
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'geohash',
+ geohashCol: 'geohash_code',
+ };
+
+ const result = getSpatialColumns(spatial);
+ expect(result).toEqual(['geohash_code']);
+ });
+
+ test('getSpatialColumns throws error when spatial is null', () => {
+ expect(() => getSpatialColumns(null as any)).toThrow('Bad spatial key');
+ });
+
+ test('getSpatialColumns throws error when spatial type is missing', () => {
+ const spatial = {} as SpatialFormData['spatial'];
+ expect(() => getSpatialColumns(spatial)).toThrow('Bad spatial key');
+ });
+
+ test('getSpatialColumns throws error when latlong columns are missing', ()
=> {
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'latlong',
+ };
+ expect(() => getSpatialColumns(spatial)).toThrow(
+ 'Longitude and latitude columns are required for latlong type',
+ );
+ });
+
+ test('getSpatialColumns throws error when delimited column is missing', ()
=> {
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'delimited',
+ };
+ expect(() => getSpatialColumns(spatial)).toThrow(
+ 'Longitude/latitude column is required for delimited type',
+ );
+ });
+
+ test('getSpatialColumns throws error when geohash column is missing', () => {
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'geohash',
+ };
+ expect(() => getSpatialColumns(spatial)).toThrow(
+ 'Geohash column is required for geohash type',
+ );
+ });
+
+ test('getSpatialColumns throws error for unknown spatial type', () => {
+ const spatial = {
+ type: 'unknown',
+ } as any;
+ expect(() => getSpatialColumns(spatial)).toThrow(
+ 'Unknown spatial type: unknown',
+ );
+ });
+
+ test('addSpatialNullFilters adds null filters for spatial columns', () => {
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ };
+ const existingFilters: QueryObjectFilterClause[] = [
+ { col: 'other_col', op: '==', val: 'test' },
+ ];
+
+ const result = addSpatialNullFilters(spatial, existingFilters);
+
+ expect(result).toEqual([
+ { col: 'other_col', op: '==', val: 'test' },
+ { col: 'longitude', op: 'IS NOT NULL', val: null },
+ { col: 'latitude', op: 'IS NOT NULL', val: null },
+ ]);
+ });
+
+ test('addSpatialNullFilters returns original filters when spatial is null',
() => {
+ const existingFilters: QueryObjectFilterClause[] = [
+ { col: 'test_col', op: '==', val: 'test' },
+ ];
+
+ const result = addSpatialNullFilters(null as any, existingFilters);
+ expect(result).toBe(existingFilters);
+ });
+
+ test('addSpatialNullFilters works with empty filters array', () => {
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'delimited',
+ lonlatCol: 'coordinates',
+ };
+
+ const result = addSpatialNullFilters(spatial, []);
+
+ expect(result).toEqual([
+ { col: 'coordinates', op: 'IS NOT NULL', val: null },
+ ]);
+ });
+
+ test('buildSpatialQuery throws error when spatial is missing', () => {
+ const formData = {} as SpatialFormData;
+
+ expect(() => buildSpatialQuery(formData)).toThrow(
+ 'Spatial configuration is required for this chart',
+ );
+ });
+
+ test('buildSpatialQuery calls buildQueryContext with correct parameters', ()
=> {
+ const mockBuildQueryContext =
+ jest.requireMock('@superset-ui/core').buildQueryContext;
+ const formData: SpatialFormData = {
+ spatial: {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ },
+ size: 'count',
+ js_columns: ['extra_col'],
+ } as SpatialFormData;
+
+ buildSpatialQuery(formData);
+
+ expect(mockBuildQueryContext).toHaveBeenCalledWith(formData, {
+ buildQuery: expect.any(Function),
+ });
+ });
+
+ test('processSpatialData processes latlong data correctly', () => {
+ const records = [
+ { longitude: -122.4, latitude: 37.8, count: 10, extra: 'test1' },
+ { longitude: -122.5, latitude: 37.9, count: 20, extra: 'test2' },
+ ];
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ };
+ const metricLabel = 'count';
+ const jsColumns = ['extra'];
+
+ const result = processSpatialData(records, spatial, metricLabel,
jsColumns);
+
+ expect(result).toHaveLength(2);
+ expect(result[0]).toEqual({
+ position: [-122.4, 37.8],
+ weight: 10,
+ extraProps: { extra: 'test1' },
+ });
+ expect(result[1]).toEqual({
+ position: [-122.5, 37.9],
+ weight: 20,
+ extraProps: { extra: 'test2' },
+ });
+ });
+
+ test('processSpatialData processes delimited data correctly', () => {
+ const records = [
+ { coordinates: '-122.4,37.8', count: 15 },
+ { coordinates: '-122.5,37.9', count: 25 },
+ ];
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'delimited',
+ lonlatCol: 'coordinates',
+ };
+
+ const result = processSpatialData(records, spatial, 'count');
+
+ expect(result).toHaveLength(2);
+ expect(result[0]).toEqual({
+ position: [-122.4, 37.8],
+ weight: 15,
+ extraProps: {},
+ });
+ });
+
+ test('processSpatialData processes geohash data correctly', () => {
+ mockDecode.mockReturnValue({
+ latitude: 37.8,
+ longitude: -122.4,
+ error: {
+ latitude: 0,
+ longitude: 0,
+ },
+ });
+
+ const records = [{ geohash: 'dr5regw3p', count: 30 }];
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'geohash',
+ geohashCol: 'geohash',
+ };
+
+ const result = processSpatialData(records, spatial, 'count');
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toEqual({
+ position: [-122.4, 37.8],
+ weight: 30,
+ extraProps: {},
+ });
+ expect(mockDecode).toHaveBeenCalledWith('dr5regw3p');
+ });
+
+ test('processSpatialData reverses coordinates when reverseCheckbox is true',
() => {
+ const records = [{ longitude: -122.4, latitude: 37.8, count: 10 }];
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ reverseCheckbox: true,
+ };
+
+ const result = processSpatialData(records, spatial, 'count');
+
+ expect(result[0].position).toEqual([37.8, -122.4]);
+ });
+
+ test('processSpatialData handles invalid coordinates', () => {
+ const records = [
+ { longitude: 'invalid', latitude: 37.8, count: 10 },
+ { longitude: -122.4, latitude: NaN, count: 20 },
+ // 'latlong' spatial type expects longitude/latitude fields
+ // so records with 'coordinates' should be filtered out
+ { coordinates: 'invalid,coords', count: 30 },
+ { coordinates: '-122.4,invalid', count: 40 },
+ ];
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ };
+
+ const result = processSpatialData(records, spatial, 'count');
+
+ expect(result).toHaveLength(0);
+ });
+
+ test('processSpatialData handles missing metric values', () => {
+ const records = [
+ { longitude: -122.4, latitude: 37.8, count: null },
+ { longitude: -122.5, latitude: 37.9 },
+ { longitude: -122.6, latitude: 38.0, count: 'invalid' },
+ ];
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ };
+
+ const result = processSpatialData(records, spatial, 'count');
+
+ expect(result).toHaveLength(3);
+ expect(result[0].weight).toBe(1);
+ expect(result[1].weight).toBe(1);
+ expect(result[2].weight).toBe(1);
+ });
+
+ test('processSpatialData returns empty array for empty records', () => {
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ };
+
+ const result = processSpatialData([], spatial);
+
+ expect(result).toEqual([]);
+ });
+
+ test('processSpatialData returns empty array when spatial is null', () => {
+ const records = [{ longitude: -122.4, latitude: 37.8 }];
+
+ const result = processSpatialData(records, null as any);
+
+ expect(result).toEqual([]);
+ });
+
+ test('processSpatialData handles delimited coordinate edge cases', () => {
+ const records = [
+ { coordinates: '', count: 10 },
+ { coordinates: null, count: 20 },
+ { coordinates: undefined, count: 30 },
+ { coordinates: '-122.4', count: 40 }, // only one coordinate
+ { coordinates: 'a,b', count: 50 }, // non-numeric
+ { coordinates: ' -122.4 , 37.8 ', count: 60 }, // with spaces
+ ];
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'delimited',
+ lonlatCol: 'coordinates',
+ };
+
+ const result = processSpatialData(records, spatial, 'count');
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toEqual({
+ position: [-122.4, 37.8],
+ weight: 60,
+ extraProps: {},
+ });
+ });
+
+ test('processSpatialData copies additional properties correctly', () => {
+ const records = [
+ {
+ longitude: -122.4,
+ latitude: 37.8,
+ count: 10,
+ category: 'A',
+ description: 'Test location',
+ extra_col: 'extra_value',
+ },
+ ];
+ const spatial: SpatialFormData['spatial'] = {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ };
+ const jsColumns = ['extra_col'];
+
+ const result = processSpatialData(records, spatial, 'count', jsColumns);
+
+ expect(result[0]).toEqual({
+ position: [-122.4, 37.8],
+ weight: 10,
+ extraProps: { extra_col: 'extra_value' },
+ category: 'A',
+ description: 'Test location',
+ });
+
+ expect(result[0]).not.toHaveProperty('longitude');
+ expect(result[0]).not.toHaveProperty('latitude');
+ expect(result[0]).not.toHaveProperty('count');
+ expect(result[0]).not.toHaveProperty('extra_col');
+ });
+
+ test('transformSpatialProps transforms chart props correctly', () => {
+ const mockGetMetricLabel =
+ jest.requireMock('@superset-ui/core').getMetricLabel;
+ mockGetMetricLabel.mockReturnValue('count_label');
+
+ const chartProps: ChartProps = {
+ datasource: {
+ id: 1,
+ type: DatasourceType.Table,
+ columns: [],
+ name: '',
+ metrics: [],
+ },
+ height: 400,
+ width: 600,
+ hooks: {
+ onAddFilter: jest.fn(),
+ onContextMenu: jest.fn(),
+ setControlValue: jest.fn(),
+ setDataMask: jest.fn(),
+ },
+ queriesData: [
+ {
+ data: [
+ { longitude: -122.4, latitude: 37.8, count: 10 },
+ { longitude: -122.5, latitude: 37.9, count: 20 },
+ ],
+ },
+ ],
+ rawFormData: {
+ spatial: {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ },
+ size: 'count',
+ js_columns: [],
+ viewport: {
+ zoom: 10,
+ latitude: 37.8,
+ longitude: -122.4,
+ },
+ } as unknown as SpatialFormData,
+ filterState: {},
+ emitCrossFilters: true,
+ annotationData: {},
+ rawDatasource: {},
+ initialValues: {},
+ formData: {
+ spatial: {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ },
+ size: 'count',
+ js_columns: [],
+ viewport: {
+ zoom: 10,
+ latitude: 37.8,
+ longitude: -122.4,
+ },
+ },
+ ownState: {},
+ behaviors: [],
+ theme: {} as unknown as SupersetTheme,
+ };
+
+ const result = transformSpatialProps(chartProps);
+
+ expect(result).toMatchObject({
+ datasource: chartProps.datasource,
+ emitCrossFilters: chartProps.emitCrossFilters,
+ formData: chartProps.rawFormData,
+ height: 400,
+ width: 600,
+ filterState: {},
+ onAddFilter: chartProps.hooks.onAddFilter,
+ onContextMenu: chartProps.hooks.onContextMenu,
+ setControlValue: chartProps.hooks.setControlValue,
+ setDataMask: chartProps.hooks.setDataMask,
+ viewport: {
+ zoom: 10,
+ latitude: 37.8,
+ longitude: -122.4,
+ height: 400,
+ width: 600,
+ },
+ });
+
+ expect(result.payload.data.features).toHaveLength(2);
+ expect(result.payload.data.mapboxApiKey).toBe('test_api_key');
+ expect(result.payload.data.metricLabels).toEqual(['count_label']);
+ });
+
+ test('transformSpatialProps handles missing hooks gracefully', () => {
+ const chartProps: ChartProps = {
+ datasource: {
+ id: 1,
+ type: DatasourceType.Table,
+ columns: [],
+ name: '',
+ metrics: [],
+ },
+ height: 400,
+ width: 600,
+ hooks: {},
+ queriesData: [{ data: [] }],
+ rawFormData: {
+ spatial: {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ },
+ } as SpatialFormData,
+ filterState: {},
+ emitCrossFilters: true,
+ annotationData: {},
+ rawDatasource: {},
+ initialValues: {},
+ formData: {
+ spatial: {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ },
+ },
+ ownState: {},
+ behaviors: [],
+ theme: {} as unknown as SupersetTheme,
+ };
+
+ const result = transformSpatialProps(chartProps);
+
+ expect(typeof result.onAddFilter).toBe('function');
+ expect(typeof result.onContextMenu).toBe('function');
+ expect(typeof result.setControlValue).toBe('function');
+ expect(typeof result.setDataMask).toBe('function');
+ expect(typeof result.setTooltip).toBe('function');
+ });
+
+ test('transformSpatialProps handles missing metric', () => {
+ const mockGetMetricLabel =
+ jest.requireMock('@superset-ui/core').getMetricLabel;
+ mockGetMetricLabel.mockReturnValue(undefined);
+
+ const chartProps: ChartProps = {
+ datasource: {
+ id: 1,
+ type: DatasourceType.Table,
+ columns: [],
+ name: '',
+ metrics: [],
+ },
+ height: 400,
+ width: 600,
+ hooks: {},
+ queriesData: [{ data: [] }],
+ rawFormData: {
+ spatial: {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ },
+ } as SpatialFormData,
+ filterState: {},
+ emitCrossFilters: true,
+ annotationData: {},
+ rawDatasource: {},
+ initialValues: {},
+ formData: {
+ spatial: {
+ type: 'latlong',
+ lonCol: 'longitude',
+ latCol: 'latitude',
+ },
+ },
+ ownState: {},
+ behaviors: [],
+ theme: {} as unknown as SupersetTheme,
+ };
+
+ const result = transformSpatialProps(chartProps);
+
+ expect(result.payload.data.metricLabels).toEqual([]);
+ });
+});
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.ts
new file mode 100644
index 0000000000..28625c5872
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/spatialUtils.ts
@@ -0,0 +1,400 @@
+/**
+ * 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,
+ getMetricLabel,
+ QueryFormData,
+ QueryObjectFilterClause,
+ ensureIsArray,
+ ChartProps,
+ normalizeOrderBy,
+} from '@superset-ui/core';
+import { decode } from 'ngeohash';
+import { addTooltipColumnsToQuery } from './buildQueryUtils';
+
+export interface SpatialConfiguration {
+ type: 'latlong' | 'delimited' | 'geohash';
+ lonCol?: string;
+ latCol?: string;
+ lonlatCol?: string;
+ geohashCol?: string;
+ reverseCheckbox?: boolean;
+}
+
+export interface DataRecord {
+ [key: string]: string | number | null | undefined;
+}
+
+export interface BootstrapData {
+ common?: {
+ conf?: {
+ MAPBOX_API_KEY?: string;
+ };
+ };
+}
+
+export interface SpatialFormData extends QueryFormData {
+ spatial: SpatialConfiguration;
+ size?: string;
+ grid_size?: number;
+ js_data_mutator?: string;
+ js_agg_function?: string;
+ js_columns?: string[];
+ color_scheme?: string;
+ color_scheme_type?: string;
+ color_breakpoints?: number[];
+ default_breakpoint_color?: string;
+ tooltip_contents?: unknown[];
+ tooltip_template?: string;
+ color_picker?: string;
+}
+
+export interface SpatialPoint {
+ position: [number, number];
+ weight: number;
+ extraProps?: Record<string, unknown>;
+ [key: string]: unknown;
+}
+
+export function getSpatialColumns(spatial: SpatialConfiguration): string[] {
+ if (!spatial || !spatial.type) {
+ throw new Error('Bad spatial key');
+ }
+
+ switch (spatial.type) {
+ case 'latlong':
+ if (!spatial.lonCol || !spatial.latCol) {
+ throw new Error(
+ 'Longitude and latitude columns are required for latlong type',
+ );
+ }
+ return [spatial.lonCol, spatial.latCol];
+ case 'delimited':
+ if (!spatial.lonlatCol) {
+ throw new Error(
+ 'Longitude/latitude column is required for delimited type',
+ );
+ }
+ return [spatial.lonlatCol];
+ case 'geohash':
+ if (!spatial.geohashCol) {
+ throw new Error('Geohash column is required for geohash type');
+ }
+ return [spatial.geohashCol];
+ default:
+ throw new Error(`Unknown spatial type: ${spatial.type}`);
+ }
+}
+
+export function addSpatialNullFilters(
+ spatial: SpatialConfiguration,
+ filters: QueryObjectFilterClause[],
+): QueryObjectFilterClause[] {
+ if (!spatial) return filters;
+
+ const spatialColumns = getSpatialColumns(spatial);
+ const nullFilters: QueryObjectFilterClause[] = spatialColumns.map(column =>
({
+ col: column,
+ op: 'IS NOT NULL',
+ val: null,
+ }));
+
+ return [...filters, ...nullFilters];
+}
+
+export function buildSpatialQuery(formData: SpatialFormData) {
+ const { spatial, size: metric, js_columns, tooltip_contents } = formData;
+
+ if (!spatial) {
+ throw new Error(`Spatial configuration is required for this chart`);
+ }
+ return buildQueryContext(formData, {
+ buildQuery: baseQueryObject => {
+ const spatialColumns = getSpatialColumns(spatial);
+ let columns = [...(baseQueryObject.columns || []), ...spatialColumns];
+ const metrics = metric ? [metric] : [];
+
+ if (js_columns?.length) {
+ js_columns.forEach(col => {
+ if (!columns.includes(col)) {
+ columns.push(col);
+ }
+ });
+ }
+
+ columns = addTooltipColumnsToQuery(columns, tooltip_contents);
+
+ const filters = addSpatialNullFilters(
+ spatial,
+ ensureIsArray(baseQueryObject.filters || []),
+ );
+
+ const orderby = metric
+ ? normalizeOrderBy({ orderby: [[metric, false]] }).orderby
+ : baseQueryObject.orderby;
+
+ return [
+ {
+ ...baseQueryObject,
+ columns,
+ metrics,
+ filters,
+ orderby,
+ is_timeseries: false,
+ row_limit: baseQueryObject.row_limit,
+ },
+ ];
+ },
+ });
+}
+
+function parseCoordinates(latlong: string): [number, number] | null {
+ if (!latlong || typeof latlong !== 'string') {
+ return null;
+ }
+
+ try {
+ const coords = latlong.split(',').map(coord => parseFloat(coord.trim()));
+ if (
+ coords.length === 2 &&
+ !Number.isNaN(coords[0]) &&
+ !Number.isNaN(coords[1])
+ ) {
+ return [coords[0], coords[1]];
+ }
+ return null;
+ } catch (error) {
+ return null;
+ }
+}
+
+function reverseGeohashDecode(geohashCode: string): [number, number] | null {
+ if (!geohashCode || typeof geohashCode !== 'string') {
+ return null;
+ }
+
+ try {
+ const { latitude: lat, longitude: lng } = decode(geohashCode);
+ if (
+ Number.isNaN(lat) ||
+ Number.isNaN(lng) ||
+ lat < -90 ||
+ lat > 90 ||
+ lng < -180 ||
+ lng > 180
+ ) {
+ return null;
+ }
+ return [lng, lat];
+ } catch (error) {
+ return null;
+ }
+}
+
+export function addJsColumnsToExtraProps<
+ T extends { extraProps?: Record<string, unknown> },
+>(feature: T, record: DataRecord, jsColumns?: string[]): T {
+ if (!jsColumns?.length) {
+ return feature;
+ }
+
+ const extraProps: Record<string, unknown> = { ...(feature.extraProps ?? {})
};
+
+ jsColumns.forEach(col => {
+ if (record[col] !== undefined) {
+ extraProps[col] = record[col];
+ }
+ });
+
+ return { ...feature, extraProps };
+}
+
+export function processSpatialData(
+ records: DataRecord[],
+ spatial: SpatialConfiguration,
+ metricLabel?: string,
+ jsColumns?: string[],
+): SpatialPoint[] {
+ if (!spatial || !records.length) {
+ return [];
+ }
+
+ const features: SpatialPoint[] = [];
+ const spatialColumns = getSpatialColumns(spatial);
+ const jsColumnsSet = jsColumns ? new Set(jsColumns) : null;
+ const spatialColumnsSet = new Set(spatialColumns);
+
+ for (const record of records) {
+ let position: [number, number] | null = null;
+
+ switch (spatial.type) {
+ case 'latlong':
+ if (spatial.lonCol && spatial.latCol) {
+ const lon = parseFloat(String(record[spatial.lonCol] ?? ''));
+ const lat = parseFloat(String(record[spatial.latCol] ?? ''));
+ if (!Number.isNaN(lon) && !Number.isNaN(lat)) {
+ position = [lon, lat];
+ }
+ }
+ break;
+ case 'delimited':
+ if (spatial.lonlatCol) {
+ position = parseCoordinates(String(record[spatial.lonlatCol] ?? ''));
+ }
+ break;
+ case 'geohash':
+ if (spatial.geohashCol) {
+ const geohashValue = record[spatial.geohashCol];
+ if (geohashValue) {
+ position = reverseGeohashDecode(String(geohashValue));
+ }
+ }
+ break;
+ default:
+ continue;
+ }
+
+ if (!position) {
+ continue;
+ }
+
+ if (spatial.reverseCheckbox) {
+ position = [position[1], position[0]];
+ }
+
+ let weight = 1;
+ if (metricLabel && record[metricLabel] != null) {
+ const metricValue = parseFloat(String(record[metricLabel]));
+ if (!Number.isNaN(metricValue)) {
+ weight = metricValue;
+ }
+ }
+
+ let spatialPoint: SpatialPoint = {
+ position,
+ weight,
+ extraProps: {},
+ };
+
+ spatialPoint = addJsColumnsToExtraProps(spatialPoint, record, jsColumns);
+ Object.keys(record).forEach(key => {
+ if (spatialColumnsSet.has(key)) {
+ return;
+ }
+
+ if (key === metricLabel) {
+ return;
+ }
+
+ if (jsColumnsSet?.has(key)) {
+ return;
+ }
+
+ spatialPoint[key] = record[key];
+ });
+
+ features.push(spatialPoint);
+ }
+
+ return features;
+}
+
+const NOOP = () => {};
+
+export function getMapboxApiKey(mapboxApiKey?: string): string {
+ if (mapboxApiKey) {
+ return mapboxApiKey;
+ }
+
+ if (typeof document !== 'undefined') {
+ try {
+ const appContainer = document.getElementById('app');
+ const dataBootstrap = appContainer?.getAttribute('data-bootstrap');
+ if (dataBootstrap) {
+ const bootstrapData: BootstrapData = JSON.parse(dataBootstrap);
+ return bootstrapData?.common?.conf?.MAPBOX_API_KEY || '';
+ }
+ } catch (error) {
+ throw new Error(
+ `Failed to read MAPBOX_API_KEY from bootstrap data: ${error}`,
+ );
+ }
+ }
+
+ return '';
+}
+
+export function transformSpatialProps(chartProps: ChartProps) {
+ const {
+ datasource,
+ height,
+ hooks,
+ queriesData,
+ rawFormData: formData,
+ width,
+ filterState,
+ emitCrossFilters,
+ } = chartProps;
+
+ const {
+ onAddFilter = NOOP,
+ onContextMenu = NOOP,
+ setControlValue = NOOP,
+ setDataMask = NOOP,
+ } = hooks;
+
+ const { spatial, size: metric, js_columns } = formData as SpatialFormData;
+ const metricLabel = metric ? getMetricLabel(metric) : undefined;
+
+ const queryData = queriesData[0];
+ const records = queryData?.data || [];
+ const features = processSpatialData(
+ records,
+ spatial,
+ metricLabel,
+ js_columns,
+ );
+
+ return {
+ datasource,
+ emitCrossFilters,
+ formData,
+ height,
+ onAddFilter,
+ onContextMenu,
+ payload: {
+ ...queryData,
+ data: {
+ features,
+ mapboxApiKey: getMapboxApiKey(),
+ metricLabels: metricLabel ? [metricLabel] : [],
+ },
+ },
+ setControlValue,
+ filterState,
+ viewport: {
+ ...formData.viewport,
+ height,
+ width,
+ },
+ width,
+ setDataMask,
+ setTooltip: () => {},
+ };
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts
new file mode 100644
index 0000000000..6427db900a
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/transformUtils.ts
@@ -0,0 +1,142 @@
+/**
+ * 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 { getMapboxApiKey, DataRecord } from './spatialUtils';
+
+const NOOP = () => {};
+
+export interface BaseHooks {
+ onAddFilter: ChartProps['hooks']['onAddFilter'];
+ onContextMenu: ChartProps['hooks']['onContextMenu'];
+ setControlValue: ChartProps['hooks']['setControlValue'];
+ setDataMask: ChartProps['hooks']['setDataMask'];
+}
+
+export interface BaseTransformPropsResult {
+ datasource: ChartProps['datasource'];
+ emitCrossFilters: ChartProps['emitCrossFilters'];
+ formData: ChartProps['rawFormData'];
+ height: ChartProps['height'];
+ onAddFilter: ChartProps['hooks']['onAddFilter'];
+ onContextMenu: ChartProps['hooks']['onContextMenu'];
+ payload: {
+ data: {
+ features: unknown[];
+ mapboxApiKey: string;
+ metricLabels?: string[];
+ };
+ [key: string]: unknown;
+ };
+ setControlValue: ChartProps['hooks']['setControlValue'];
+ filterState: ChartProps['filterState'];
+ viewport: {
+ height: number;
+ width: number;
+ [key: string]: unknown;
+ };
+ width: ChartProps['width'];
+ setDataMask: ChartProps['hooks']['setDataMask'];
+ setTooltip: () => void;
+}
+
+export function extractHooks(hooks: ChartProps['hooks']): BaseHooks {
+ return {
+ onAddFilter: hooks?.onAddFilter || NOOP,
+ onContextMenu: hooks?.onContextMenu || NOOP,
+ setControlValue: hooks?.setControlValue || NOOP,
+ setDataMask: hooks?.setDataMask || NOOP,
+ };
+}
+
+export function createBaseTransformResult(
+ chartProps: ChartProps,
+ features: unknown[],
+ metricLabels?: string[],
+): BaseTransformPropsResult {
+ const {
+ datasource,
+ height,
+ queriesData,
+ rawFormData: formData,
+ width,
+ filterState,
+ emitCrossFilters,
+ } = chartProps;
+
+ const hooks = extractHooks(chartProps.hooks);
+ const queryData = queriesData[0];
+
+ return {
+ datasource,
+ emitCrossFilters,
+ formData,
+ height,
+ ...hooks,
+ payload: {
+ ...queryData,
+ data: {
+ features,
+ mapboxApiKey: getMapboxApiKey(),
+ metricLabels: metricLabels || [],
+ },
+ },
+ filterState,
+ viewport: {
+ ...formData.viewport,
+ height,
+ width,
+ },
+ width,
+ setTooltip: NOOP,
+ };
+}
+
+export function getRecordsFromQuery(
+ queriesData: ChartProps['queriesData'],
+): DataRecord[] {
+ return queriesData[0]?.data || [];
+}
+
+export function parseMetricValue(value: unknown): number | undefined {
+ if (value == null) return undefined;
+ const parsed = parseFloat(String(value));
+ return Number.isNaN(parsed) ? undefined : parsed;
+}
+
+export function addPropertiesToFeature<T extends Record<string, unknown>>(
+ feature: T,
+ record: DataRecord,
+ excludeKeys: Set<string>,
+): T {
+ const result = { ...feature } as Record<string, unknown>;
+ Object.keys(record).forEach(key => {
+ if (!excludeKeys.has(key)) {
+ result[key] = record[key];
+ }
+ });
+ return result as T;
+}
+
+export function getMetricLabelFromFormData(
+ metric: string | { value?: string } | undefined,
+): string | undefined {
+ if (!metric) return undefined;
+ if (typeof metric === 'string') return getMetricLabel(metric);
+ return metric.value ? getMetricLabel(metric.value) : undefined;
+}
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
index be9359a616..be06ecaa15 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
@@ -615,7 +615,7 @@ export const deckGLColorBreakpointsSelect:
CustomControlItem = {
};
export const breakpointsDefaultColor: CustomControlItem = {
- name: 'deafult_breakpoint_color',
+ name: 'default_breakpoint_color',
config: {
label: t('Default color'),
type: 'ColorPickerControl',
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utils/crossFiltersDataMask.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utils/crossFiltersDataMask.ts
index 0b63a1017f..95a8350d11 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utils/crossFiltersDataMask.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utils/crossFiltersDataMask.ts
@@ -73,7 +73,10 @@ export interface ValidatedPickingData {
sourcePosition?: [number, number];
targetPosition?: [number, number];
path?: string;
- geometry?: any;
+ geometry?: {
+ type: string;
+ coordinates: number[] | number[][] | number[][][];
+ };
}
const getFiltersBySpatialType = ({
@@ -96,7 +99,7 @@ const getFiltersBySpatialType = ({
type,
delimiter,
} = spatialData;
- let values: any[] = [];
+ let values: (string | number | [number, number] | [number, number][])[] = [];
let filters: QueryObjectFilterClause[] = [];
let customColumnLabel;
diff --git a/superset/views/base.py b/superset/views/base.py
index 1e6e12d2cc..b07d63f5cb 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -120,6 +120,7 @@ FRONTEND_CONF_KEYS = (
"SQLLAB_QUERY_RESULT_TIMEOUT",
"SYNC_DB_PERMISSIONS_IN_ASYNC_MODE",
"TABLE_VIZ_MAX_ROW_SERVER",
+ "MAPBOX_API_KEY",
)
logger = logging.getLogger(__name__)
diff --git a/superset/views/core.py b/superset/views/core.py
index c495594216..d52a01f356 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -169,7 +169,9 @@ class Superset(BaseSupersetView):
return json_error_response(payload=payload, status=400)
return self.json_response(
{
- "data": payload["df"].to_dict("records"),
+ "data": payload["df"].to_dict("records")
+ if payload["df"] is not None
+ else [],
"colnames": payload.get("colnames"),
"coltypes": payload.get("coltypes"),
"rowcount": payload.get("rowcount"),