feat(glyph): consolidate deckgl Polygon layer to defineChart()

Collapse multi-file plugin into single index.tsx. Polygon keeps its
custom buildQuery (line_column required, optional metric/elevation
metric handling, null filter) and transformProps (json/geohash/zipcode
polygon decoders, fixed/metric elevation, reverse_long_lat). Largest
deckgl layer (500+ lines) but follows the same pattern. Polygon.tsx
component stays as sibling for Multi.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-05-14 18:56:52 -07:00
parent 46555d12cb
commit dd1226f8e5
5 changed files with 521 additions and 593 deletions

View File

@@ -1,129 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
buildQueryContext,
ensureIsArray,
SqlaFormData,
getMetricLabel,
QueryObjectFilterClause,
QueryObject,
QueryFormColumn,
QueryFormMetric,
} from '@superset-ui/core';
import { addTooltipColumnsToQuery } from '../buildQueryUtils';
export interface DeckPolygonFormData extends SqlaFormData {
line_column?: string;
line_type?: string;
metric?: string;
point_radius_fixed?:
| {
value?: string;
}
| {
type: 'fix';
value: string;
}
| {
type: 'metric';
value: QueryFormMetric;
};
reverse_long_lat?: boolean;
filter_nulls?: boolean;
js_columns?: string[];
tooltip_contents?: unknown[];
tooltip_template?: string;
}
export default function buildQuery(formData: DeckPolygonFormData) {
const {
line_column,
metric,
point_radius_fixed,
filter_nulls = true,
js_columns,
tooltip_contents,
} = formData;
if (!line_column) {
throw new Error('Polygon column is required for Polygon charts');
}
return buildQueryContext(formData, (baseQueryObject: QueryObject) => {
let columns: QueryFormColumn[] = [
...ensureIsArray(baseQueryObject.columns || []),
line_column,
];
const jsColumns = ensureIsArray(js_columns || []);
jsColumns.forEach((col: string) => {
if (!columns.includes(col)) {
columns.push(col);
}
});
columns = addTooltipColumnsToQuery(columns, tooltip_contents);
const metrics = [];
if (metric) {
metrics.push(metric);
}
if (point_radius_fixed) {
if ('type' in point_radius_fixed) {
if (
point_radius_fixed.type === 'metric' &&
point_radius_fixed.value != null
) {
metrics.push(point_radius_fixed.value);
}
}
}
const filters = ensureIsArray(baseQueryObject.filters || []);
if (filter_nulls) {
const nullFilters: QueryObjectFilterClause[] = [
{
col: line_column,
op: 'IS NOT NULL',
},
];
if (metric) {
nullFilters.push({
col: getMetricLabel(metric),
op: 'IS NOT NULL',
});
}
filters.push(...nullFilters);
}
return [
{
...baseQueryObject,
columns,
metrics,
filters,
is_timeseries: false,
row_limit: baseQueryObject.row_limit,
},
];
});
}

View File

@@ -1,225 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
ControlPanelConfig,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import { t } from '@apache-superset/core/translation';
import timeGrainSqlaAnimationOverrides from '../../utilities/controls';
import { COLOR_SCHEME_TYPES, formatSelectOptions } from '../../utilities/utils';
import {
filterNulls,
autozoom,
jsColumns,
jsDataMutator,
jsTooltip,
jsOnclickHref,
legendFormat,
legendPosition,
fillColorPicker,
strokeColorPicker,
filled,
stroked,
extruded,
viewport,
pointRadiusFixed,
multiplier,
lineWidth,
lineType,
reverseLongLat,
mapboxStyle,
maplibreStyle,
mapProvider,
deckGLCategoricalColorSchemeTypeSelect,
deckGLLinearColorSchemeSelect,
deckGLColorBreakpointsSelect,
breakpointsDefaultColor,
tooltipContents,
tooltipTemplate,
} from '../../utilities/Shared_DeckGL';
import { dndLineColumn } from '../../utilities/sharedDndControls';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
[
{
...dndLineColumn,
config: {
...dndLineColumn.config,
label: t('Polygon Column'),
},
},
],
[
{
...lineType,
config: {
...lineType.config,
label: t('Polygon Encoding'),
},
},
],
['adhoc_filters'],
['metric'],
[
{
...pointRadiusFixed,
config: {
...pointRadiusFixed.config,
label: t('Elevation'),
},
},
],
['row_limit'],
[reverseLongLat],
[filterNulls],
[tooltipContents],
[tooltipTemplate],
],
},
{
label: t('Map'),
expanded: true,
controlSetRows: [
[mapProvider],
[mapboxStyle],
[maplibreStyle],
[viewport],
[autozoom],
],
},
{
label: t('Polygon Settings'),
expanded: true,
controlSetRows: [
[
{
...deckGLCategoricalColorSchemeTypeSelect,
config: {
...deckGLCategoricalColorSchemeTypeSelect.config,
choices: [
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
[COLOR_SCHEME_TYPES.linear_palette, t('Linear palette')],
[COLOR_SCHEME_TYPES.color_breakpoints, t('Color breakpoints')],
],
default: COLOR_SCHEME_TYPES.linear_palette,
},
},
fillColorPicker,
deckGLLinearColorSchemeSelect,
breakpointsDefaultColor,
deckGLColorBreakpointsSelect,
strokeColorPicker,
],
[filled, stroked],
[extruded],
[multiplier],
[lineWidth],
[
{
name: 'line_width_unit',
config: {
type: 'SelectControl',
label: t('Line width unit'),
default: 'pixels',
choices: [
['meters', t('meters')],
['pixels', t('pixels')],
],
renderTrigger: true,
},
},
],
[
{
name: 'opacity',
config: {
type: 'SliderControl',
label: t('Opacity'),
default: 80,
step: 1,
min: 0,
max: 100,
renderTrigger: true,
description: t('Opacity, expects values between 0 and 100'),
},
},
],
[
{
name: 'num_buckets',
config: {
type: 'SelectControl',
multi: false,
freeForm: true,
label: t('Number of buckets to group data'),
default: 5,
choices: formatSelectOptions([2, 3, 5, 10]),
description: t('How many buckets should the data be grouped in.'),
renderTrigger: true,
},
},
],
[
{
name: 'break_points',
config: {
type: 'SelectControl',
multi: true,
freeForm: true,
label: t('Bucket break points'),
choices: formatSelectOptions([]),
description: t(
'List of n+1 values for bucketing metric into n buckets.',
),
renderTrigger: true,
},
},
],
[legendPosition],
[legendFormat],
],
},
{
label: t('Advanced'),
controlSetRows: [
[jsColumns],
[jsDataMutator],
[jsTooltip],
[jsOnclickHref],
],
},
],
controlOverrides: {
metric: {
validators: [],
},
time_grain_sqla: timeGrainSqlaAnimationOverrides,
},
formDataOverrides: formData => ({
...formData,
metric: getStandardizedControls().shiftMetric(),
}),
};
export default config;

View File

@@ -1,53 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example from './images/example.png';
import exampleDark from './images/example-dark.png';
import transformProps from './transformProps';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
const metadata = new ChartMetadata({
category: t('Map'),
credits: ['https://uber.github.io/deck.gl'],
description: t(
'Visualizes geographic areas from your data as polygons on a Mapbox rendered map. Polygons can be colored using a metric.',
),
name: t('deck.gl Polygon'),
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
tags: [t('deckGL'), t('3D'), t('Multi-Dimensions'), t('Geo')],
behaviors: [Behavior.InteractiveChart],
});
export default class PolygonChartPlugin extends ChartPlugin {
constructor() {
super({
buildQuery,
loadChart: () => import('./Polygon'),
controlPanel,
metadata,
transformProps,
});
}
}

View File

@@ -0,0 +1,521 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import {
Behavior,
buildQueryContext,
ChartProps,
ensureIsArray,
getMetricLabel,
QueryFormColumn,
QueryFormMetric,
QueryObject,
QueryObjectFilterClause,
SqlaFormData,
} from '@superset-ui/core';
import { getStandardizedControls } from '@superset-ui/chart-controls';
import { defineChart } from '@superset-ui/glyph-core';
import { decode_bbox } from 'ngeohash';
import PolygonComponent from './Polygon';
import { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils';
import {
createBaseTransformResult,
getRecordsFromQuery,
getMetricLabelFromFormData,
parseMetricValue,
addPropertiesToFeature,
} from '../transformUtils';
import { addTooltipColumnsToQuery } from '../buildQueryUtils';
import timeGrainSqlaAnimationOverrides from '../../utilities/controls';
import { COLOR_SCHEME_TYPES, formatSelectOptions } from '../../utilities/utils';
import {
filterNulls,
autozoom,
jsColumns,
jsDataMutator,
jsTooltip,
jsOnclickHref,
legendFormat,
legendPosition,
fillColorPicker,
strokeColorPicker,
filled,
stroked,
extruded,
viewport,
pointRadiusFixed,
multiplier,
lineWidth,
lineType,
reverseLongLat,
mapboxStyle,
maplibreStyle,
mapProvider,
deckGLCategoricalColorSchemeTypeSelect,
deckGLLinearColorSchemeSelect,
deckGLColorBreakpointsSelect,
breakpointsDefaultColor,
tooltipContents,
tooltipTemplate,
} from '../../utilities/Shared_DeckGL';
import { dndLineColumn } from '../../utilities/sharedDndControls';
import thumbnail from './images/thumbnail.png';
import thumbnailDark from './images/thumbnail-dark.png';
import example from './images/example.png';
import exampleDark from './images/example-dark.png';
// ─── Types ───────────────────────────────────────────────────────────────────
export interface DeckPolygonFormData extends SqlaFormData {
line_column?: string;
line_type?: string;
metric?: string;
point_radius_fixed?:
| { value?: string }
| { type: 'fix'; value: string }
| { type: 'metric'; value: QueryFormMetric };
reverse_long_lat?: boolean;
filter_nulls?: boolean;
js_columns?: string[];
tooltip_contents?: unknown[];
tooltip_template?: string;
}
interface PolygonFeature {
polygon?: number[][];
name?: string;
elevation?: number;
extraProps?: Record<string, unknown>;
metrics?: Record<string, number | string>;
}
// ─── buildQuery ──────────────────────────────────────────────────────────────
export function buildQuery(formData: DeckPolygonFormData) {
const {
line_column,
metric,
point_radius_fixed,
filter_nulls = true,
js_columns,
tooltip_contents,
} = formData;
if (!line_column) {
throw new Error('Polygon column is required for Polygon charts');
}
return buildQueryContext(formData, (baseQueryObject: QueryObject) => {
let columns: QueryFormColumn[] = [
...ensureIsArray(baseQueryObject.columns || []),
line_column,
];
const jsCols = ensureIsArray(js_columns || []);
jsCols.forEach((col: string) => {
if (!columns.includes(col)) {
columns.push(col);
}
});
columns = addTooltipColumnsToQuery(columns, tooltip_contents);
const metrics = [];
if (metric) {
metrics.push(metric);
}
if (point_radius_fixed) {
if ('type' in point_radius_fixed) {
if (
point_radius_fixed.type === 'metric' &&
point_radius_fixed.value != null
) {
metrics.push(point_radius_fixed.value);
}
}
}
const filters = ensureIsArray(baseQueryObject.filters || []);
if (filter_nulls) {
const nullFilters: QueryObjectFilterClause[] = [
{ col: line_column, op: 'IS NOT NULL' },
];
if (metric) {
nullFilters.push({ col: getMetricLabel(metric), op: 'IS NOT NULL' });
}
filters.push(...nullFilters);
}
return [
{
...baseQueryObject,
columns,
metrics,
filters,
is_timeseries: false,
row_limit: baseQueryObject.row_limit,
},
];
});
}
// ─── transformProps ──────────────────────────────────────────────────────────
function parseElevationValue(value: string): number | undefined {
const parsed = parseFloat(value);
return Number.isNaN(parsed) ? undefined : parsed;
}
export function processPolygonData(
records: DataRecord[],
formData: DeckPolygonFormData,
): PolygonFeature[] {
const {
line_column,
line_type,
metric,
point_radius_fixed,
reverse_long_lat,
js_columns,
} = formData;
if (!line_column || !records.length) {
return [];
}
const metricLabel = getMetricLabelFromFormData(metric);
let elevationLabel: string | undefined;
let fixedElevationValue: number | undefined;
if (point_radius_fixed) {
if ('type' in point_radius_fixed) {
if (
point_radius_fixed.type === 'metric' &&
point_radius_fixed.value != null
) {
elevationLabel = getMetricLabel(point_radius_fixed.value);
} else if (
point_radius_fixed.type === 'fix' &&
point_radius_fixed.value
) {
fixedElevationValue = parseElevationValue(point_radius_fixed.value);
}
} else if (point_radius_fixed.value) {
fixedElevationValue = parseElevationValue(point_radius_fixed.value);
}
}
const excludeKeys = new Set([line_column, ...(js_columns || [])]);
return records
.map(record => {
let feature: PolygonFeature = {
extraProps: {},
metrics: {},
};
feature = addJsColumnsToExtraProps(feature, record, js_columns);
const updatedFeature = addPropertiesToFeature(
feature as unknown as Record<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 (parsed.geometry?.coordinates) {
polygonCoords =
parsed.geometry.coordinates[0] || parsed.geometry.coordinates;
} else if (Array.isArray(parsed)) {
polygonCoords = parsed;
} else {
return null;
}
break;
}
case 'geohash':
polygonCoords = [];
const decoded = decode_bbox(String(rawPolygonData));
if (decoded) {
polygonCoords.push([decoded[1], decoded[0]]);
polygonCoords.push([decoded[1], decoded[2]]);
polygonCoords.push([decoded[3], decoded[2]]);
polygonCoords.push([decoded[3], decoded[0]]);
polygonCoords.push([decoded[1], decoded[0]]);
}
break;
case 'zipcode':
default: {
polygonCoords = Array.isArray(rawPolygonData) ? rawPolygonData : [];
break;
}
}
if (reverse_long_lat && polygonCoords.length > 0) {
polygonCoords = polygonCoords.map(coord => [coord[1], coord[0]]);
}
feature.polygon = polygonCoords;
if (fixedElevationValue !== undefined) {
feature.elevation = fixedElevationValue;
} else if (elevationLabel && record[elevationLabel] != null) {
const elevationValue = parseMetricValue(record[elevationLabel]);
if (elevationValue !== undefined) {
feature.elevation = elevationValue;
}
}
if (metricLabel && record[metricLabel] != null) {
const metricValue = record[metricLabel];
if (
typeof metricValue === 'string' ||
typeof metricValue === 'number'
) {
feature.metrics![metricLabel] = metricValue;
}
}
} catch {
return null;
}
return feature;
})
.filter((feature): feature is PolygonFeature => feature !== null);
}
function transformProps(chartProps: ChartProps) {
const { rawFormData: formData } = chartProps;
const records = getRecordsFromQuery(chartProps.queriesData);
const features = processPolygonData(records, formData as DeckPolygonFormData);
return createBaseTransformResult(chartProps, features);
}
// ─── Plugin definition ───────────────────────────────────────────────────────
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default defineChart<Record<string, never>, any>({
metadata: {
name: t('deck.gl Polygon'),
description: t(
'Visualizes geographic areas from your data as polygons on a Mapbox rendered map. Polygons can be colored using a metric.',
),
category: t('Map'),
credits: ['https://uber.github.io/deck.gl'],
behaviors: [Behavior.InteractiveChart],
tags: [t('deckGL'), t('3D'), t('Multi-Dimensions'), t('Geo')],
thumbnail,
thumbnailDark,
exampleGallery: [{ url: example, urlDark: exampleDark }],
},
arguments: {},
suppressQuerySection: true,
prependSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
[
{
...dndLineColumn,
config: {
...dndLineColumn.config,
label: t('Polygon Column'),
},
},
],
[
{
...lineType,
config: {
...lineType.config,
label: t('Polygon Encoding'),
},
},
],
['adhoc_filters'],
['metric'],
[
{
...pointRadiusFixed,
config: {
...pointRadiusFixed.config,
label: t('Elevation'),
},
},
],
['row_limit'],
[reverseLongLat],
[filterNulls],
[tooltipContents],
[tooltipTemplate],
],
},
{
label: t('Map'),
expanded: true,
controlSetRows: [
[mapProvider],
[mapboxStyle],
[maplibreStyle],
[viewport],
[autozoom],
],
},
{
label: t('Polygon Settings'),
expanded: true,
controlSetRows: [
[
{
...deckGLCategoricalColorSchemeTypeSelect,
config: {
...deckGLCategoricalColorSchemeTypeSelect.config,
choices: [
[COLOR_SCHEME_TYPES.fixed_color, t('Fixed color')],
[COLOR_SCHEME_TYPES.linear_palette, t('Linear palette')],
[COLOR_SCHEME_TYPES.color_breakpoints, t('Color breakpoints')],
],
default: COLOR_SCHEME_TYPES.linear_palette,
},
},
fillColorPicker,
deckGLLinearColorSchemeSelect,
breakpointsDefaultColor,
deckGLColorBreakpointsSelect,
strokeColorPicker,
],
[filled, stroked],
[extruded],
[multiplier],
[lineWidth],
[
{
name: 'line_width_unit',
config: {
type: 'SelectControl',
label: t('Line width unit'),
default: 'pixels',
choices: [
['meters', t('meters')],
['pixels', t('pixels')],
],
renderTrigger: true,
},
},
],
[
{
name: 'opacity',
config: {
type: 'SliderControl',
label: t('Opacity'),
default: 80,
step: 1,
min: 0,
max: 100,
renderTrigger: true,
description: t('Opacity, expects values between 0 and 100'),
},
},
],
[
{
name: 'num_buckets',
config: {
type: 'SelectControl',
multi: false,
freeForm: true,
label: t('Number of buckets to group data'),
default: 5,
choices: formatSelectOptions([2, 3, 5, 10]),
description: t('How many buckets should the data be grouped in.'),
renderTrigger: true,
},
},
],
[
{
name: 'break_points',
config: {
type: 'SelectControl',
multi: true,
freeForm: true,
label: t('Bucket break points'),
choices: formatSelectOptions([]),
description: t(
'List of n+1 values for bucketing metric into n buckets.',
),
renderTrigger: true,
},
},
],
[legendPosition],
[legendFormat],
],
},
{
label: t('Advanced'),
controlSetRows: [
[jsColumns],
[jsDataMutator],
[jsTooltip],
[jsOnclickHref],
],
},
],
additionalControlOverrides: {
metric: {
validators: [],
},
time_grain_sqla: timeGrainSqlaAnimationOverrides,
},
formDataOverrides: formData => ({
...formData,
metric: getStandardizedControls().shiftMetric(),
}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
buildQuery: (formData: any) => buildQuery(formData as DeckPolygonFormData),
transform: chartProps => transformProps(chartProps),
render: ({ transformedProps }) => (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<PolygonComponent {...(transformedProps as any)} />
),
});

View File

@@ -1,186 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ChartProps, getMetricLabel } from '@superset-ui/core';
import { addJsColumnsToExtraProps, DataRecord } from '../spatialUtils';
import {
createBaseTransformResult,
getRecordsFromQuery,
getMetricLabelFromFormData,
parseMetricValue,
addPropertiesToFeature,
} from '../transformUtils';
import { DeckPolygonFormData } from './buildQuery';
import { decode_bbox } from 'ngeohash';
function parseElevationValue(value: string): number | undefined {
const parsed = parseFloat(value);
return Number.isNaN(parsed) ? undefined : parsed;
}
interface PolygonFeature {
polygon?: number[][];
name?: string;
elevation?: number;
extraProps?: Record<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);
let elevationLabel: string | undefined;
let fixedElevationValue: number | undefined;
if (point_radius_fixed) {
if ('type' in point_radius_fixed) {
if (
point_radius_fixed.type === 'metric' &&
point_radius_fixed.value != null
) {
elevationLabel = getMetricLabel(point_radius_fixed.value);
} else if (
point_radius_fixed.type === 'fix' &&
point_radius_fixed.value
) {
fixedElevationValue = parseElevationValue(point_radius_fixed.value);
}
} else if (point_radius_fixed.value) {
fixedElevationValue = parseElevationValue(point_radius_fixed.value);
}
}
const excludeKeys = new Set([line_column, ...(js_columns || [])]);
return records
.map(record => {
let feature: PolygonFeature = {
extraProps: {},
metrics: {},
};
feature = addJsColumnsToExtraProps(feature, record, js_columns);
const updatedFeature = addPropertiesToFeature(
feature as unknown as Record<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 (parsed.geometry?.coordinates) {
// Non-standard format with nested geometry
polygonCoords =
parsed.geometry.coordinates[0] || parsed.geometry.coordinates;
} else if (Array.isArray(parsed)) {
polygonCoords = parsed;
} else {
return null;
}
break;
}
case 'geohash':
polygonCoords = [];
const decoded = decode_bbox(String(rawPolygonData));
if (decoded) {
polygonCoords.push([decoded[1], decoded[0]]); // SW (minLon, minLat)
polygonCoords.push([decoded[1], decoded[2]]); // NW (minLon, maxLat)
polygonCoords.push([decoded[3], decoded[2]]); // NE (maxLon, maxLat)
polygonCoords.push([decoded[3], decoded[0]]); // SE (maxLon, minLat)
polygonCoords.push([decoded[1], decoded[0]]); // SW (close polygon)
}
break;
case 'zipcode':
default: {
polygonCoords = Array.isArray(rawPolygonData) ? rawPolygonData : [];
break;
}
}
if (reverse_long_lat && polygonCoords.length > 0) {
polygonCoords = polygonCoords.map(coord => [coord[1], coord[0]]);
}
feature.polygon = polygonCoords;
if (fixedElevationValue !== undefined) {
feature.elevation = fixedElevationValue;
} else if (elevationLabel && record[elevationLabel] != null) {
const elevationValue = parseMetricValue(record[elevationLabel]);
if (elevationValue !== undefined) {
feature.elevation = elevationValue;
}
}
if (metricLabel && record[metricLabel] != null) {
const metricValue = record[metricLabel];
if (
typeof metricValue === 'string' ||
typeof metricValue === 'number'
) {
feature.metrics![metricLabel] = metricValue;
}
}
} catch {
return null;
}
return feature;
})
.filter((feature): feature is PolygonFeature => feature !== null);
}
export default function transformProps(chartProps: ChartProps) {
const { rawFormData: formData } = chartProps;
const records = getRecordsFromQuery(chartProps.queriesData);
const features = processPolygonData(records, formData as DeckPolygonFormData);
return createBaseTransformResult(chartProps, features);
}