You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2026/08/18 23:16:13 UTC

(superset) 02/03: fix(echarts): account for non-legend chart padding

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

sadpandajoe pushed a commit to branch fix-plain-legend-overlap-timeseries
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 07717dc89ef77258cddd20662be4a102883bb142
Author: sadpandajoe <jc...@gmail.com>
AuthorDate: Tue Aug 18 22:47:31 2026 +0000

    fix(echarts): account for non-legend chart padding
---
 RCA.md                                             | 13 +++++--
 .../src/Timeseries/transformProps.ts               | 16 +++++++++
 .../plugin-chart-echarts/src/utils/series.ts       | 12 +++----
 .../test/Timeseries/transformProps.test.ts         | 40 ++++++++++++++++++++++
 4 files changed, 72 insertions(+), 9 deletions(-)

diff --git a/RCA.md b/RCA.md
index 2f08d431c46..b93969d4257 100644
--- a/RCA.md
+++ b/RCA.md
@@ -39,8 +39,14 @@
 - **Verified:**
   `superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts` replaces
   the horizontal 40% cap with an 80px minimum plot-space floor. It reserves the
-  full estimated legend margin whenever that leaves at least 80px for the plot;
-  for larger legends, it reserves all available height except that floor.
+  full estimated legend margin whenever that leaves at least 80px of genuinely
+  available height for the plot; for larger legends, it reserves the available
+  height except that floor.
+- **Verified:**
+  `superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts`
+  calculates the top and bottom padding required without a legend, including
+  zoom controls and axis-title offsets, and excludes those fixed reservations
+  before applying the plot-space floor.
 - **Verified:** The horizontal path continues to return `LegendType.Plain`; no
   Plain-to-Scroll fallback was added.
 - **Verified:**
@@ -48,6 +54,9 @@
   a 40-item, 800-by-400 regression case whose estimated eleven rows require a
   260px margin, and updates the small-chart overflow expectation to preserve an
   80px plot area.
+- **Verified:** A transform-level regression test covers a 200px-tall zoomable
+  chart with a many-item top Plain legend and verifies that the resulting grid
+  retains 80px of plot height after both top and bottom padding.
 - **Verified:** Vertical Plain legend layout is unchanged.
 
 ## Latent Bugs Found
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index 43e9a2f431b..e8daba62ae0 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -1122,6 +1122,18 @@ export default function transformProps(
     name,
     icon: 'roundRect',
   }));
+  const nonLegendPadding = getPadding(
+    false,
+    legendOrientation,
+    addYAxisLabelOffset,
+    zoomable,
+    undefined,
+    addXAxisLabelOffset,
+    yAxisTitlePosition,
+    convertInteger(yAxisTitleMargin),
+    convertInteger(xAxisTitleMargin),
+    isHorizontal,
+  );
   const getLegendLayout = (candidateLegendMargin?: string | number | null) => {
     const padding = getPadding(
       showLegend,
@@ -1137,6 +1149,10 @@ export default function transformProps(
     );
 
     return resolveLegendLayout({
+      availableHeight: Math.max(
+        height - nonLegendPadding.top - nonLegendPadding.bottom,
+        0,
+      ),
       availableWidth:
         legendOrientation === LegendOrientation.Top ||
         legendOrientation === LegendOrientation.Bottom
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 8e90cf1eca3..cae2e11535a 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -60,6 +60,7 @@ const DEFAULT_LEGEND_ICON_WIDTH = 25;
 const LEGEND_ICON_LABEL_GAP = 5;
 const LEGEND_HORIZONTAL_SIDE_GUTTER = 16;
 const LEGEND_HORIZONTAL_ROW_HEIGHT = 24;
+// Smallest plot height that still preserves the chart's visible structure.
 const MIN_HORIZONTAL_LEGEND_PLOT_SPACE = 80;
 const MAX_LEGEND_MARGIN_RATIO = 0.4;
 const LEGEND_VERTICAL_SIDE_GUTTER = 16;
@@ -269,13 +270,10 @@ function getHorizontalPlainLegendLayout({
   const requiredMargin =
     defaultLegendPadding[orientation] +
     Math.max(0, rowsForMargin - 1) * LEGEND_HORIZONTAL_ROW_HEIGHT;
-  const boundedMargin =
-    availableHeight > 0
-      ? Math.min(
-          requiredMargin,
-          Math.max(availableHeight - MIN_HORIZONTAL_LEGEND_PLOT_SPACE, 0),
-        )
-      : requiredMargin;
+  const boundedMargin = Math.min(
+    requiredMargin,
+    Math.max(availableHeight - MIN_HORIZONTAL_LEGEND_PLOT_SPACE, 0),
+  );
 
   return {
     effectiveMargin: Math.max(currentMargin, boundedMargin),
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 ff44aa7d3b6..7968bc580c6 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
@@ -37,6 +37,7 @@ import {
 import { GenericDataType } from '@apache-superset/core/common';
 import { supersetTheme } from '@apache-superset/core/theme';
 import type { SeriesOption } from 'echarts';
+import type { GridComponentOption } from 'echarts/components';
 import transformProps from '../../src/Timeseries/transformProps';
 import {
   EchartsTimeseriesSeriesType,
@@ -1035,6 +1036,45 @@ test('honors an explicit List selection for zoomable top legends even when toolb
   expect((transformed.echartOptions.legend as any).type).toBe(LegendType.Plain);
 });
 
+test('preserves plot space for a small zoomable chart with a many-item Plain legend', () => {
+  const chartHeight = 200;
+  const manyLegendValues = Object.fromEntries(
+    Array.from({ length: 40 }, (_, index) => [
+      `Country ${index + 1}, Product`,
+      index + 1,
+    ]),
+  );
+  const chartProps = createTestChartProps({
+    height: chartHeight,
+    formData: {
+      ...formData,
+      legendType: LegendType.Plain,
+      legendOrientation: LegendOrientation.Top,
+      showLegend: true,
+      zoomable: true,
+      yAxisTitleMargin: 0,
+      yAxisTitlePosition: 'Left',
+    },
+    queriesData: [
+      createTestQueryData(
+        createTestData([manyLegendValues], { intervalMs: 300000000 }),
+      ),
+    ],
+  });
+
+  const transformed = transformProps(chartProps);
+  const grid = transformed.echartOptions.grid as GridComponentOption;
+
+  expect((transformed.echartOptions.legend as { type?: LegendType }).type).toBe(
+    LegendType.Plain,
+  );
+  expect(typeof grid.top).toBe('number');
+  expect(typeof grid.bottom).toBe('number');
+  expect(
+    chartHeight - (Number(grid.top) + Number(grid.bottom)),
+  ).toBeGreaterThanOrEqual(80);
+});
+
 test('honors user-selected plain legend type for top orientation when space allows (#39540)', () => {
   // Regression test for issue #39540: switching the legend type control from
   // scroll to plain must reach the rendered ECharts config. Horizontal legends