mirror of
https://github.com/apache/superset.git
synced 2026-06-07 16:49:17 +00:00
refactor(deckgl): update deck.gl charts to use new api (#34859)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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] : [],
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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] : [],
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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: () => {},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user