mirror of
https://github.com/apache/superset.git
synced 2026-05-29 20:29:34 +00:00
feat(glyph): consolidate deckgl Scatter layer to defineChart()
Collapse multi-file plugin into single index.tsx. Scatter keeps its custom buildQuery (spatial config required, metric-or-fixed radius handling) and transformProps (radius from metric/fix value, category column, js columns). Also wires `onInit?: ControlPanelConfig['onInit']` through to defineChart so Scatter can clear time_grain_sqla and granularity on chart initialization. The onInit hook is now an optional metadata-level override surfaced in the generated ControlPanelConfig. Scatter.tsx component stays as sibling for Multi. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -265,6 +265,14 @@ export interface ChartDefinition<
|
||||
*/
|
||||
formDataOverrides?: (formData: QueryFormData) => QueryFormData;
|
||||
|
||||
/**
|
||||
* onInit hook - called once when the chart's controls are first initialized.
|
||||
* Use to reset/override control values that should not persist from the
|
||||
* dataset's defaults (e.g., clearing time_grain_sqla on charts that don't
|
||||
* use it).
|
||||
*/
|
||||
onInit?: ControlPanelConfig['onInit'];
|
||||
|
||||
/**
|
||||
* Custom buildQuery function - use for charts that need post-processing operators
|
||||
* If not provided, a default query builder is generated from arguments
|
||||
@@ -627,6 +635,7 @@ function generateControlPanel<TArgs extends ChartArguments>(
|
||||
controlOverrides?: ChartDefinition<TArgs>['controlOverrides'],
|
||||
additionalControlOverrides?: ChartDefinition<TArgs>['additionalControlOverrides'],
|
||||
formDataOverrides?: ChartDefinition<TArgs>['formDataOverrides'],
|
||||
onInit?: ChartDefinition<TArgs>['onInit'],
|
||||
additionalSections?: ControlPanelSectionConfig[],
|
||||
prependSections?: ControlPanelSectionConfig[],
|
||||
chartOptionsTabOverride?: 'customize' | 'data',
|
||||
@@ -753,6 +762,10 @@ function generateControlPanel<TArgs extends ChartArguments>(
|
||||
config.formDataOverrides = formDataOverrides;
|
||||
}
|
||||
|
||||
if (onInit) {
|
||||
config.onInit = onInit;
|
||||
}
|
||||
|
||||
// Store raw glyph args for native rendering (bypasses expandControlConfig pipeline)
|
||||
config._glyphArgs = args;
|
||||
|
||||
@@ -973,6 +986,7 @@ export function defineChart<
|
||||
controlOverrides,
|
||||
additionalControlOverrides,
|
||||
formDataOverrides,
|
||||
onInit,
|
||||
buildQuery: customBuildQuery,
|
||||
transform,
|
||||
render,
|
||||
@@ -989,6 +1003,7 @@ export function defineChart<
|
||||
controlOverrides,
|
||||
additionalControlOverrides,
|
||||
formDataOverrides,
|
||||
onInit,
|
||||
additionalSections,
|
||||
prependSections,
|
||||
chartOptionsTabOverride,
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
buildQueryContext,
|
||||
ensureIsArray,
|
||||
getMetricLabel,
|
||||
QueryFormMetric,
|
||||
QueryFormOrderBy,
|
||||
SqlaFormData,
|
||||
QueryFormColumn,
|
||||
QueryObject,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
getSpatialColumns,
|
||||
addSpatialNullFilters,
|
||||
SpatialFormData,
|
||||
} from '../spatialUtils';
|
||||
import {
|
||||
addJsColumnsToColumns,
|
||||
addTooltipColumnsToQuery,
|
||||
} from '../buildQueryUtils';
|
||||
import { isMetricValue } from '../utils/metricUtils';
|
||||
|
||||
export interface DeckScatterFormData
|
||||
extends Omit<SpatialFormData, 'color_picker'>, SqlaFormData {
|
||||
// Can be a string (legacy format) or an object with type and value
|
||||
point_radius_fixed?:
|
||||
| string // Legacy format: metric name directly
|
||||
| {
|
||||
type?: 'fix' | 'metric';
|
||||
value?: QueryFormMetric | number;
|
||||
};
|
||||
multiplier?: number;
|
||||
point_unit?: string;
|
||||
min_radius?: number;
|
||||
max_radius?: number;
|
||||
color_picker?: { r: number; g: number; b: number; a: number };
|
||||
dimension?: string;
|
||||
}
|
||||
|
||||
export default function buildQuery(formData: DeckScatterFormData) {
|
||||
const {
|
||||
spatial,
|
||||
point_radius_fixed,
|
||||
dimension,
|
||||
js_columns,
|
||||
tooltip_contents,
|
||||
} = formData;
|
||||
|
||||
if (!spatial) {
|
||||
throw new Error('Spatial configuration is required for Scatter charts');
|
||||
}
|
||||
|
||||
return buildQueryContext(formData, {
|
||||
buildQuery: (baseQueryObject: QueryObject) => {
|
||||
const spatialColumns = getSpatialColumns(spatial);
|
||||
let columns = [...(baseQueryObject.columns || []), ...spatialColumns];
|
||||
|
||||
if (dimension) {
|
||||
columns.push(dimension);
|
||||
}
|
||||
|
||||
const columnStrings = columns.map(col =>
|
||||
typeof col === 'string' ? col : col.label || col.sqlExpression || '',
|
||||
);
|
||||
const withJsColumns = addJsColumnsToColumns(columnStrings, js_columns);
|
||||
|
||||
columns = withJsColumns as QueryFormColumn[];
|
||||
columns = addTooltipColumnsToQuery(columns, tooltip_contents);
|
||||
|
||||
// Only add metric if point_radius_fixed is a metric type
|
||||
const isMetric = isMetricValue(point_radius_fixed);
|
||||
// Extract metric value: legacy string format or object with metric value
|
||||
const rawValue =
|
||||
typeof point_radius_fixed === 'string'
|
||||
? point_radius_fixed
|
||||
: point_radius_fixed?.value;
|
||||
const metricValue: QueryFormMetric | null =
|
||||
isMetric && rawValue !== undefined && typeof rawValue !== 'number'
|
||||
? (rawValue as QueryFormMetric)
|
||||
: null;
|
||||
|
||||
// Preserve existing metrics and only add radius metric if it's metric-based
|
||||
const existingMetrics = baseQueryObject.metrics || [];
|
||||
// Deduplicate metrics using getMetricLabel for comparison
|
||||
const existingLabels = new Set(
|
||||
existingMetrics.map(m => getMetricLabel(m)),
|
||||
);
|
||||
const metrics: QueryFormMetric[] =
|
||||
metricValue && !existingLabels.has(getMetricLabel(metricValue))
|
||||
? [...existingMetrics, metricValue]
|
||||
: existingMetrics;
|
||||
|
||||
const filters = addSpatialNullFilters(
|
||||
spatial,
|
||||
ensureIsArray(baseQueryObject.filters || []),
|
||||
);
|
||||
|
||||
// orderby needs string label, not the full metric object
|
||||
const orderby =
|
||||
isMetric && metricValue
|
||||
? ([[getMetricLabel(metricValue), false]] as QueryFormOrderBy[])
|
||||
: (baseQueryObject.orderby as QueryFormOrderBy[]) || [];
|
||||
|
||||
return [
|
||||
{
|
||||
...baseQueryObject,
|
||||
columns,
|
||||
metrics,
|
||||
filters,
|
||||
orderby,
|
||||
is_timeseries: false,
|
||||
row_limit: baseQueryObject.row_limit,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ControlPanelConfig } from '@superset-ui/chart-controls';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { validateNonEmpty } from '@superset-ui/core';
|
||||
import timeGrainSqlaAnimationOverrides from '../../utilities/controls';
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
jsOnclickHref,
|
||||
legendFormat,
|
||||
legendPosition,
|
||||
viewport,
|
||||
spatial,
|
||||
pointRadiusFixed,
|
||||
multiplier,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
generateDeckGLColorSchemeControls,
|
||||
tooltipContents,
|
||||
tooltipTemplate,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
onInit: controlState => ({
|
||||
...controlState,
|
||||
time_grain_sqla: {
|
||||
...controlState.time_grain_sqla,
|
||||
value: null,
|
||||
},
|
||||
granularity: {
|
||||
...controlState.granularity,
|
||||
value: null,
|
||||
},
|
||||
}),
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[spatial, null],
|
||||
['row_limit', filterNulls],
|
||||
['adhoc_filters'],
|
||||
[tooltipContents],
|
||||
[tooltipTemplate],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[mapProvider],
|
||||
[mapboxStyle],
|
||||
[maplibreStyle],
|
||||
[autozoom, viewport],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Point Size'),
|
||||
controlSetRows: [
|
||||
[pointRadiusFixed],
|
||||
[
|
||||
{
|
||||
name: 'point_unit',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Point Unit'),
|
||||
default: 'square_m',
|
||||
clearable: false,
|
||||
choices: [
|
||||
['square_m', t('Square meters')],
|
||||
['square_km', t('Square kilometers')],
|
||||
['square_miles', t('Square miles')],
|
||||
['radius_m', t('Radius in meters')],
|
||||
['radius_km', t('Radius in kilometers')],
|
||||
['radius_miles', t('Radius in miles')],
|
||||
],
|
||||
description: t(
|
||||
'The unit of measure for the specified point radius',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'min_radius',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Minimum Radius'),
|
||||
isFloat: true,
|
||||
validators: [validateNonEmpty],
|
||||
renderTrigger: true,
|
||||
default: 2,
|
||||
description: t(
|
||||
'Minimum radius size of the circle, in pixels. As the zoom level changes, this ' +
|
||||
'insures that the circle respects this minimum radius.',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'max_radius',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Maximum Radius'),
|
||||
isFloat: true,
|
||||
validators: [validateNonEmpty],
|
||||
renderTrigger: true,
|
||||
default: 250,
|
||||
description: t(
|
||||
'Maximum radius size of the circle, in pixels. As the zoom level changes, this ' +
|
||||
'insures that the circle respects this maximum radius.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[multiplier, null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Point Color'),
|
||||
controlSetRows: [
|
||||
[legendPosition],
|
||||
[legendFormat],
|
||||
...generateDeckGLColorSchemeControls({}),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
validators: [],
|
||||
},
|
||||
time_grain_sqla: timeGrainSqlaAnimationOverrides,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example from './images/example.png';
|
||||
import exampleDark from './images/example-dark.png';
|
||||
import buildQuery from './buildQuery';
|
||||
import transformProps from './transformProps';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Map'),
|
||||
credits: ['https://uber.github.io/deck.gl'],
|
||||
description: t(
|
||||
'A map that takes rendering circles with a variable radius at latitude/longitude coordinates',
|
||||
),
|
||||
name: t('deck.gl Scatterplot'),
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
tags: [
|
||||
t('deckGL'),
|
||||
t('Comparison'),
|
||||
t('Scatter'),
|
||||
t('2D'),
|
||||
t('Geo'),
|
||||
t('Intensity'),
|
||||
t('Density'),
|
||||
],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
});
|
||||
|
||||
export default class ScatterChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
buildQuery,
|
||||
loadChart: () => import('./Scatter'),
|
||||
controlPanel,
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
Behavior,
|
||||
buildQueryContext,
|
||||
ChartProps,
|
||||
ensureIsArray,
|
||||
getMetricLabel,
|
||||
QueryFormColumn,
|
||||
QueryFormMetric,
|
||||
QueryFormOrderBy,
|
||||
QueryObject,
|
||||
SqlaFormData,
|
||||
validateNonEmpty,
|
||||
} from '@superset-ui/core';
|
||||
import { defineChart } from '@superset-ui/glyph-core';
|
||||
import ScatterComponent from './Scatter';
|
||||
import {
|
||||
processSpatialData,
|
||||
getSpatialColumns,
|
||||
addSpatialNullFilters,
|
||||
SpatialFormData,
|
||||
DataRecord,
|
||||
} from '../spatialUtils';
|
||||
import {
|
||||
createBaseTransformResult,
|
||||
getRecordsFromQuery,
|
||||
getMetricLabelFromFormData,
|
||||
parseMetricValue,
|
||||
addPropertiesToFeature,
|
||||
} from '../transformUtils';
|
||||
import {
|
||||
addJsColumnsToColumns,
|
||||
addTooltipColumnsToQuery,
|
||||
} from '../buildQueryUtils';
|
||||
import {
|
||||
isMetricValue,
|
||||
isFixedValue,
|
||||
getFixedValue,
|
||||
} from '../utils/metricUtils';
|
||||
import timeGrainSqlaAnimationOverrides from '../../utilities/controls';
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
jsOnclickHref,
|
||||
legendFormat,
|
||||
legendPosition,
|
||||
viewport,
|
||||
spatial,
|
||||
pointRadiusFixed,
|
||||
multiplier,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
generateDeckGLColorSchemeControls,
|
||||
tooltipContents,
|
||||
tooltipTemplate,
|
||||
} from '../../utilities/Shared_DeckGL';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example from './images/example.png';
|
||||
import exampleDark from './images/example-dark.png';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface DeckScatterFormData
|
||||
extends Omit<SpatialFormData, 'color_picker'>,
|
||||
SqlaFormData {
|
||||
// Can be a string (legacy format) or an object with type and value
|
||||
point_radius_fixed?:
|
||||
| string // Legacy format: metric name directly
|
||||
| {
|
||||
type?: 'fix' | 'metric';
|
||||
value?: QueryFormMetric | number;
|
||||
};
|
||||
multiplier?: number;
|
||||
point_unit?: string;
|
||||
min_radius?: number;
|
||||
max_radius?: number;
|
||||
color_picker?: { r: number; g: number; b: number; a: number };
|
||||
dimension?: string;
|
||||
}
|
||||
|
||||
interface ScatterPoint {
|
||||
position: [number, number];
|
||||
radius?: number;
|
||||
color?: [number, number, number, number];
|
||||
cat_color?: string;
|
||||
metric?: number;
|
||||
extraProps?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// ─── buildQuery ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function buildQuery(formData: DeckScatterFormData) {
|
||||
const {
|
||||
spatial: spatialCfg,
|
||||
point_radius_fixed,
|
||||
dimension,
|
||||
js_columns,
|
||||
tooltip_contents,
|
||||
} = formData;
|
||||
|
||||
if (!spatialCfg) {
|
||||
throw new Error('Spatial configuration is required for Scatter charts');
|
||||
}
|
||||
|
||||
return buildQueryContext(formData, {
|
||||
buildQuery: (baseQueryObject: QueryObject) => {
|
||||
const spatialColumns = getSpatialColumns(spatialCfg);
|
||||
let columns = [...(baseQueryObject.columns || []), ...spatialColumns];
|
||||
|
||||
if (dimension) {
|
||||
columns.push(dimension);
|
||||
}
|
||||
|
||||
const columnStrings = columns.map(col =>
|
||||
typeof col === 'string' ? col : col.label || col.sqlExpression || '',
|
||||
);
|
||||
const withJsColumns = addJsColumnsToColumns(columnStrings, js_columns);
|
||||
|
||||
columns = withJsColumns as QueryFormColumn[];
|
||||
columns = addTooltipColumnsToQuery(columns, tooltip_contents);
|
||||
|
||||
const isMetric = isMetricValue(point_radius_fixed);
|
||||
const rawValue =
|
||||
typeof point_radius_fixed === 'string'
|
||||
? point_radius_fixed
|
||||
: point_radius_fixed?.value;
|
||||
const metricValue: QueryFormMetric | null =
|
||||
isMetric && rawValue !== undefined && typeof rawValue !== 'number'
|
||||
? (rawValue as QueryFormMetric)
|
||||
: null;
|
||||
|
||||
const existingMetrics = baseQueryObject.metrics || [];
|
||||
const existingLabels = new Set(
|
||||
existingMetrics.map(m => getMetricLabel(m)),
|
||||
);
|
||||
const metrics: QueryFormMetric[] =
|
||||
metricValue && !existingLabels.has(getMetricLabel(metricValue))
|
||||
? [...existingMetrics, metricValue]
|
||||
: existingMetrics;
|
||||
|
||||
const filters = addSpatialNullFilters(
|
||||
spatialCfg,
|
||||
ensureIsArray(baseQueryObject.filters || []),
|
||||
);
|
||||
|
||||
const orderby =
|
||||
isMetric && metricValue
|
||||
? ([[getMetricLabel(metricValue), false]] as QueryFormOrderBy[])
|
||||
: (baseQueryObject.orderby as QueryFormOrderBy[]) || [];
|
||||
|
||||
return [
|
||||
{
|
||||
...baseQueryObject,
|
||||
columns,
|
||||
metrics,
|
||||
filters,
|
||||
orderby,
|
||||
is_timeseries: false,
|
||||
row_limit: baseQueryObject.row_limit,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ─── transformProps ──────────────────────────────────────────────────────────
|
||||
|
||||
export function processScatterData(
|
||||
records: DataRecord[],
|
||||
spatialCfg: DeckScatterFormData['spatial'],
|
||||
radiusMetricLabel?: string,
|
||||
categoryColumn?: string,
|
||||
jsCols?: string[],
|
||||
fixedRadiusValue?: number | string | null,
|
||||
): ScatterPoint[] {
|
||||
if (!spatialCfg || !records.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const spatialFeatures = processSpatialData(records, spatialCfg);
|
||||
const excludeKeys = new Set([
|
||||
'position',
|
||||
'weight',
|
||||
'extraProps',
|
||||
...(spatialCfg
|
||||
? [
|
||||
spatialCfg.lonCol,
|
||||
spatialCfg.latCol,
|
||||
spatialCfg.lonlatCol,
|
||||
spatialCfg.geohashCol,
|
||||
].filter(Boolean)
|
||||
: []),
|
||||
radiusMetricLabel,
|
||||
categoryColumn,
|
||||
...(jsCols || []),
|
||||
]);
|
||||
|
||||
return spatialFeatures.map(feature => {
|
||||
let scatterPoint: ScatterPoint = {
|
||||
position: feature.position,
|
||||
extraProps: feature.extraProps || {},
|
||||
};
|
||||
|
||||
if (fixedRadiusValue != null) {
|
||||
const parsedFixedRadius = parseMetricValue(fixedRadiusValue);
|
||||
if (parsedFixedRadius !== undefined) {
|
||||
scatterPoint.radius = parsedFixedRadius;
|
||||
}
|
||||
} else if (radiusMetricLabel && feature[radiusMetricLabel] != null) {
|
||||
const radiusValue = parseMetricValue(feature[radiusMetricLabel]);
|
||||
if (radiusValue !== undefined) {
|
||||
scatterPoint.radius = radiusValue;
|
||||
scatterPoint.metric = radiusValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (categoryColumn && feature[categoryColumn] != null) {
|
||||
scatterPoint.cat_color = String(feature[categoryColumn]);
|
||||
}
|
||||
|
||||
scatterPoint = addPropertiesToFeature(
|
||||
scatterPoint,
|
||||
feature as DataRecord,
|
||||
excludeKeys,
|
||||
);
|
||||
return scatterPoint;
|
||||
});
|
||||
}
|
||||
|
||||
function transformProps(chartProps: ChartProps) {
|
||||
const { rawFormData: formData } = chartProps;
|
||||
const {
|
||||
spatial: spatialCfg,
|
||||
point_radius_fixed,
|
||||
dimension,
|
||||
js_columns,
|
||||
} = formData as DeckScatterFormData;
|
||||
|
||||
const fixedRadiusValue = isFixedValue(point_radius_fixed)
|
||||
? getFixedValue(point_radius_fixed)
|
||||
: null;
|
||||
|
||||
const radiusMetricLabel = getMetricLabelFromFormData(point_radius_fixed);
|
||||
const records = getRecordsFromQuery(chartProps.queriesData);
|
||||
|
||||
const features = processScatterData(
|
||||
records,
|
||||
spatialCfg,
|
||||
radiusMetricLabel,
|
||||
dimension,
|
||||
js_columns,
|
||||
fixedRadiusValue,
|
||||
);
|
||||
|
||||
return createBaseTransformResult(
|
||||
chartProps,
|
||||
features,
|
||||
radiusMetricLabel ? [radiusMetricLabel] : [],
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Plugin definition ───────────────────────────────────────────────────────
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default defineChart<Record<string, never>, any>({
|
||||
metadata: {
|
||||
name: t('deck.gl Scatterplot'),
|
||||
description: t(
|
||||
'A map that takes rendering circles with a variable radius at latitude/longitude coordinates',
|
||||
),
|
||||
category: t('Map'),
|
||||
credits: ['https://uber.github.io/deck.gl'],
|
||||
behaviors: [Behavior.InteractiveChart],
|
||||
tags: [
|
||||
t('deckGL'),
|
||||
t('Comparison'),
|
||||
t('Scatter'),
|
||||
t('2D'),
|
||||
t('Geo'),
|
||||
t('Intensity'),
|
||||
t('Density'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
},
|
||||
arguments: {},
|
||||
suppressQuerySection: true,
|
||||
onInit: controlState => ({
|
||||
...controlState,
|
||||
time_grain_sqla: {
|
||||
...controlState.time_grain_sqla,
|
||||
value: null,
|
||||
},
|
||||
granularity: {
|
||||
...controlState.granularity,
|
||||
value: null,
|
||||
},
|
||||
}),
|
||||
prependSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[spatial, null],
|
||||
['row_limit', filterNulls],
|
||||
['adhoc_filters'],
|
||||
[tooltipContents],
|
||||
[tooltipTemplate],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Map'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[mapProvider],
|
||||
[mapboxStyle],
|
||||
[maplibreStyle],
|
||||
[autozoom, viewport],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Point Size'),
|
||||
controlSetRows: [
|
||||
[pointRadiusFixed],
|
||||
[
|
||||
{
|
||||
name: 'point_unit',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Point Unit'),
|
||||
default: 'square_m',
|
||||
clearable: false,
|
||||
choices: [
|
||||
['square_m', t('Square meters')],
|
||||
['square_km', t('Square kilometers')],
|
||||
['square_miles', t('Square miles')],
|
||||
['radius_m', t('Radius in meters')],
|
||||
['radius_km', t('Radius in kilometers')],
|
||||
['radius_miles', t('Radius in miles')],
|
||||
],
|
||||
description: t(
|
||||
'The unit of measure for the specified point radius',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'min_radius',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Minimum Radius'),
|
||||
isFloat: true,
|
||||
validators: [validateNonEmpty],
|
||||
renderTrigger: true,
|
||||
default: 2,
|
||||
description: t(
|
||||
'Minimum radius size of the circle, in pixels. As the zoom level changes, this ' +
|
||||
'insures that the circle respects this minimum radius.',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'max_radius',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Maximum Radius'),
|
||||
isFloat: true,
|
||||
validators: [validateNonEmpty],
|
||||
renderTrigger: true,
|
||||
default: 250,
|
||||
description: t(
|
||||
'Maximum radius size of the circle, in pixels. As the zoom level changes, this ' +
|
||||
'insures that the circle respects this maximum radius.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[multiplier, null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Point Color'),
|
||||
controlSetRows: [
|
||||
[legendPosition],
|
||||
[legendFormat],
|
||||
...generateDeckGLColorSchemeControls({}),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
additionalControlOverrides: {
|
||||
size: {
|
||||
validators: [],
|
||||
},
|
||||
time_grain_sqla: timeGrainSqlaAnimationOverrides,
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
buildQuery: (formData: any) => buildQuery(formData as DeckScatterFormData),
|
||||
transform: chartProps => transformProps(chartProps),
|
||||
render: ({ transformedProps }) => (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
<ScatterComponent {...(transformedProps as any)} />
|
||||
),
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
import { processSpatialData, DataRecord } from '../spatialUtils';
|
||||
import {
|
||||
createBaseTransformResult,
|
||||
getRecordsFromQuery,
|
||||
getMetricLabelFromFormData,
|
||||
parseMetricValue,
|
||||
addPropertiesToFeature,
|
||||
} from '../transformUtils';
|
||||
import { DeckScatterFormData } from './buildQuery';
|
||||
import { isFixedValue, getFixedValue } from '../utils/metricUtils';
|
||||
|
||||
interface ScatterPoint {
|
||||
position: [number, number];
|
||||
radius?: number;
|
||||
color?: [number, number, number, number];
|
||||
cat_color?: string;
|
||||
metric?: number;
|
||||
extraProps?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function processScatterData(
|
||||
records: DataRecord[],
|
||||
spatial: DeckScatterFormData['spatial'],
|
||||
radiusMetricLabel?: string,
|
||||
categoryColumn?: string,
|
||||
jsColumns?: string[],
|
||||
fixedRadiusValue?: number | string | null,
|
||||
): ScatterPoint[] {
|
||||
if (!spatial || !records.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const spatialFeatures = processSpatialData(records, spatial);
|
||||
const excludeKeys = new Set([
|
||||
'position',
|
||||
'weight',
|
||||
'extraProps',
|
||||
...(spatial
|
||||
? [
|
||||
spatial.lonCol,
|
||||
spatial.latCol,
|
||||
spatial.lonlatCol,
|
||||
spatial.geohashCol,
|
||||
].filter(Boolean)
|
||||
: []),
|
||||
radiusMetricLabel,
|
||||
categoryColumn,
|
||||
...(jsColumns || []),
|
||||
]);
|
||||
|
||||
return spatialFeatures.map(feature => {
|
||||
let scatterPoint: ScatterPoint = {
|
||||
position: feature.position,
|
||||
extraProps: feature.extraProps || {},
|
||||
};
|
||||
|
||||
// Handle radius: either from metric or fixed value
|
||||
if (fixedRadiusValue != null) {
|
||||
// Use fixed radius value for all points
|
||||
const parsedFixedRadius = parseMetricValue(fixedRadiusValue);
|
||||
if (parsedFixedRadius !== undefined) {
|
||||
scatterPoint.radius = parsedFixedRadius;
|
||||
}
|
||||
} else if (radiusMetricLabel && feature[radiusMetricLabel] != null) {
|
||||
// Use metric value for radius
|
||||
const radiusValue = parseMetricValue(feature[radiusMetricLabel]);
|
||||
if (radiusValue !== undefined) {
|
||||
scatterPoint.radius = radiusValue;
|
||||
scatterPoint.metric = radiusValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (categoryColumn && feature[categoryColumn] != null) {
|
||||
scatterPoint.cat_color = String(feature[categoryColumn]);
|
||||
}
|
||||
|
||||
scatterPoint = addPropertiesToFeature(
|
||||
scatterPoint,
|
||||
feature as DataRecord,
|
||||
excludeKeys,
|
||||
);
|
||||
return scatterPoint;
|
||||
});
|
||||
}
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { rawFormData: formData } = chartProps;
|
||||
const { spatial, point_radius_fixed, dimension, js_columns } =
|
||||
formData as DeckScatterFormData;
|
||||
|
||||
// Check if this is a fixed value or metric
|
||||
const fixedRadiusValue = isFixedValue(point_radius_fixed)
|
||||
? getFixedValue(point_radius_fixed)
|
||||
: null;
|
||||
|
||||
const radiusMetricLabel = getMetricLabelFromFormData(point_radius_fixed);
|
||||
const records = getRecordsFromQuery(chartProps.queriesData);
|
||||
|
||||
const features = processScatterData(
|
||||
records,
|
||||
spatial,
|
||||
radiusMetricLabel,
|
||||
dimension,
|
||||
js_columns,
|
||||
fixedRadiusValue,
|
||||
);
|
||||
|
||||
return createBaseTransformResult(
|
||||
chartProps,
|
||||
features,
|
||||
radiusMetricLabel ? [radiusMetricLabel] : [],
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user