This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 5e0ee40762 feat(chart): support icons and text in the `deck.gl
Geojson` visualization (#36201)
5e0ee40762 is described below
commit 5e0ee40762140917afab0b2a5123bf9a57d60b90
Author: Joshua Daniel <[email protected]>
AuthorDate: Tue Dec 16 17:28:04 2025 -0500
feat(chart): support icons and text in the `deck.gl Geojson` visualization
(#36201)
Co-authored-by: Joshua Daniel <[email protected]>
---
.../src/layers/Geojson/Geojson.test.ts | 121 ++++++++++
.../src/layers/Geojson/Geojson.tsx | 151 +++++++++++-
.../src/layers/Geojson/controlPanel.ts | 265 ++++++++++++++++++++-
.../src/utilities/Shared_DeckGL.tsx | 2 +-
.../src/utilities/controls.ts | 1 +
5 files changed, 535 insertions(+), 5 deletions(-)
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.test.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.test.ts
new file mode 100644
index 0000000000..bf3b8527df
--- /dev/null
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.test.ts
@@ -0,0 +1,121 @@
+/**
+ * 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 { SqlaFormData } from '@superset-ui/core';
+import {
+ computeGeoJsonTextOptionsFromJsOutput,
+ computeGeoJsonTextOptionsFromFormData,
+ computeGeoJsonIconOptionsFromJsOutput,
+ computeGeoJsonIconOptionsFromFormData,
+} from './Geojson';
+
+jest.mock('@deck.gl/react', () => ({
+ __esModule: true,
+ default: () => null,
+}));
+
+test('computeGeoJsonTextOptionsFromJsOutput returns an empty object for
non-object input', () => {
+ expect(computeGeoJsonTextOptionsFromJsOutput(null)).toEqual({});
+ expect(computeGeoJsonTextOptionsFromJsOutput(42)).toEqual({});
+ expect(computeGeoJsonTextOptionsFromJsOutput([1, 2, 3])).toEqual({});
+ expect(computeGeoJsonTextOptionsFromJsOutput('string')).toEqual({});
+});
+
+test('computeGeoJsonTextOptionsFromJsOutput extracts valid text options from
the input object', () => {
+ const input = {
+ getText: 'name',
+ getTextColor: [1, 2, 3, 255],
+ invalidOption: true,
+ };
+ const expectedOutput = {
+ getText: 'name',
+ getTextColor: [1, 2, 3, 255],
+ };
+ expect(computeGeoJsonTextOptionsFromJsOutput(input)).toEqual(expectedOutput);
+});
+
+test('computeGeoJsonTextOptionsFromFormData computes text options based on
form data', () => {
+ const formData: SqlaFormData = {
+ label_property_name: 'name',
+ label_color: { r: 1, g: 2, b: 3, a: 1 },
+ label_size: 123,
+ label_size_unit: 'pixels',
+ datasource: 'test_datasource',
+ viz_type: 'deck_geojson',
+ };
+
+ const expectedOutput = {
+ getText: expect.any(Function),
+ getTextColor: [1, 2, 3, 255],
+ getTextSize: 123,
+ textSizeUnits: 'pixels',
+ };
+
+ const actualOutput = computeGeoJsonTextOptionsFromFormData(formData);
+ expect(actualOutput).toEqual(expectedOutput);
+
+ const sampleFeature = { properties: { name: 'Test' } };
+ expect(actualOutput.getText(sampleFeature)).toBe('Test');
+});
+
+test('computeGeoJsonIconOptionsFromJsOutput returns an empty object for
non-object input', () => {
+ expect(computeGeoJsonIconOptionsFromJsOutput(null)).toEqual({});
+ expect(computeGeoJsonIconOptionsFromJsOutput(42)).toEqual({});
+ expect(computeGeoJsonIconOptionsFromJsOutput([1, 2, 3])).toEqual({});
+ expect(computeGeoJsonIconOptionsFromJsOutput('string')).toEqual({});
+});
+
+test('computeGeoJsonIconOptionsFromJsOutput extracts valid icon options from
the input object', () => {
+ const input = {
+ getIcon: 'icon_name',
+ getIconColor: [1, 2, 3, 255],
+ invalidOption: false,
+ };
+
+ const expectedOutput = {
+ getIcon: 'icon_name',
+ getIconColor: [1, 2, 3, 255],
+ };
+
+ expect(computeGeoJsonIconOptionsFromJsOutput(input)).toEqual(expectedOutput);
+});
+
+test('computeGeoJsonIconOptionsFromFormData computes icon options based on
form data', () => {
+ const formData: SqlaFormData = {
+ icon_url: 'https://example.com/icon.png',
+ icon_size: 123,
+ icon_size_unit: 'pixels',
+ datasource: 'test_datasource',
+ viz_type: 'deck_geojson',
+ };
+
+ const expectedOutput = {
+ getIcon: expect.any(Function),
+ getIconSize: 123,
+ iconSizeUnits: 'pixels',
+ };
+
+ const actualOutput = computeGeoJsonIconOptionsFromFormData(formData);
+ expect(actualOutput).toEqual(expectedOutput);
+
+ expect(actualOutput.getIcon()).toEqual({
+ url: 'https://example.com/icon.png',
+ height: 128,
+ width: 128,
+ });
+});
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
index acf98237ac..4ccaaaa8dd 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/Geojson.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { memo, useCallback, useMemo, useRef } from 'react';
-import { GeoJsonLayer } from '@deck.gl/layers';
+import { GeoJsonLayer, GeoJsonLayerProps } from '@deck.gl/layers';
// ignoring the eslint error below since typescript prefers 'geojson' to
'@types/geojson'
// eslint-disable-next-line import/no-unresolved
import { Feature, Geometry, GeoJsonProperties } from 'geojson';
@@ -29,6 +29,7 @@ import {
JsonValue,
QueryFormData,
SetDataMaskHook,
+ SqlaFormData,
} from '@superset-ui/core';
import {
@@ -44,6 +45,7 @@ import { TooltipProps } from '../../components/Tooltip';
import { Point } from '../../types';
import { GetLayerType } from '../../factory';
import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
+import { BLACK_COLOR, PRIMARY_COLOR } from '../../utilities/controls';
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
properties: JsonObject;
@@ -137,6 +139,114 @@ const getFillColor = (feature: JsonObject,
filterStateValue: unknown[]) => {
};
const getLineColor = (feature: JsonObject) => feature?.properties?.strokeColor;
+const isObject = (value: unknown): value is Record<string, unknown> =>
+ typeof value === 'object' && value !== null && !Array.isArray(value);
+
+export const computeGeoJsonTextOptionsFromJsOutput = (
+ output: unknown,
+): Partial<GeoJsonLayerProps> => {
+ if (!isObject(output)) return {};
+
+ // Properties sourced from:
+ //
https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-2
+ const options: (keyof GeoJsonLayerProps)[] = [
+ 'getText',
+ 'getTextColor',
+ 'getTextAngle',
+ 'getTextSize',
+ 'getTextAnchor',
+ 'getTextAlignmentBaseline',
+ 'getTextPixelOffset',
+ 'getTextBackgroundColor',
+ 'getTextBorderColor',
+ 'getTextBorderWidth',
+ 'textSizeUnits',
+ 'textSizeScale',
+ 'textSizeMinPixels',
+ 'textSizeMaxPixels',
+ 'textCharacterSet',
+ 'textFontFamily',
+ 'textFontWeight',
+ 'textLineHeight',
+ 'textMaxWidth',
+ 'textWordBreak',
+ 'textBackground',
+ 'textBackgroundPadding',
+ 'textOutlineColor',
+ 'textOutlineWidth',
+ 'textBillboard',
+ 'textFontSettings',
+ ];
+
+ const allEntries = Object.entries(output);
+ const validEntries = allEntries.filter(([k]) =>
+ options.includes(k as keyof GeoJsonLayerProps),
+ );
+ return Object.fromEntries(validEntries);
+};
+
+export const computeGeoJsonTextOptionsFromFormData = (
+ fd: SqlaFormData,
+): Partial<GeoJsonLayerProps> => {
+ const lc = fd.label_color ?? BLACK_COLOR;
+
+ return {
+ getText: (f: JsonObject) => f?.properties?.[fd.label_property_name],
+ getTextColor: [lc.r, lc.g, lc.b, 255 * lc.a],
+ getTextSize: parseInt(fd.label_size, 10),
+ textSizeUnits: fd.label_size_unit,
+ };
+};
+
+export const computeGeoJsonIconOptionsFromJsOutput = (
+ output: unknown,
+): Partial<GeoJsonLayerProps> => {
+ if (!isObject(output)) return {};
+
+ // Properties sourced from:
+ //
https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-1
+ const options: (keyof GeoJsonLayerProps)[] = [
+ 'getIcon',
+ 'getIconSize',
+ 'getIconColor',
+ 'getIconAngle',
+ 'getIconPixelOffset',
+ 'iconSizeUnits',
+ 'iconSizeScale',
+ 'iconSizeMinPixels',
+ 'iconSizeMaxPixels',
+ 'iconAtlas',
+ 'iconMapping',
+ 'iconBillboard',
+ 'iconAlphaCutoff',
+ ];
+
+ const allEntries = Object.entries(output);
+ const validEntries = allEntries.filter(([k]) =>
+ options.includes(k as keyof GeoJsonLayerProps),
+ );
+ return Object.fromEntries(validEntries);
+};
+
+export const computeGeoJsonIconOptionsFromFormData = (
+ fd: SqlaFormData,
+): Partial<GeoJsonLayerProps> => ({
+ getIcon: fd.icon_url
+ ? () => ({
+ url: fd.icon_url,
+ // This is the size deck.gl resizes the icon internally while
preserving
+ // its aspect ratio. This is not the actual size the icon is rendered
at,
+ // which is instead controlled by getIconSize below. These are set
because
+ // deck.gl requires it, and 128x128 is a reasonable default. Read more
at:
+ // https://deck.gl/docs/api-reference/layers/icon-layer#geticon
+ width: 128,
+ height: 128,
+ })
+ : undefined,
+ getIconSize: parseInt(fd.icon_size, 10),
+ iconSizeUnits: fd.icon_size_unit,
+});
+
export const getLayer: GetLayerType<GeoJsonLayer> = function ({
formData,
onContextMenu,
@@ -147,8 +257,8 @@ export const getLayer: GetLayerType<GeoJsonLayer> =
function ({
emitCrossFilters,
}) {
const fd = formData;
- const fc = fd.fill_color_picker;
- const sc = fd.stroke_color_picker;
+ const fc = fd.fill_color_picker ?? PRIMARY_COLOR;
+ const sc = fd.stroke_color_picker ?? PRIMARY_COLOR;
const fillColor = [fc.r, fc.g, fc.b, 255 * fc.a];
const strokeColor = [sc.r, sc.g, sc.b, 255 * sc.a];
const propOverrides: JsonObject = {};
@@ -169,6 +279,38 @@ export const getLayer: GetLayerType<GeoJsonLayer> =
function ({
processedFeatures = jsFnMutator(features) as ProcessedFeature[];
}
+ let pointType = 'circle';
+ if (fd.enable_labels) {
+ pointType = `${pointType}+text`;
+ }
+ if (fd.enable_icons) {
+ pointType = `${pointType}+icon`;
+ }
+
+ let labelOpts: Partial<GeoJsonLayerProps> = {};
+ if (fd.enable_labels) {
+ if (fd.enable_label_javascript_mode) {
+ const generator = sandboxedEval(fd.label_javascript_config_generator);
+ if (typeof generator === 'function') {
+ labelOpts = computeGeoJsonTextOptionsFromJsOutput(generator());
+ }
+ } else {
+ labelOpts = computeGeoJsonTextOptionsFromFormData(fd);
+ }
+ }
+
+ let iconOpts: Partial<GeoJsonLayerProps> = {};
+ if (fd.enable_icons) {
+ if (fd.enable_icon_javascript_mode) {
+ const generator = sandboxedEval(fd.icon_javascript_config_generator);
+ if (typeof generator === 'function') {
+ iconOpts = computeGeoJsonIconOptionsFromJsOutput(generator());
+ }
+ } else {
+ iconOpts = computeGeoJsonIconOptionsFromFormData(fd);
+ }
+ }
+
return new GeoJsonLayer({
id: `geojson-layer-${fd.slice_id}` as const,
data: processedFeatures,
@@ -181,6 +323,9 @@ export const getLayer: GetLayerType<GeoJsonLayer> =
function ({
getLineWidth: fd.line_width || 1,
pointRadiusScale: fd.point_radius_scale,
lineWidthUnits: fd.line_width_unit,
+ pointType,
+ ...labelOpts,
+ ...iconOpts,
...commonLayerProps({
formData: fd,
setTooltip,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/controlPanel.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/controlPanel.ts
index 568659e874..88c5a1a418 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/controlPanel.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Geojson/controlPanel.ts
@@ -17,7 +17,12 @@
* under the License.
*/
import { ControlPanelConfig } from '@superset-ui/chart-controls';
-import { t, legacyValidateInteger } from '@superset-ui/core';
+import {
+ t,
+ legacyValidateInteger,
+ isFeatureEnabled,
+ FeatureFlag,
+} from '@superset-ui/core';
import { formatSelectOptions } from '../../utilities/utils';
import {
filterNulls,
@@ -36,8 +41,27 @@ import {
lineWidth,
tooltipContents,
tooltipTemplate,
+ jsFunctionControl,
} from '../../utilities/Shared_DeckGL';
import { dndGeojsonColumn } from '../../utilities/sharedDndControls';
+import { BLACK_COLOR } from '../../utilities/controls';
+
+const defaultLabelConfigGenerator = `() => ({
+ // Check the documentation at:
+ //
https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-2
+ getText: f => f.properties.name,
+ getTextColor: [0, 0, 0, 255],
+ getTextSize: 24,
+ textSizeUnits: 'pixels',
+})`;
+
+const defaultIconConfigGenerator = `() => ({
+ // Check the documentation at:
+ //
https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-1
+ getIcon: () => ({ url: '', height: 128, width: 128 }),
+ getIconSize: 32,
+ iconSizeUnits: 'pixels',
+})`;
const config: ControlPanelConfig = {
controlPanelSections: [
@@ -63,6 +87,245 @@ const config: ControlPanelConfig = {
[fillColorPicker, strokeColorPicker],
[filled, stroked],
[extruded],
+ [
+ {
+ name: 'enable_labels',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Enable labels'),
+ description: t('Enables rendering of labels for GeoJSON points'),
+ default: false,
+ renderTrigger: true,
+ },
+ },
+ ],
+ [
+ {
+ name: 'enable_label_javascript_mode',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Enable label JavaScript mode'),
+ description: t(
+ 'Enables custom label configuration via JavaScript',
+ ),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_labels &&
+ isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
+ default: false,
+ renderTrigger: true,
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'label_property_name',
+ config: {
+ type: 'TextControl',
+ label: t('Label property name'),
+ description: t('The feature property to use for point labels'),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_labels &&
+ (!form_data.enable_label_javascript_mode ||
+ !isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
+ default: 'name',
+ renderTrigger: true,
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'label_color',
+ config: {
+ type: 'ColorPickerControl',
+ label: t('Label color'),
+ description: t('The color of the point labels'),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_labels &&
+ (!form_data.enable_label_javascript_mode ||
+ !isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
+ default: BLACK_COLOR,
+ renderTrigger: true,
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'label_size',
+ config: {
+ type: 'SelectControl',
+ freeForm: true,
+ label: t('Label size'),
+ description: t('The font size of the point labels'),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_labels &&
+ (!form_data.enable_label_javascript_mode ||
+ !isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
+ validators: [legacyValidateInteger],
+ choices: formatSelectOptions([8, 16, 24, 32, 64, 128]),
+ default: 24,
+ renderTrigger: true,
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'label_size_unit',
+ config: {
+ type: 'SelectControl',
+ label: t('Label size unit'),
+ description: t('The unit for label size'),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_labels &&
+ (!form_data.enable_label_javascript_mode ||
+ !isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
+ choices: [
+ ['meters', t('Meters')],
+ ['pixels', t('Pixels')],
+ ],
+ default: 'pixels',
+ renderTrigger: true,
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'label_javascript_config_generator',
+ config: {
+ ...jsFunctionControl(
+ t('Label JavaScript config generator'),
+ t(
+ 'A JavaScript function that generates a label configuration
object',
+ ),
+ undefined,
+ undefined,
+ defaultLabelConfigGenerator,
+ ),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_labels &&
+ !!form_data.enable_label_javascript_mode &&
+ isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'enable_icons',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Enable icons'),
+ description: t('Enables rendering of icons for GeoJSON points'),
+ default: false,
+ renderTrigger: true,
+ },
+ },
+ ],
+ [
+ {
+ name: 'enable_icon_javascript_mode',
+ config: {
+ type: 'CheckboxControl',
+ label: t('Enable icon JavaScript mode'),
+ description: t(
+ 'Enables custom icon configuration via JavaScript',
+ ),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_icons &&
+ isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
+ default: false,
+ renderTrigger: true,
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'icon_url',
+ config: {
+ type: 'TextControl',
+ label: t('Icon URL'),
+ description: t(
+ 'The image URL of the icon to display for GeoJSON points. ' +
+ 'Note that the image URL must conform to the content ' +
+ 'security policy (CSP) in order to load correctly.',
+ ),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_icons &&
+ (!form_data.enable_icon_javascript_mode ||
+ !isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
+ default: '',
+ renderTrigger: true,
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'icon_size',
+ config: {
+ type: 'SelectControl',
+ freeForm: true,
+ label: t('Icon size'),
+ description: t('The size of the point icons'),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_icons &&
+ (!form_data.enable_icon_javascript_mode ||
+ !isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
+ validators: [legacyValidateInteger],
+ choices: formatSelectOptions([16, 24, 32, 64, 128]),
+ default: 32,
+ renderTrigger: true,
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'icon_size_unit',
+ config: {
+ type: 'SelectControl',
+ label: t('Icon size unit'),
+ description: t('The unit for icon size'),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_icons &&
+ (!form_data.enable_icon_javascript_mode ||
+ !isFeatureEnabled(FeatureFlag.EnableJavascriptControls)),
+ choices: [
+ ['meters', t('Meters')],
+ ['pixels', t('Pixels')],
+ ],
+ default: 'pixels',
+ renderTrigger: true,
+ resetOnHide: false,
+ },
+ },
+ ],
+ [
+ {
+ name: 'icon_javascript_config_generator',
+ config: {
+ ...jsFunctionControl(
+ t('Icon JavaScript config generator'),
+ t(
+ 'A JavaScript function that generates an icon configuration
object',
+ ),
+ undefined,
+ undefined,
+ defaultIconConfigGenerator,
+ ),
+ visibility: ({ form_data }) =>
+ !!form_data.enable_icons &&
+ !!form_data.enable_icon_javascript_mode &&
+ isFeatureEnabled(FeatureFlag.EnableJavascriptControls),
+ resetOnHide: false,
+ },
+ },
+ ],
[lineWidth],
[
{
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
index f1a49107de..a40b7734c3 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx
@@ -96,7 +96,7 @@ const jsFunctionInfo = (
</div>
);
-function jsFunctionControl(
+export function jsFunctionControl(
label: string,
description: string,
extraDescr = null,
diff --git
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/controls.ts
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/controls.ts
index 03816e96dc..4900e7e506 100644
---
a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/controls.ts
+++
b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/utilities/controls.ts
@@ -39,6 +39,7 @@ export function columnChoices(datasource: Dataset |
QueryResponse | null) {
}
export const PRIMARY_COLOR = { r: 0, g: 122, b: 135, a: 1 };
+export const BLACK_COLOR = { r: 0, g: 0, b: 0, a: 1 };
export default {
default: null,