diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts index 016a18bbe02e..88631b48686a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts @@ -60,11 +60,9 @@ const DEFAULT_LEGEND_ICON_WIDTH = 25; const LEGEND_ICON_LABEL_GAP = 5; const LEGEND_HORIZONTAL_SIDE_GUTTER = 16; const LEGEND_HORIZONTAL_ROW_HEIGHT = 24; -const LEGEND_HORIZONTAL_MAX_ROWS = 2; -const LEGEND_HORIZONTAL_MAX_HEIGHT_RATIO = 0.25; +// Cap the reserved horizontal legend margin so an overflowing legend can't eat the plot. +const MAX_LEGEND_MARGIN_RATIO = 0.4; const LEGEND_VERTICAL_SIDE_GUTTER = 16; -const LEGEND_VERTICAL_ROW_HEIGHT = 24; -const LEGEND_VERTICAL_MAX_WIDTH_RATIO = 0.4; const LEGEND_SELECTOR_GAP = 10; const LEGEND_MARGIN_GUTTER = 45; // ECharts does not expose pre-render measurements for plain legends, so these @@ -84,10 +82,6 @@ export type LegendLayoutResult = { effectiveType: LegendType; }; -const SCROLL_LEGEND_LAYOUT: LegendLayoutResult = { - effectiveType: LegendType.Scroll, -}; - function getLegendLabel(item: LegendDataItem): string { if (typeof item === 'string' || typeof item === 'number') { return String(item); @@ -269,37 +263,30 @@ function getHorizontalPlainLegendLayout({ showSelectors, theme, ); + const rowsForMargin = Number.isFinite(rowCount) + ? rowCount + : legendLabels.length; const requiredMargin = defaultLegendPadding[orientation] + - Math.max(0, rowCount - 1) * LEGEND_HORIZONTAL_ROW_HEIGHT; - const maxLegendHeight = + Math.max(0, rowsForMargin - 1) * LEGEND_HORIZONTAL_ROW_HEIGHT; + const boundedMargin = availableHeight > 0 - ? availableHeight * LEGEND_HORIZONTAL_MAX_HEIGHT_RATIO - : Infinity; - - if ( - !Number.isFinite(rowCount) || - rowCount > LEGEND_HORIZONTAL_MAX_ROWS || - requiredMargin > maxLegendHeight - ) { - return SCROLL_LEGEND_LAYOUT; - } + ? Math.min(requiredMargin, availableHeight * MAX_LEGEND_MARGIN_RATIO) + : requiredMargin; return { - effectiveMargin: Math.max(currentMargin, requiredMargin), + effectiveMargin: Math.max(currentMargin, boundedMargin), effectiveType: LegendType.Plain, }; } function getVerticalPlainLegendLayout({ - availableHeight, availableWidth, currentMargin, legendLabels, showSelectors, theme, }: { - availableHeight: number; availableWidth: number; currentMargin: number; legendLabels: string[]; @@ -313,17 +300,6 @@ function getVerticalPlainLegendLayout({ }; } - const selectorHeight = showSelectors - ? LEGEND_VERTICAL_ROW_HEIGHT + LEGEND_SELECTOR_GAP - : 0; - const effectiveAvailableHeight = Math.max( - availableHeight - LEGEND_VERTICAL_SIDE_GUTTER - selectorHeight, - 0, - ); - const rowsPerColumn = Math.floor( - (effectiveAvailableHeight + DEFAULT_LEGEND_ITEM_GAP) / - (LEGEND_VERTICAL_ROW_HEIGHT + DEFAULT_LEGEND_ITEM_GAP), - ); const requiredSelectorMargin = showSelectors ? ESTIMATED_LEGEND_SELECTOR_WIDTH + LEGEND_VERTICAL_SIDE_GUTTER : 0; @@ -333,21 +309,13 @@ function getVerticalPlainLegendLayout({ requiredSelectorMargin, ), ); - const maxLegendWidth = + const boundedMargin = availableWidth > 0 - ? availableWidth * LEGEND_VERTICAL_MAX_WIDTH_RATIO - : Infinity; - - if ( - rowsPerColumn <= 0 || - legendLabels.length > rowsPerColumn || - requiredMargin > maxLegendWidth - ) { - return SCROLL_LEGEND_LAYOUT; - } + ? Math.min(requiredMargin, availableWidth * MAX_LEGEND_MARGIN_RATIO) + : requiredMargin; return { - effectiveMargin: Math.max(currentMargin, requiredMargin), + effectiveMargin: Math.max(currentMargin, boundedMargin), effectiveType: LegendType.Plain, }; } @@ -404,7 +372,6 @@ export function getLegendLayoutResult({ } return getVerticalPlainLegendLayout({ - availableHeight: resolvedAvailableHeight, availableWidth: resolvedAvailableWidth, currentMargin: resolvedLegendMargin, legendLabels, diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Gantt/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Gantt/transformProps.test.ts index f260959e4d2d..acb7d34e2ae6 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Gantt/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Gantt/transformProps.test.ts @@ -18,6 +18,7 @@ */ import { AxisType, ChartProps } from '@superset-ui/core'; import { supersetTheme } from '@apache-superset/core/theme'; +import type { LegendComponentOption } from 'echarts/components'; import { LegendOrientation, LegendType, @@ -304,7 +305,7 @@ describe('legend sorting', () => { expect(legendData).toEqual(['series value 2', 'series value 1']); }); - test('falls back to scroll for plain legends with an overlong legend item', () => { + test('honors an explicit List selection for plain legends with an overlong legend item', () => { const props = new ChartProps({ ...chartPropsConfig, width: 320, @@ -321,7 +322,7 @@ describe('legend sorting', () => { 'Y Axis': 'first', tooltip_column: 'tooltip value 1', series: - 'This is a ridiculously long legend label that should switch to scroll', + 'This is a ridiculously long legend label that stays a plain List', }, { startTime: Date.UTC(2025, 1, 1, 18, 0, 0), @@ -344,7 +345,9 @@ describe('legend sorting', () => { const result = transformProps(props as EchartsGanttChartProps); - expect((result.echartOptions.legend as any).type).toBe(LegendType.Scroll); + expect((result.echartOptions.legend as LegendComponentOption).type).toBe( + LegendType.Plain, + ); }); test('keeps legend visibility driven by showLegend for single-series charts', () => { diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts index 3174f4a1639d..3d18c755237c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts @@ -141,7 +141,7 @@ describe('Pie transformProps', () => { ]); }); - test('falls back to scroll for plain legends with overlong labels', () => { + test('honors an explicit List selection for plain legends with overlong labels', () => { const longLegendChartProps = new ChartProps({ formData: { colorScheme: 'bnbColors', @@ -182,7 +182,7 @@ describe('Pie transformProps', () => { ); expect((transformed.echartOptions.legend as any).type).toBe( - LegendType.Scroll, + LegendType.Plain, ); }); }); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/transformProps.test.ts index 80e434294c13..cacb0b46a567 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/transformProps.test.ts @@ -852,7 +852,7 @@ describe('Bar Chart X-axis Time Formatting', () => { type: LegendType.Plain, }); - test('should fall back to scroll for horizontal bottom legends after margin expansion reduces available width', () => { + test('honors an explicit List selection for horizontal bottom legends and reserves margin', () => { const legendLabels = [ 'This is a long sales legend', 'This is a long marketing legend', @@ -899,48 +899,9 @@ describe('Bar Chart X-axis Time Formatting', () => { legendType: LegendType.Plain, showLegend: true, }; - const baselineChartProps = createEchartsTimeseriesTestChartProps< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps - >({ - defaultFormData: regressionFormData, - defaultVizType: 'echarts_timeseries_bar', - defaultQueriesData: longLegendData, - width: baseChartPropsConfig.width, - height: baseChartPropsConfig.height, - }); - const baselineTransformed = transformProps(baselineChartProps); - const legendItems = ( - (baselineTransformed.echartOptions.legend as LegendComponentOption) - .data as Array - ).map(item => (typeof item === 'string' ? item : item.name)); - let chartWidth: number | undefined; - let expandedLegendMargin: number | null = null; - - for (let width = 300; width <= 700; width += 1) { - const initialLayout = getBottomLegendLayout(width, legendItems, null); - - if (initialLayout.effectiveType !== LegendType.Plain) { - continue; - } - - const refinedLayout = getBottomLegendLayout( - width, - legendItems, - initialLayout.effectiveMargin ?? null, - ); - - if (refinedLayout.effectiveType === LegendType.Scroll) { - chartWidth = width; - expandedLegendMargin = initialLayout.effectiveMargin ?? null; - break; - } - } - - expect(chartWidth).toBeDefined(); - expect(expandedLegendMargin).not.toBeNull(); - const resolvedChartWidth = chartWidth ?? baseChartPropsConfig.width; - + // A narrow chart forces the long-label bottom legend to wrap onto + // multiple rows — the case that previously flipped List to scroll. + const chartWidth = 320; const chartProps = createEchartsTimeseriesTestChartProps< EchartsTimeseriesFormData, EchartsTimeseriesChartProps @@ -948,7 +909,7 @@ describe('Bar Chart X-axis Time Formatting', () => { defaultFormData: regressionFormData, defaultVizType: 'echarts_timeseries_bar', defaultQueriesData: longLegendData, - width: resolvedChartWidth, + width: chartWidth, height: baseChartPropsConfig.height, }); @@ -956,7 +917,12 @@ describe('Bar Chart X-axis Time Formatting', () => { const legend = transformedProps.echartOptions .legend as LegendComponentOption; const grid = transformedProps.echartOptions.grid as GridComponentOption; - const expectedPadding = getPadding( + const legendItems = (legend.data as Array).map( + item => (typeof item === 'string' ? item : item.name), + ); + + const layout = getBottomLegendLayout(chartWidth, legendItems, null); + const basePadding = getPadding( true, LegendOrientation.Bottom, false, @@ -968,30 +934,41 @@ describe('Bar Chart X-axis Time Formatting', () => { undefined, true, ); - [expectedPadding.bottom, expectedPadding.left] = [ - expectedPadding.left, - expectedPadding.bottom, + [basePadding.bottom, basePadding.left] = [ + basePadding.left, + basePadding.bottom, ]; - const expandedPadding = getPadding( + + // The explicit List selection is honored end-to-end (never flips). + expect(legend.type).toBe(LegendType.Plain); + expect(layout.effectiveType).toBe(LegendType.Plain); + + // #38675's margin reservation is retained: the wrapped rows reserve a + // finite margin beyond the single-row baseline, so the grid shrinks to + // reduce clipping instead of the legend flipping to scroll. + expect(Number.isFinite(layout.effectiveMargin)).toBe(true); + + const reservedPadding = getPadding( true, LegendOrientation.Bottom, false, false, - expandedLegendMargin, + layout.effectiveMargin, false, undefined, undefined, undefined, true, ); - [expandedPadding.bottom, expandedPadding.left] = [ - expandedPadding.left, - expandedPadding.bottom, + [reservedPadding.bottom, reservedPadding.left] = [ + reservedPadding.left, + reservedPadding.bottom, ]; - expect(legend.type).toBe(LegendType.Scroll); - expect(grid.bottom).toBe(expectedPadding.bottom); - expect(grid.bottom).not.toBe(expandedPadding.bottom); + expect(grid.bottom).toBe(reservedPadding.bottom); + expect(grid.bottom as number).toBeGreaterThan( + basePadding.bottom as number, + ); }); }); }); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts index 9519122760ce..012f81f64a06 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts @@ -916,40 +916,38 @@ describe('legend sorting', () => { 'Boston', ]); }); +}); - test('falls back to scroll for zoomable top legends when toolbox space reduces available width', () => { - const narrowLegendData = [ - createTestQueryData( - createTestData( - [ - { - Alpha: 1, - Beta: 2, - Gamma: 3, - }, - ], - { intervalMs: 300000000 }, - ), +test('honors an explicit List selection for zoomable top legends even when toolbox space reduces available width', () => { + const narrowLegendData = [ + createTestQueryData( + createTestData( + [ + { + Alpha: 1, + Beta: 2, + Gamma: 3, + }, + ], + { intervalMs: 300000000 }, ), - ]; - const chartProps = createTestChartProps({ - width: 190 + TIMESERIES_CONSTANTS.legendTopRightOffset, - formData: { - ...formData, - legendType: LegendType.Plain, - legendOrientation: LegendOrientation.Top, - showLegend: true, - zoomable: true, - }, - queriesData: narrowLegendData, - }); + ), + ]; + const chartProps = createTestChartProps({ + width: 190 + TIMESERIES_CONSTANTS.legendTopRightOffset, + formData: { + ...formData, + legendType: LegendType.Plain, + legendOrientation: LegendOrientation.Top, + showLegend: true, + zoomable: true, + }, + queriesData: narrowLegendData, + }); - const transformed = transformProps(chartProps); + const transformed = transformProps(chartProps); - expect((transformed.echartOptions.legend as any).type).toBe( - LegendType.Scroll, - ); - }); + expect((transformed.echartOptions.legend as any).type).toBe(LegendType.Plain); }); test('honors user-selected plain legend type for top orientation when space allows (#39540)', () => { diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts index ce67af42e178..13f36dddb531 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts @@ -1018,9 +1018,8 @@ test('getLegendLayoutResult honors user-selected plain type for many horizontal }); expect(layout.effectiveType).toBe(LegendType.Plain); - expect(layout.effectiveMargin).toBeGreaterThanOrEqual( - defaultLegendPadding[LegendOrientation.Top], - ); + // One row of items fits, so only the base top padding is reserved. + expect(layout.effectiveMargin).toBe(20); }); test('getLegendLayoutResult keeps user-selected plain type for bottom-oriented legends when space allows', () => { @@ -1082,47 +1081,85 @@ test('getLegendLayoutResult adds extra margin for wrapped plain horizontal legen ); }); -test('getLegendLayoutResult falls back to scroll when horizontal plain legends exceed two rows', () => { - expect( - getLegendLayoutResult({ - chartHeight: 400, - chartWidth: 240, - legendItems: [ - 'This is a long legend label', - 'Another long legend label', - 'Third long legend label', - ], - legendMargin: null, - orientation: LegendOrientation.Top, - show: true, - theme, - type: LegendType.Plain, - }), - ).toEqual({ - effectiveType: LegendType.Scroll, +test('getLegendLayoutResult keeps plain when horizontal plain legends exceed two rows', () => { + const layout = getLegendLayoutResult({ + chartHeight: 400, + chartWidth: 240, + legendItems: [ + 'This is a long legend label', + 'Another long legend label', + 'Third long legend label', + ], + legendMargin: null, + orientation: LegendOrientation.Top, + show: true, + theme, + type: LegendType.Plain, }); + + expect(layout.effectiveType).toBe(LegendType.Plain); + // Each label is wider than the available width, so the row estimate + // overflows and falls back to one row per label: 20 + 2 * 24. + expect(layout.effectiveMargin).toBe(68); }); -test('getLegendLayoutResult falls back to scroll when a single horizontal plain legend item exceeds available width', () => { - expect( - getLegendLayoutResult({ - chartHeight: 400, - chartWidth: 260, - legendItems: [ - 'This is a ridiculously long legend label that should not fit on one line', - ], - legendMargin: null, - orientation: LegendOrientation.Top, - show: true, - theme, - type: LegendType.Plain, - }), - ).toEqual({ - effectiveType: LegendType.Scroll, +test('getLegendLayoutResult bounds reserved margin for overflowing horizontal legends so the plot is not collapsed', () => { + const chartHeight = 200; + const layout = getLegendLayoutResult({ + chartHeight, + chartWidth: 100, + legendItems: Array.from({ length: 100 }, (_, index) => `Series ${index}`), + legendMargin: null, + orientation: LegendOrientation.Top, + show: true, + theme, + type: LegendType.Plain, + }); + + expect(layout.effectiveType).toBe(LegendType.Plain); + expect(Number.isFinite(layout.effectiveMargin)).toBe(true); + // 40% of the 200px chart height. + expect(layout.effectiveMargin).toBe(80); +}); + +test('getLegendLayoutResult bounds reserved margin for long vertical legend labels so the plot is not collapsed', () => { + const chartWidth = 1000; + const layout = getLegendLayoutResult({ + chartHeight: 400, + chartWidth, + legendItems: ['A'.repeat(200)], + legendMargin: null, + orientation: LegendOrientation.Left, + show: true, + theme, + type: LegendType.Plain, }); + + expect(layout.effectiveType).toBe(LegendType.Plain); + // 40% of the 1000px chart width. + expect(layout.effectiveMargin).toBe(400); +}); + +test('getLegendLayoutResult keeps plain when a single horizontal plain legend item exceeds available width', () => { + const layout = getLegendLayoutResult({ + chartHeight: 400, + chartWidth: 260, + legendItems: [ + 'This is a ridiculously long legend label that should not fit on one line', + ], + legendMargin: null, + orientation: LegendOrientation.Top, + show: true, + theme, + type: LegendType.Plain, + }); + + expect(layout.effectiveType).toBe(LegendType.Plain); + // Overflow fallback with a single label reserves no extra row: 20 + 0 * 24. + expect(layout.effectiveMargin).toBe(20); }); -test('getLegendLayoutResult falls back to scroll when reserved horizontal width reduces plain legend capacity', () => { +test('getLegendLayoutResult keeps plain when reserved horizontal width reduces plain legend capacity', () => { const availableWidth = getHorizontalLegendAvailableWidth({ chartWidth: 265, orientation: LegendOrientation.Top, @@ -1130,38 +1167,38 @@ test('getLegendLayoutResult falls back to scroll when reserved horizontal width zoomable: true, }); - expect( - getLegendLayoutResult({ - availableWidth, - chartHeight: 400, - chartWidth: 265, - legendItems: ['Alpha', 'Beta', 'Gamma'], - legendMargin: null, - orientation: LegendOrientation.Top, - show: true, - theme, - type: LegendType.Plain, - }), - ).toEqual({ - effectiveType: LegendType.Scroll, + const layout = getLegendLayoutResult({ + availableWidth, + chartHeight: 400, + chartWidth: 265, + legendItems: ['Alpha', 'Beta', 'Gamma'], + legendMargin: null, + orientation: LegendOrientation.Top, + show: true, + theme, + type: LegendType.Plain, }); + + expect(layout.effectiveType).toBe(LegendType.Plain); + // 190px of available width wraps the three items onto three rows: 20 + 2 * 24. + expect(layout.effectiveMargin).toBe(68); }); -test('getLegendLayoutResult falls back to scroll when horizontal legend selectors alone exceed available width', () => { - expect( - getLegendLayoutResult({ - chartHeight: 400, - chartWidth: 95, - legendItems: ['A'], - legendMargin: null, - orientation: LegendOrientation.Top, - show: true, - theme, - type: LegendType.Plain, - }), - ).toEqual({ - effectiveType: LegendType.Scroll, +test('getLegendLayoutResult keeps plain when horizontal legend selectors alone exceed available width', () => { + const layout = getLegendLayoutResult({ + chartHeight: 400, + chartWidth: 95, + legendItems: ['A'], + legendMargin: null, + orientation: LegendOrientation.Top, + show: true, + theme, + type: LegendType.Plain, }); + + expect(layout.effectiveType).toBe(LegendType.Plain); + // The selector alone overflows, so the fallback reserves one row: 20 + 0 * 24. + expect(layout.effectiveMargin).toBe(20); }); test('getLegendLayoutResult keeps plain vertical legends when they fit within a single column', () => { @@ -1202,56 +1239,87 @@ test('getLegendLayoutResult adds extra margin for wide vertical plain legends', ); }); -test('getLegendLayoutResult falls back to scroll when vertical plain legends exceed one column', () => { - expect( - getLegendLayoutResult({ - chartHeight: 160, - chartWidth: 800, - legendItems: ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon'], - legendMargin: null, - orientation: LegendOrientation.Left, - show: true, - theme, - type: LegendType.Plain, - }), - ).toEqual({ - effectiveType: LegendType.Scroll, +test('getLegendLayoutResult keeps plain when vertical plain legends exceed one column', () => { + const layout = getLegendLayoutResult({ + chartHeight: 160, + chartWidth: 800, + legendItems: ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon'], + legendMargin: null, + orientation: LegendOrientation.Left, + show: true, + theme, + type: LegendType.Plain, }); + + expect(layout.effectiveType).toBe(LegendType.Plain); + // The widest label needs 128px, under the 170px base left padding, so the + // base padding wins. + expect(layout.effectiveMargin).toBe(170); }); -test('getLegendLayoutResult falls back to scroll when vertical plain legend selectors exceed available width', () => { - expect( - getLegendLayoutResult({ - chartHeight: 400, - chartWidth: 300, - legendItems: ['A', 'B', 'C'], - legendMargin: null, - orientation: LegendOrientation.Left, - show: true, - theme, - type: LegendType.Plain, - }), - ).toEqual({ - effectiveType: LegendType.Scroll, +test('getLegendLayoutResult keeps plain when vertical plain legend selectors exceed available width', () => { + const layout = getLegendLayoutResult({ + chartHeight: 400, + chartWidth: 300, + legendItems: ['A', 'B', 'C'], + legendMargin: null, + orientation: LegendOrientation.Left, + show: true, + theme, + type: LegendType.Plain, + }); + + expect(layout.effectiveType).toBe(LegendType.Plain); + // The 128px selector requirement is clamped to 120px (40% of the 300px + // width) and both stay under the 170px base left padding. + expect(layout.effectiveMargin).toBe(170); +}); + +test('getLegendLayoutResult honors an explicit List selection with many series', () => { + const manyItems = Array.from({ length: 40 }, (_, i) => `Series ${i + 1}`); + + const horizontal = getLegendLayoutResult({ + chartHeight: 400, + chartWidth: 800, + legendItems: manyItems, + legendMargin: null, + orientation: LegendOrientation.Top, + show: true, + theme, + type: LegendType.Plain, + }); + expect(horizontal.effectiveType).toBe(LegendType.Plain); + + const vertical = getLegendLayoutResult({ + chartHeight: 160, + chartWidth: 800, + legendItems: manyItems, + legendMargin: null, + orientation: LegendOrientation.Left, + show: true, + theme, + type: LegendType.Plain, }); + expect(vertical.effectiveType).toBe(LegendType.Plain); }); test('getLegendLayoutResult counts empty-string legend labels when estimating layout', () => { - expect( - getLegendLayoutResult({ - chartHeight: 400, - chartWidth: 116, - legendItems: ['', 'A', 'B', 'C', 'D'], - legendMargin: null, - orientation: LegendOrientation.Top, - show: true, - showSelectors: false, - theme, - type: LegendType.Plain, - }), - ).toEqual({ - effectiveType: LegendType.Scroll, + const layout = getLegendLayoutResult({ + chartHeight: 400, + chartWidth: 116, + legendItems: ['', 'A', 'B', 'C', 'D'], + legendMargin: null, + orientation: LegendOrientation.Top, + show: true, + showSelectors: false, + theme, + type: LegendType.Plain, }); + + expect(layout.effectiveType).toBe(LegendType.Plain); + // The empty label still occupies a slot, wrapping five items onto three + // rows: 20 + 2 * 24. + expect(layout.effectiveMargin).toBe(68); }); test('resolveLegendLayout returns both raw and effective legend layout values', () => {