This is an automated email from the ASF dual-hosted git repository. msyavuz 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 fb840b8e71 fix(deck.gl): restore legend display for Polygon charts with linear palette and fixed color schemes (#35142) fb840b8e71 is described below commit fb840b8e714ff4c54736bd79d932e690706401d9 Author: Joe Li <j...@preset.io> AuthorDate: Tue Sep 16 10:20:42 2025 -0700 fix(deck.gl): restore legend display for Polygon charts with linear palette and fixed color schemes (#35142) Co-authored-by: Claude <nore...@anthropic.com> --- .../src/layers/Polygon/Polygon.test.tsx | 355 +++++++++++++++++++++ .../src/layers/Polygon/Polygon.tsx | 7 +- 2 files changed, 359 insertions(+), 3 deletions(-) diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.test.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.test.tsx new file mode 100644 index 0000000000..a3d97f4880 --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.test.tsx @@ -0,0 +1,355 @@ +/** + * 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 '@superset-ui/core'; +import DeckGLPolygon, { getPoints } from './Polygon'; +import { COLOR_SCHEME_TYPES } from '../../utilities/utils'; +import * as utils from '../../utils'; + +// Mock the utils functions +const mockGetBuckets = jest.spyOn(utils, 'getBuckets'); +const mockGetColorBreakpointsBuckets = jest.spyOn( + utils, + 'getColorBreakpointsBuckets', +); + +// Mock DeckGL container and Legend +jest.mock('../../DeckGLContainer', () => ({ + DeckGLContainerStyledWrapper: ({ children }: any) => ( + <div data-testid="deckgl-container">{children}</div> + ), +})); + +jest.mock('../../components/Legend', () => ({ categories, position }: any) => ( + <div + data-testid="legend" + data-categories={JSON.stringify(categories)} + data-position={position} + > + Legend Mock + </div> +)); + +const mockProps = { + formData: { + // Required QueryFormData properties + datasource: 'test_datasource', + viz_type: 'deck_polygon', + // Polygon-specific properties + metric: { label: 'population' }, + color_scheme_type: COLOR_SCHEME_TYPES.linear_palette, + legend_position: 'tr', + legend_format: '.2f', + autozoom: false, + mapbox_style: 'mapbox://styles/mapbox/light-v9', + opacity: 80, + filled: true, + stroked: true, + extruded: false, + line_width: 1, + line_width_unit: 'pixels', + multiplier: 1, + break_points: [], + num_buckets: '5', + linear_color_scheme: 'blue_white_yellow', + }, + payload: { + data: { + features: [ + { + population: 100000, + polygon: [ + [0, 0], + [1, 0], + [1, 1], + [0, 1], + ], + }, + { + population: 200000, + polygon: [ + [2, 2], + [3, 2], + [3, 3], + [2, 3], + ], + }, + ], + mapboxApiKey: 'test-key', + }, + form_data: {}, + }, + setControlValue: jest.fn(), + viewport: { longitude: 0, latitude: 0, zoom: 1 }, + onAddFilter: jest.fn(), + width: 800, + height: 600, + onContextMenu: jest.fn(), + setDataMask: jest.fn(), + filterState: undefined, + emitCrossFilters: false, +}; + +describe('DeckGLPolygon bucket generation logic', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetBuckets.mockReturnValue({ + '100000 - 150000': { color: [0, 100, 200], enabled: true }, + '150000 - 200000': { color: [50, 150, 250], enabled: true }, + }); + mockGetColorBreakpointsBuckets.mockReturnValue({}); + }); + + const renderWithTheme = (component: React.ReactElement) => + render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>); + + test('should use getBuckets for linear_palette color scheme', () => { + const propsWithLinearPalette = { + ...mockProps, + formData: { + ...mockProps.formData, + color_scheme_type: COLOR_SCHEME_TYPES.linear_palette, + }, + }; + + renderWithTheme(<DeckGLPolygon {...propsWithLinearPalette} />); + + // Should call getBuckets, not getColorBreakpointsBuckets + expect(mockGetBuckets).toHaveBeenCalled(); + expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); + }); + + test('should use getBuckets for fixed_color color scheme', () => { + const propsWithFixedColor = { + ...mockProps, + formData: { + ...mockProps.formData, + color_scheme_type: COLOR_SCHEME_TYPES.fixed_color, + }, + }; + + renderWithTheme(<DeckGLPolygon {...propsWithFixedColor} />); + + // Should call getBuckets, not getColorBreakpointsBuckets + expect(mockGetBuckets).toHaveBeenCalled(); + expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); + }); + + test('should use getColorBreakpointsBuckets for color_breakpoints scheme', () => { + const propsWithBreakpoints = { + ...mockProps, + formData: { + ...mockProps.formData, + color_scheme_type: COLOR_SCHEME_TYPES.color_breakpoints, + color_breakpoints: [ + { + minValue: 0, + maxValue: 100000, + color: { r: 255, g: 0, b: 0, a: 100 }, + }, + { + minValue: 100001, + maxValue: 200000, + color: { r: 0, g: 255, b: 0, a: 100 }, + }, + ], + }, + }; + + mockGetColorBreakpointsBuckets.mockReturnValue({ + '0 - 100000': { color: [255, 0, 0], enabled: true }, + '100001 - 200000': { color: [0, 255, 0], enabled: true }, + }); + + renderWithTheme(<DeckGLPolygon {...propsWithBreakpoints} />); + + // Should call getColorBreakpointsBuckets, not getBuckets + expect(mockGetColorBreakpointsBuckets).toHaveBeenCalled(); + expect(mockGetBuckets).not.toHaveBeenCalled(); + }); + + test('should use getBuckets when color_scheme_type is undefined (backward compatibility)', () => { + const propsWithUndefinedScheme = { + ...mockProps, + formData: { + ...mockProps.formData, + color_scheme_type: undefined, + }, + }; + + renderWithTheme(<DeckGLPolygon {...propsWithUndefinedScheme} />); + + // Should call getBuckets for backward compatibility + expect(mockGetBuckets).toHaveBeenCalled(); + expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); + }); + + test('should use getBuckets for unsupported color schemes (categorical_palette)', () => { + const propsWithUnsupportedScheme = { + ...mockProps, + formData: { + ...mockProps.formData, + color_scheme_type: COLOR_SCHEME_TYPES.categorical_palette, + }, + }; + + renderWithTheme(<DeckGLPolygon {...propsWithUnsupportedScheme} />); + + // Should fall back to getBuckets for unsupported color schemes + expect(mockGetBuckets).toHaveBeenCalled(); + expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); + }); +}); + +describe('DeckGLPolygon Error Handling and Edge Cases', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetBuckets.mockReturnValue({}); + mockGetColorBreakpointsBuckets.mockReturnValue({}); + }); + + const renderWithTheme = (component: React.ReactElement) => + render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>); + + test('handles empty features data gracefully', () => { + const propsWithEmptyData = { + ...mockProps, + payload: { + ...mockProps.payload, + data: { + ...mockProps.payload.data, + features: [], + }, + }, + }; + + renderWithTheme(<DeckGLPolygon {...propsWithEmptyData} />); + + // Should still call getBuckets with empty data + expect(mockGetBuckets).toHaveBeenCalled(); + expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); + }); + + test('handles missing color_breakpoints for color_breakpoints scheme', () => { + const propsWithMissingBreakpoints = { + ...mockProps, + formData: { + ...mockProps.formData, + color_scheme_type: COLOR_SCHEME_TYPES.color_breakpoints, + color_breakpoints: undefined, + }, + }; + + renderWithTheme(<DeckGLPolygon {...propsWithMissingBreakpoints} />); + + // Should call getColorBreakpointsBuckets even with undefined breakpoints + expect(mockGetColorBreakpointsBuckets).toHaveBeenCalledWith(undefined); + expect(mockGetBuckets).not.toHaveBeenCalled(); + }); + + test('handles null legend_position correctly', () => { + const propsWithNullLegendPosition = { + ...mockProps, + formData: { + ...mockProps.formData, + legend_position: null, + }, + }; + + renderWithTheme(<DeckGLPolygon {...propsWithNullLegendPosition} />); + + // Legend should not be rendered when position is null + expect(screen.queryByTestId('legend')).not.toBeInTheDocument(); + }); +}); + +describe('DeckGLPolygon Legend Integration', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetBuckets.mockReturnValue({ + '100000 - 150000': { color: [0, 100, 200], enabled: true }, + '150000 - 200000': { color: [50, 150, 250], enabled: true }, + }); + }); + + const renderWithTheme = (component: React.ReactElement) => + render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>); + + test('renders legend with non-empty categories when metric and linear_palette are defined', () => { + const { container } = renderWithTheme(<DeckGLPolygon {...mockProps} />); + + // Verify the component renders and calls the correct bucket function + expect(mockGetBuckets).toHaveBeenCalled(); + expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); + + // Verify the legend mock was rendered with non-empty categories + const legendElement = container.querySelector('[data-testid="legend"]'); + expect(legendElement).toBeTruthy(); + const categoriesAttr = legendElement?.getAttribute('data-categories'); + const categoriesData = JSON.parse(categoriesAttr || '{}'); + expect(Object.keys(categoriesData)).toHaveLength(2); + }); + + test('does not render legend when metric is null', () => { + const propsWithoutMetric = { + ...mockProps, + formData: { + ...mockProps.formData, + metric: null, + }, + }; + + renderWithTheme(<DeckGLPolygon {...propsWithoutMetric} />); + + // Legend should not be rendered when no metric is defined + expect(screen.queryByTestId('legend')).not.toBeInTheDocument(); + }); +}); + +describe('getPoints utility', () => { + test('extracts points from polygon data', () => { + const data = [ + { + polygon: [ + [0, 0], + [1, 0], + [1, 1], + [0, 1], + ], + }, + { + polygon: [ + [2, 2], + [3, 2], + [3, 3], + [2, 3], + ], + }, + ]; + + const points = getPoints(data); + + expect(points).toHaveLength(8); // 4 points per polygon * 2 polygons + expect(points[0]).toEqual([0, 0]); + expect(points[4]).toEqual([2, 2]); + }); +}); diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx index 912b531b40..630a2639d0 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/Polygon.tsx @@ -335,9 +335,10 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => { const accessor = (d: JsonObject) => d[metricLabel]; const colorSchemeType = formData.color_scheme_type; - const buckets = colorSchemeType - ? getColorBreakpointsBuckets(formData.color_breakpoints) - : getBuckets(formData, payload.data.features, accessor); + const buckets = + colorSchemeType === COLOR_SCHEME_TYPES.color_breakpoints + ? getColorBreakpointsBuckets(formData.color_breakpoints) + : getBuckets(formData, payload.data.features, accessor); return ( <div style={{ position: 'relative' }}>