You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/10/21 19:44:24 UTC

(superset) branch fix/39430-deckgl-null-category-color created (now 9053da36936)

This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a change to branch fix/39430-deckgl-null-category-color
in repository https://gitbox.apache.org/repos/asf/superset.git


      at 9053da36936 chore: credit original author for deck.gl null-category fix

This branch includes the following new commits:

     new 3926744fb6d fix(deck.gl): render points with an empty categorical color value
     new 9053da36936 chore: credit original author for deck.gl null-category fix

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 01/02: fix(deck.gl): render points with an empty categorical color value

Posted by ru...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch fix/39430-deckgl-null-category-color
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 3926744fb6d96387dbf07228062135a677893054
Author: Damian Pendrak <dp...@gmail.com>
AuthorDate: Wed Jul 22 08:31:39 2026 +0200

    fix(deck.gl): render points with an empty categorical color value
    
    Scatter, Arc and Path only set `cat_color` when the dimension column held a
    non-null value. CategoricalDeckGLContainer then drops every feature whose
    `cat_color` has no matching legend entry, so rows with a null dimension
    vanished from the map instead of rendering as their own category.
    
    Assign those rows a dedicated `NULL_CATEGORY_KEY` so they get a legend entry,
    a color and a working toggle, and render that key as `Constants.NULL_DISPLAY`
    ("N/A") in both the legend and the tooltips - the same placeholder the table
    charts and the results pane already use for null values.
    
    Co-Authored-By: Claude Opus 4.8 <no...@anthropic.com>
---
 .../src/CategoricalDeckGLContainer.test.tsx        |  23 ++++
 .../src/components/Legend.test.tsx                 |  14 ++
 .../preset-chart-deckgl/src/components/Legend.tsx  |   5 +
 .../src/layers/Arc/transformProps.test.ts          | 153 +++++++++++++++++++++
 .../src/layers/Arc/transformProps.ts               |   8 +-
 .../src/layers/Path/transformProps.test.ts         |  30 ++++
 .../src/layers/Path/transformProps.ts              |   8 +-
 .../src/layers/Scatter/Scatter.tsx                 |   4 +-
 .../src/layers/Scatter/transformProps.test.ts      |  44 ++++++
 .../src/layers/Scatter/transformProps.ts           |   8 +-
 .../src/utilities/tooltipUtils.test.tsx            |  15 +-
 .../src/utilities/tooltipUtils.tsx                 |   3 +-
 .../plugins/preset-chart-deckgl/src/utils.ts       |   8 ++
 13 files changed, 312 insertions(+), 11 deletions(-)

diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/CategoricalDeckGLContainer.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/CategoricalDeckGLContainer.test.tsx
index 31e34451754..58ef4ae3743 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/CategoricalDeckGLContainer.test.tsx
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/CategoricalDeckGLContainer.test.tsx
@@ -26,6 +26,7 @@ import CategoricalDeckGLContainer, {
 } from './CategoricalDeckGLContainer';
 import { addColorToFeatures } from './utils/addColor';
 import { COLOR_SCHEME_TYPES } from './utilities/utils';
+import { NULL_CATEGORY_KEY } from './utils';
 
 // maplibre-gl 6 is ESM-only, so jest can't resolve it; DeckGLContainer.tsx
 // calls setWorkerUrl() on it at import time.
@@ -275,3 +276,25 @@ test('with autozoom a tilt written back by Explore keeps the viewer on the same
   );
   expect(mockViewports.at(-1)).toEqual(fitted);
 });
+
+test('rows with a null dimension get their own legend category', () => {
+  const fd = {
+    datasource: '1__table',
+    viz_type: 'deck_scatter',
+    color_scheme_type: COLOR_SCHEME_TYPES.categorical_palette,
+    color_scheme: 'supersetColors',
+    dimension: 'category',
+    slice_id: 42,
+    color_picker: { r: 0, g: 0, b: 0, a: 1 },
+  } as unknown as QueryFormData;
+  const data = [{ cat_color: 'A' }, { cat_color: NULL_CATEGORY_KEY }];
+
+  const categories = getCategories(fd, data);
+
+  expect(categories[NULL_CATEGORY_KEY]?.enabled).toBe(true);
+  expect(categories[NULL_CATEGORY_KEY]?.color).toBeDefined();
+
+  // Without a category entry the container drops the feature entirely, which is
+  // how null-dimension rows used to disappear from the map.
+  expect(data.filter(d => categories[d.cat_color]?.enabled)).toHaveLength(2);
+});
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.test.tsx
index 4d36b66efcd..f65322cb431 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.test.tsx
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.test.tsx
@@ -22,7 +22,9 @@ import { createEvent, fireEvent, render, screen } from '@testing-library/react';
 import '@testing-library/jest-dom';
 import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme';
 import type { ReactElement } from 'react';
+import { Constants } from '@superset-ui/core/components';
 import Legend, { type LegendProps } from './Legend';
+import { NULL_CATEGORY_KEY } from '../utils';
 
 const renderWithTheme = (component: ReactElement) =>
   render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
@@ -73,6 +75,18 @@ test('leaves labels untouched when no format is provided', () => {
   expect(screen.getByText('[1, 81)')).toBeInTheDocument();
 });
 
+test('shows the null category as N/A instead of its internal key', () => {
+  renderWithTheme(
+    <Legend
+      format=",.2f"
+      categories={{ [NULL_CATEGORY_KEY]: { enabled: true, color: [0, 0, 0] } }}
+    />,
+  );
+
+  expect(screen.getByText(Constants.NULL_DISPLAY)).toBeInTheDocument();
+  expect(screen.queryByText(NULL_CATEGORY_KEY)).not.toBeInTheDocument();
+});
+
 test('clicking a legend item toggles the category without triggering anchor navigation', () => {
   // Regression proof for #33576: legend items render as real <button>
   // elements (not href="#" anchors), so a click has no default navigation
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.tsx
index 21056c6a166..bb6634201de 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.tsx
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.tsx
@@ -22,6 +22,7 @@ import { memo } from 'react';
 import { formatNumber } from '@superset-ui/core';
 import { styled } from '@apache-superset/core/theme';
 import { Color } from '@deck.gl/core';
+import { NULL_CATEGORY_KEY, formatNullCategory } from '../utils';
 
 const StyledLegend = styled.div`
   ${({ theme }) => `
@@ -120,6 +121,10 @@ const Legend = ({
   };
 
   const formatCategoryLabel = (k: string) => {
+    if (k === NULL_CATEGORY_KEY) {
+      return formatNullCategory(k);
+    }
+
     if (!d3Format) {
       return k;
     }
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/transformProps.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/transformProps.test.ts
new file mode 100644
index 00000000000..8984c2e395d
--- /dev/null
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/transformProps.test.ts
@@ -0,0 +1,153 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ChartProps, DatasourceType } from '@superset-ui/core';
+import transformProps from './transformProps';
+import { NULL_CATEGORY_KEY } from '../../utils';
+
+interface ArcFeature {
+  sourcePosition: [number, number];
+  targetPosition: [number, number];
+  cat_color?: string;
+  extraProps?: Record<string, unknown>;
+}
+
+jest.mock('../spatialUtils', () => ({
+  ...jest.requireActual('../spatialUtils'),
+  getMapboxApiKey: jest.fn(() => 'mock-mapbox-key'),
+}));
+
+const mockChartProps: Partial<ChartProps> = {
+  rawFormData: {
+    start_spatial: {
+      type: 'latlong',
+      latCol: 'START_LAT',
+      lonCol: 'START_LON',
+    },
+    end_spatial: {
+      type: 'latlong',
+      latCol: 'END_LAT',
+      lonCol: 'END_LON',
+    },
+    viewport: {},
+  },
+  queriesData: [
+    {
+      data: [
+        {
+          START_LAT: 37.8,
+          START_LON: -122.4,
+          END_LAT: 37.9,
+          END_LON: -122.3,
+          category: 'A',
+        },
+        {
+          START_LAT: 38.0,
+          START_LON: -122.2,
+          END_LAT: 38.1,
+          END_LON: -122.1,
+          category: 'B',
+        },
+      ],
+    },
+  ],
+  datasource: {
+    type: DatasourceType.Table,
+    id: 1,
+    name: 'test_datasource',
+    columns: [],
+    metrics: [],
+  },
+  height: 400,
+  width: 600,
+  hooks: {},
+  filterState: {},
+  emitCrossFilters: false,
+};
+
+test('Arc transformProps should set cat_color from dimension column', () => {
+  const props = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      dimension: 'category',
+    },
+  };
+
+  const result = transformProps(props as ChartProps);
+  const features = result.payload.data.features as ArcFeature[];
+
+  expect(features).toHaveLength(2);
+  expect(features[0]?.cat_color).toBe('A');
+  expect(features[1]?.cat_color).toBe('B');
+});
+
+test('Arc transformProps should fall back to NULL_CATEGORY_KEY for empty dimension values', () => {
+  const props = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      dimension: 'category',
+    },
+    queriesData: [
+      {
+        data: [
+          {
+            START_LAT: 37.8,
+            START_LON: -122.4,
+            END_LAT: 37.9,
+            END_LON: -122.3,
+            category: 'A',
+          },
+          {
+            START_LAT: 38.0,
+            START_LON: -122.2,
+            END_LAT: 38.1,
+            END_LON: -122.1,
+            category: null,
+          },
+          {
+            START_LAT: 38.2,
+            START_LON: -122.0,
+            END_LAT: 38.3,
+            END_LON: -121.9,
+            category: undefined,
+          },
+        ],
+      },
+    ],
+  };
+
+  const result = transformProps(props as ChartProps);
+  const features = result.payload.data.features as ArcFeature[];
+
+  expect(features).toHaveLength(3);
+  expect(features[0]?.cat_color).toBe('A');
+  expect(features[1]?.cat_color).toBe(NULL_CATEGORY_KEY);
+  expect(features[2]?.cat_color).toBe(NULL_CATEGORY_KEY);
+});
+
+test('Arc transformProps should not set cat_color when no dimension is configured', () => {
+  const result = transformProps(mockChartProps as ChartProps);
+  const features = result.payload.data.features as ArcFeature[];
+
+  expect(features).toHaveLength(2);
+  expect(features[0]?.cat_color).toBeUndefined();
+  expect(features[1]?.cat_color).toBeUndefined();
+});
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/transformProps.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/transformProps.ts
index 44e369d44e0..d9e4789ab74 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/transformProps.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/transformProps.ts
@@ -24,6 +24,7 @@ import {
   addPropertiesToFeature,
 } from '../transformUtils';
 import { DeckArcFormData } from './buildQuery';
+import { NULL_CATEGORY_KEY } from '../../utils';
 
 interface ArcPoint {
   sourcePosition: [number, number];
@@ -65,8 +66,11 @@ function processArcData(
         extraProps: {},
       };
 
-      if (dimension && record[dimension] != null) {
-        arcPoint.cat_color = String(record[dimension]);
+      if (dimension) {
+        arcPoint.cat_color =
+          record[dimension] != null
+            ? String(record[dimension])
+            : NULL_CATEGORY_KEY;
       }
 
       // eslint-disable-next-line no-underscore-dangle
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/transformProps.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/transformProps.test.ts
index c8ba39d0e94..d643ef7339d 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/transformProps.test.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/transformProps.test.ts
@@ -18,6 +18,7 @@
  */
 import { ChartProps, DatasourceType } from '@superset-ui/core';
 import transformProps from './transformProps';
+import { NULL_CATEGORY_KEY } from '../../utils';
 
 interface PathFeature {
   path: [number, number][];
@@ -320,6 +321,35 @@ test('Path transformProps should set cat_color from dimension column', () => {
   expect(features[2]?.cat_color).toBe('express');
 });
 
+test('Path transformProps should fall back to NULL_CATEGORY_KEY for empty dimension values', () => {
+  const props = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      dimension: 'route_type',
+    },
+    queriesData: [
+      {
+        data: [
+          { path_json: samplePath1, route_type: 'express' },
+          { path_json: samplePath2, route_type: null },
+          { path_json: samplePath3, route_type: undefined },
+        ],
+      },
+    ],
+  };
+
+  const result = transformProps(props as ChartProps);
+  const features = result.payload.data.features as PathFeature[];
+
+  // Path emits features in reverse record order.
+  expect(features.map(f => f.cat_color)).toEqual([
+    NULL_CATEGORY_KEY,
+    NULL_CATEGORY_KEY,
+    'express',
+  ]);
+});
+
 test('Path transformProps should include metric labels when breakpoint_metric is set', () => {
   const props = {
     ...mockChartProps,
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/transformProps.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/transformProps.ts
index ae098956ca3..9625db8fe39 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/transformProps.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/transformProps.ts
@@ -27,6 +27,7 @@ import {
 } from '../transformUtils';
 import { DeckPathFormData } from './buildQuery';
 import { isFixedValue, getFixedValue } from '../utils/metricUtils';
+import { NULL_CATEGORY_KEY } from '../../utils';
 
 declare global {
   interface Window {
@@ -145,8 +146,11 @@ function processPathData(
       }
     }
 
-    if (categoryColumn && record[categoryColumn] != null) {
-      feature.cat_color = String(record[categoryColumn]);
+    if (categoryColumn) {
+      feature.cat_color =
+        record[categoryColumn] != null
+          ? String(record[categoryColumn])
+          : NULL_CATEGORY_KEY;
     }
 
     feature = addPropertiesToFeature(feature, record, excludeKeys);
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/Scatter.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/Scatter.tsx
index 5209493a3b0..2eb7f46b2f1 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/Scatter.tsx
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/Scatter.tsx
@@ -25,7 +25,7 @@ import { createCategoricalDeckGLComponent, GetLayerType } from '../../factory';
 import { createTooltipContent } from '../../utilities/tooltipUtils';
 import TooltipRow from '../../TooltipRow';
 import { unitToRadius } from '../../utils/geo';
-import { HIGHLIGHT_COLOR_ARRAY } from '../../utils';
+import { HIGHLIGHT_COLOR_ARRAY, formatNullCategory } from '../../utils';
 import { isMetricValue, extractMetricKey } from '../utils/metricUtils';
 
 function getMetricLabel(metric: any) {
@@ -67,7 +67,7 @@ function setTooltipContent(
         {o.object?.cat_color && (
           <TooltipRow
             label={`${t('Category')}: `}
-            value={`${o.object?.cat_color}`}
+            value={formatNullCategory(`${o.object?.cat_color}`)}
           />
         )}
         {o.object?.metric && label && (
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts
index 9728425c937..2489c0b3c80 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.test.ts
@@ -22,6 +22,7 @@ import { logging } from '@apache-superset/core/utils';
 import transformProps, {
   filterDrawableGeographicPoints,
 } from './transformProps';
+import { NULL_CATEGORY_KEY } from '../../utils';
 
 interface ScatterFeature {
   position: [number, number];
@@ -197,6 +198,49 @@ test('Scatter transformProps should handle dimension for category colors', () =>
   expect(features[1]?.cat_color).toBe('B');
 });
 
+test('Scatter transformProps should fall back to NULL_CATEGORY_KEY for empty dimension values', () => {
+  const propsWithNullCategory = {
+    ...mockChartProps,
+    rawFormData: {
+      ...mockChartProps.rawFormData,
+      dimension: 'category',
+      point_radius_fixed: {
+        type: 'fix',
+        value: '1000',
+      },
+    },
+    queriesData: [
+      {
+        data: [
+          {
+            LATITUDE: 37.8,
+            LONGITUDE: -122.4,
+            category: 'A',
+          },
+          {
+            LATITUDE: 37.9,
+            LONGITUDE: -122.3,
+            category: null,
+          },
+          {
+            LATITUDE: 38.0,
+            LONGITUDE: -122.2,
+            category: undefined,
+          },
+        ],
+      },
+    ],
+  };
+
+  const result = transformProps(propsWithNullCategory as ChartProps);
+  const features = result.payload.data.features as ScatterFeature[];
+
+  expect(features).toHaveLength(3);
+  expect(features[0]?.cat_color).toBe('A');
+  expect(features[1]?.cat_color).toBe(NULL_CATEGORY_KEY);
+  expect(features[2]?.cat_color).toBe(NULL_CATEGORY_KEY);
+});
+
 test('Scatter transformProps should not include metric labels for fixed radius', () => {
   const fixedProps = {
     ...mockChartProps,
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts
index 6f2f2612d0a..ba558c79303 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Scatter/transformProps.ts
@@ -32,6 +32,7 @@ import {
   getFixedValue,
   getTypedFixedRadius,
 } from '../utils/metricUtils';
+import { NULL_CATEGORY_KEY } from '../../utils';
 
 interface ScatterPoint {
   position: [number, number];
@@ -155,9 +156,10 @@ function processScatterData(
       }
     }
 
-    const categoryValue = getSourceValue(categoryColumn);
-    if (categoryValue != null) {
-      scatterPoint.cat_color = String(categoryValue);
+    if (categoryColumn) {
+      const categoryValue = getSourceValue(categoryColumn);
+      scatterPoint.cat_color =
+        categoryValue != null ? String(categoryValue) : NULL_CATEGORY_KEY;
     }
 
     scatterPoint = addPropertiesToFeature(
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/utilities/tooltipUtils.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/utilities/tooltipUtils.test.tsx
index 7b528a03b27..5cf0614612a 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/utilities/tooltipUtils.test.tsx
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/utilities/tooltipUtils.test.tsx
@@ -18,8 +18,21 @@
  */
 
 import { JsonObject, QueryFormData } from '@superset-ui/core';
+import { Constants } from '@superset-ui/core/components';
 import { render } from '@testing-library/react';
-import { createTooltipContent } from './tooltipUtils';
+import { createTooltipContent, CommonTooltipRows } from './tooltipUtils';
+import { NULL_CATEGORY_KEY } from '../utils';
+
+test('category row shows the null category as N/A, matching the legend', () => {
+  const { container } = render(
+    <>
+      {CommonTooltipRows.category({ object: { cat_color: NULL_CATEGORY_KEY } })}
+    </>,
+  );
+
+  expect(container.textContent).toContain(Constants.NULL_DISPLAY);
+  expect(container.textContent).not.toContain(NULL_CATEGORY_KEY);
+});
 
 test('buildFieldBasedTooltipItems does not append configured metric when tooltip_contents already has string metric', () => {
   const formData = {
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/utilities/tooltipUtils.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/utilities/tooltipUtils.tsx
index b1c4a1442ef..213694d1e5a 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/utilities/tooltipUtils.tsx
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/utilities/tooltipUtils.tsx
@@ -23,6 +23,7 @@ import { useMemo, memo } from 'react';
 import { HandlebarsRenderer } from './HandlebarsRenderer';
 import TooltipRow from '../TooltipRow';
 import { createDefaultTemplateWithLimits } from './multiValueUtils';
+import { formatNullCategory } from '../utils';
 
 const MemoizedHandlebarsRenderer = memo(HandlebarsRenderer);
 
@@ -58,7 +59,7 @@ export const CommonTooltipRows = {
     o.object?.cat_color ? (
       <TooltipRow
         label={`${t('Category')}: `}
-        value={`${o.object.cat_color}`}
+        value={formatNullCategory(`${o.object.cat_color}`)}
       />
     ) : null,
 
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/utils.ts b/superset-frontend/plugins/preset-chart-deckgl/src/utils.ts
index b33dad7517e..8ed2b2050df 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/utils.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/utils.ts
@@ -25,12 +25,20 @@ import {
   QueryFormData,
   SequentialScheme,
 } from '@superset-ui/core';
+import { Constants } from '@superset-ui/core/components';
 import { Color } from '@deck.gl/core';
 import { hexToRGB } from './utils/colors';
 import { ColorBreakpointType } from './types';
 
 export const TRANSPARENT_COLOR_ARRAY = [0, 0, 0, 0] as Color;
 export const HIGHLIGHT_COLOR_ARRAY = [255, 0, 0, 255] as Color;
+// Stands in for a null dimension value so those rows still get a category, a
+// color and a legend entry. Kept distinct from NULL_DISPLAY so the key stays
+// stable across locales.
+export const NULL_CATEGORY_KEY = '<NULL>';
+
+export const formatNullCategory = (value: string) =>
+  value === NULL_CATEGORY_KEY ? Constants.NULL_DISPLAY : value;
 
 const DEFAULT_NUM_BUCKETS = 10;
 


(superset) 02/02: chore: credit original author for deck.gl null-category fix

Posted by ru...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch fix/39430-deckgl-null-category-color
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 9053da369367312474847cb9e404ed16dd215b73
Author: Evan Rusackas <ev...@rusackas.com>
AuthorDate: Wed Oct 21 12:44:02 2026 -0700

    chore: credit original author for deck.gl null-category fix
    
    Adopting this fix as a fresh PR since the OAuth token used here lacks
    the `workflow` scope needed to push directly to the contributor's
    fork. The preceding commit is unchanged aside from a rebase onto
    current master.
    
    Co-Authored-By: Damian Pendrak <dp...@gmail.com>
    Co-Authored-By: Evan Rusackas <ev...@preset.io>
    Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>