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/06/25 19:23:41 UTC
(superset) branch master updated: fix(deck.gl): use interval notation for Polygon legend bucket labels (#41400)
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 aac02ab6793 fix(deck.gl): use interval notation for Polygon legend bucket labels (#41400)
aac02ab6793 is described below
commit aac02ab679356c61e86cada0b42785a2973b1776
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Thu Jun 25 12:23:34 2026 -0700
fix(deck.gl): use interval notation for Polygon legend bucket labels (#41400)
Co-authored-by: Claude Opus 4.8 <no...@anthropic.com>
---
.../src/components/Legend.test.tsx | 71 ++++++++++++++++++++++
.../preset-chart-deckgl/src/components/Legend.tsx | 36 +++++++++++
.../plugins/preset-chart-deckgl/src/utils.test.ts | 47 +++++++++++++-
.../plugins/preset-chart-deckgl/src/utils.ts | 9 ++-
4 files changed, 161 insertions(+), 2 deletions(-)
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
new file mode 100644
index 00000000000..009d612a6f2
--- /dev/null
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.test.tsx
@@ -0,0 +1,71 @@
+/**
+ * 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.
+ */
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { render, screen } from '@testing-library/react';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import '@testing-library/jest-dom';
+import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme';
+import type { ReactElement } from 'react';
+import Legend from './Legend';
+
+const renderWithTheme = (component: ReactElement) =>
+ render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
+
+test('formats interval-notation labels while preserving brackets', () => {
+ renderWithTheme(
+ <Legend
+ format=",.2f"
+ categories={{
+ '[1, 81)': { enabled: true, color: [0, 0, 0] },
+ '[81, 212)': { enabled: true, color: [0, 0, 0] },
+ '[212, 369]': { enabled: true, color: [0, 0, 0] },
+ }}
+ />,
+ );
+
+ expect(screen.getByText('[1.00, 81.00)')).toBeInTheDocument();
+ expect(screen.getByText('[81.00, 212.00)')).toBeInTheDocument();
+ expect(screen.getByText('[212.00, 369.00]')).toBeInTheDocument();
+});
+
+test('still formats legacy "a - b" delimiter labels', () => {
+ renderWithTheme(
+ <Legend
+ format=",.1f"
+ categories={{
+ '0 - 100000': { enabled: true, color: [0, 0, 0] },
+ '100001 - 200000': { enabled: true, color: [0, 0, 0] },
+ }}
+ />,
+ );
+
+ expect(screen.getByText('0.0 - 100,000.0')).toBeInTheDocument();
+ expect(screen.getByText('100,001.0 - 200,000.0')).toBeInTheDocument();
+});
+
+test('leaves labels untouched when no format is provided', () => {
+ renderWithTheme(
+ <Legend
+ format={null}
+ categories={{ '[1, 81)': { enabled: true, color: [0, 0, 0] } }}
+ />,
+ );
+
+ expect(screen.getByText('[1, 81)')).toBeInTheDocument();
+});
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 32b995d08d8..96518a3fa38 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.tsx
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/components/Legend.tsx
@@ -59,6 +59,33 @@ const StyledLegend = styled.div`
const categoryDelimiter = ' - ';
+const OPENING_BRACKETS = '[(';
+const CLOSING_BRACKETS = '])';
+
+// Recognize half-open interval labels like "[1, 81)" or "[81, 212]" emitted by
+// getBuckets: brackets on the ends, two comma-separated bounds in between.
+// Returns the parsed pieces, or null when the label isn't interval notation.
+const parseInterval = (label: string) => {
+ const open = label[0];
+ const close = label[label.length - 1];
+ if (!OPENING_BRACKETS.includes(open) || !CLOSING_BRACKETS.includes(close)) {
+ return null;
+ }
+
+ const bounds = label.slice(1, -1).split(',');
+ if (bounds.length !== 2) {
+ return null;
+ }
+
+ const lower = bounds[0].trim();
+ const upper = bounds[1].trim();
+ if (!lower || !upper) {
+ return null;
+ }
+
+ return { open, lower, upper, close };
+};
+
export type LegendProps = {
format: string | null;
forceCategorical?: boolean;
@@ -91,6 +118,15 @@ const Legend = ({
return k;
}
+ // Format each numeric bound of an interval label while preserving the
+ // brackets and separator, e.g. "[1, 81)" -> "[1.00, 81.00)".
+ const interval = parseInterval(k);
+ if (interval) {
+ const { open, lower, upper, close } = interval;
+
+ return `${open}${format(lower)}, ${format(upper)}${close}`;
+ }
+
if (k.includes(categoryDelimiter)) {
const values = k.split(categoryDelimiter);
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/utils.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/utils.test.ts
index 717c6f68ab8..41cdd37d789 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/utils.test.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/utils.test.ts
@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { getColorBreakpointsBuckets, getBreakPoints } from './utils';
+import { JsonObject, QueryFormData } from '@superset-ui/core';
+import {
+ getColorBreakpointsBuckets,
+ getBreakPoints,
+ getBuckets,
+ BucketsWithColorScale,
+} from './utils';
import { ColorBreakpointType } from './types';
describe('getColorBreakpointsBuckets', () => {
@@ -488,3 +494,42 @@ describe('getBreakPoints', () => {
});
});
});
+
+describe('getBuckets', () => {
+ const accessor = (d: JsonObject) => d.value;
+
+ const buildFeatures = (values: number[]) => values.map(value => ({ value }));
+
+ test('produces non-overlapping bucket labels (no shared endpoints)', () => {
+ // With break points [1, 81, 212, 369] the legacy behavior produced
+ // "1 - 81", "81 - 212", "212 - 369" where each interior breakpoint
+ // (81, 212) appeared in two adjacent labels, reading as overlapping
+ // ranges. Labels should instead form a clean, non-overlapping partition.
+ const fd: QueryFormData & BucketsWithColorScale = {
+ datasource: '1__table',
+ viz_type: 'deck_polygon',
+ break_points: ['1', '81', '212', '369'],
+ num_buckets: '3',
+ linear_color_scheme: ['#000000', '#ffffff'],
+ opacity: 100,
+ metric: 'count',
+ };
+ const features = buildFeatures([1, 50, 100, 200, 300, 369]);
+
+ const buckets = getBuckets(fd, features, accessor);
+ const labels = Object.keys(buckets);
+
+ // Three buckets for four breakpoints
+ expect(labels).toHaveLength(3);
+
+ // Interval notation: half-open everywhere except the last bucket, which
+ // is closed so the maximum value is included.
+ expect(labels).toEqual(['[1, 81)', '[81, 212)', '[212, 369]']);
+
+ // No numeric endpoint should appear as both an upper bound of one bucket
+ // and a lower bound of the next in an ambiguous "a - b" form.
+ labels.forEach(label => {
+ expect(label).not.toMatch(/^\d+(\.\d+)?\s-\s\d+(\.\d+)?$/);
+ });
+ });
+});
diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/utils.ts b/superset-frontend/plugins/preset-chart-deckgl/src/utils.ts
index 2c42b918b7f..b33dad7517e 100644
--- a/superset-frontend/plugins/preset-chart-deckgl/src/utils.ts
+++ b/superset-frontend/plugins/preset-chart-deckgl/src/utils.ts
@@ -200,8 +200,15 @@ export function getBuckets(
string,
{ color: Color | undefined; enabled: boolean }
> = {};
+ const lastBucketIndex = breakPoints.length - 2;
breakPoints.slice(1).forEach((_, i) => {
- const range = `${breakPoints[i]} - ${breakPoints[i + 1]}`;
+ // Use half-open interval notation so adjacent buckets don't share an
+ // ambiguous endpoint. This mirrors the d3 `scaleThreshold` binning used
+ // for coloring, where each breakpoint is a half-open cut point. The final
+ // bucket is closed on the right so the maximum value is included.
+ const isLastBucket = i === lastBucketIndex;
+ const closingBracket = isLastBucket ? ']' : ')';
+ const range = `[${breakPoints[i]}, ${breakPoints[i + 1]}${closingBracket}`;
const mid =
0.5 * (parseFloat(breakPoints[i]) + parseFloat(breakPoints[i + 1]));
// fix polygon doesn't show