From 5707f010a51b18d27471a09d338db1a6b8606d32 Mon Sep 17 00:00:00 2001 From: tiwarir Date: Tue, 7 Jul 2026 14:34:51 -0400 Subject: [PATCH] fix(legend): respect scroll legend width. close #12562 --- src/component/legend/ScrollableLegendView.ts | 15 ++- .../component/legend/scrollableLegend.test.ts | 105 ++++++++++++++++++ 2 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 test/ut/spec/component/legend/scrollableLegend.test.ts diff --git a/src/component/legend/ScrollableLegendView.ts b/src/component/legend/ScrollableLegendView.ts index da19a699d1..f8b16e7524 100644 --- a/src/component/legend/ScrollableLegendView.ts +++ b/src/component/legend/ScrollableLegendView.ts @@ -263,6 +263,9 @@ class ScrollableLegendView extends LegendView { const contentRect = contentGroup.getBoundingRect(); const controllerRect = controllerGroup.getBoundingRect(); const showController = this._showController = contentRect[wh] > maxSize[wh]; + const contentSize = Math.max(contentRect[hw], controllerRect[hw]); + const mainSize = Math.min(contentSize, maxSize[hw]); + const crossOverflow = contentSize > maxSize[hw]; // In case that the inner elements of contentGroup layout do not based on [0, 0] const contentPos = [-contentRect.x, -contentRect.y]; @@ -293,8 +296,8 @@ class ScrollableLegendView extends LegendView { } } - // Always align controller to content as 'middle'. - controllerPos[1 - orientIdx] += contentRect[hw] / 2 - controllerRect[hw] / 2; + // Always align controller to visible content as 'middle'. + controllerPos[1 - orientIdx] += mainSize / 2 - controllerRect[hw] / 2; contentGroup.setPosition(contentPos); containerGroup.setPosition(containerPos); @@ -307,15 +310,17 @@ class ScrollableLegendView extends LegendView { // Consider content may be overflow (should be clipped). mainRect[wh] = showController ? maxSize[wh] : contentRect[wh]; - mainRect[hw] = Math.max(contentRect[hw], controllerRect[hw]); + mainRect[hw] = mainSize; // `containerRect[yx] + containerPos[1 - orientIdx]` is 0. mainRect[yx] = Math.min(0, controllerRect[yx] + controllerPos[1 - orientIdx]); containerGroup.__rectSize = maxSize[wh]; - if (showController) { + if (showController || crossOverflow) { const clipShape = {x: 0, y: 0} as graphic.Rect['shape']; - clipShape[wh] = Math.max(maxSize[wh] - controllerRect[wh] - pageButtonGap, 0); + clipShape[wh] = showController + ? Math.max(maxSize[wh] - controllerRect[wh] - pageButtonGap, 0) + : mainRect[wh]; clipShape[hw] = mainRect[hw]; containerGroup.setClipPath(new graphic.Rect({shape: clipShape})); // Consider content may be larger than container, container rect diff --git a/test/ut/spec/component/legend/scrollableLegend.test.ts b/test/ut/spec/component/legend/scrollableLegend.test.ts new file mode 100644 index 0000000000..a966328df5 --- /dev/null +++ b/test/ut/spec/component/legend/scrollableLegend.test.ts @@ -0,0 +1,105 @@ +/* +* 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 { createChart, getViewGroup } from '../../../core/utHelper'; +import { EChartsType } from '../../../../../src/echarts'; +import { EChartsOption } from '../../../../../src/export/option'; +import Group from 'zrender/src/graphic/Group'; +import Rect from 'zrender/src/graphic/shape/Rect'; + +function findBackgroundRect(chart: EChartsType): Rect { + const legendGroup = getViewGroup(chart, 'legend'); + let backgroundRect: Rect; + + legendGroup.eachChild(function (child) { + const rect = child as Rect & { z2: number }; + if (rect.type === 'rect' && rect.z2 === -1) { + backgroundRect = rect; + } + }); + + return backgroundRect; +} + +function findClipRect(chart: EChartsType): Rect { + const legendGroup = getViewGroup(chart, 'legend'); + let clipRect: Rect; + + legendGroup.traverse(function (child) { + if (child.type === 'group') { + clipRect = ((child as Group).getClipPath() as Rect) || clipRect; + } + }); + + return clipRect; +} + +describe('legend/scrollableLegend', function () { + let chart: EChartsType; + + beforeEach(function () { + chart = createChart({ + width: 500, + height: 400 + }); + }); + + afterEach(function () { + chart.dispose(); + }); + + it('should respect width as max width for vertical scroll legend', function () { + const legendWidth = 100; + const padding = 10; + const longName = 'Long legend item name '.repeat(20); + const option: EChartsOption = { + legend: { + type: 'scroll', + orient: 'vertical', + right: 10, + top: 10, + bottom: 20, + width: legendWidth, + padding, + backgroundColor: '#212121' + }, + series: [ + { + type: 'pie', + data: [ + {value: 10, name: longName + '0'}, + {value: 20, name: longName + '1'}, + {value: 30, name: longName + '2'}, + {value: 40, name: longName + '3'} + ] + } + ] + }; + + chart.setOption(option); + + const backgroundRect = findBackgroundRect(chart); + const clipRect = findClipRect(chart); + + expect(backgroundRect).toBeDefined(); + expect(backgroundRect.shape.width).toBeLessThanOrEqual(legendWidth + padding * 2); + expect(clipRect).toBeDefined(); + expect(clipRect.shape.width).toBeLessThanOrEqual(legendWidth); + }); +});