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,

Reply via email to