diff --git a/packages/vrender-components/__tests__/unit/legend-discrete-pager-hug-content.test.ts b/packages/vrender-components/__tests__/unit/legend-discrete-pager-hug-content.test.ts new file mode 100644 index 000000000..2bf688206 --- /dev/null +++ b/packages/vrender-components/__tests__/unit/legend-discrete-pager-hug-content.test.ts @@ -0,0 +1,88 @@ +import type { IGraphic, Stage } from '@visactor/vrender-core'; +import { DiscreteLegend } from '../../src'; +import { createCanvas } from '../util/dom'; +import { createTestStage } from '../util/vrender'; + +describe('DiscreteLegend pager.hugContent', () => { + let stage: Stage; + beforeAll(() => { + createCanvas(document.body, 'main'); + stage = createTestStage('main'); + }); + + afterAll(() => { + stage.release(); + }); + + it('horizontal pager should hug content and shrink clip viewport when pager.hugContent is true', () => { + const items = new Array(6).fill(0).map((_, i) => ({ + label: `long-legend-item-label-${i}`, + shape: { fill: 'red', symbolType: 'circle' } + })); + const createLegend = (pager?: any) => + new DiscreteLegend({ + layout: 'horizontal', + maxWidth: 400, + maxRow: 1, + items, + pager + }); + + const defaultLegend = createLegend(); + stage.defaultLayer.add(defaultLegend as unknown as IGraphic); + stage.render(); + const hugLegend = createLegend({ hugContent: true, space: 10 }); + stage.defaultLayer.add(hugLegend as unknown as IGraphic); + stage.render(); + + // 默认行为不变:翻页器钉在图例可用空间末端 + const defaultPager = (defaultLegend as any)._pagerComponent; + expect(defaultPager.attribute.x).toBeCloseTo(400 - defaultPager.AABBBounds.width(), 4); + + // hugContent:翻页器紧跟内容右缘(间距为 pager.space),分页视口收缩到实际内容宽 + const hugPager = (hugLegend as any)._pagerComponent; + const contentWidth = (hugLegend as any)._itemsContainer.AABBBounds.width(); + expect(contentWidth).toBeLessThan(400 - hugPager.AABBBounds.width() - 10); + expect(hugPager.attribute.x).toBeCloseTo(contentWidth + 10, 4); + expect((hugLegend as any)._itemContext.clipContainer.attribute.width).toBeCloseTo(contentWidth, 4); + + // 整体包围盒随内容收缩 + expect(hugLegend.AABBBounds.width()).toBeLessThan(defaultLegend.AABBBounds.width()); + }); + + it('vertical pager should hug content and shrink clip viewport when pager.hugContent is true', () => { + const items = new Array(8).fill(0).map((_, i) => ({ + label: `item-${i}`, + shape: { fill: 'red', symbolType: 'circle' } + })); + const createLegend = (pager?: any) => + new DiscreteLegend({ + layout: 'vertical', + maxHeight: 100, + maxCol: 1, + items, + pager + }); + + const defaultLegend = createLegend(); + stage.defaultLayer.add(defaultLegend as unknown as IGraphic); + stage.render(); + const hugLegend = createLegend({ hugContent: true, space: 10 }); + stage.defaultLayer.add(hugLegend as unknown as IGraphic); + stage.render(); + + // 默认行为不变:翻页器钉在图例可用空间末端 + const defaultPager = (defaultLegend as any)._pagerComponent; + const defaultMaxHeight = (defaultLegend as any)._contentMaxHeight; + expect(defaultPager.attribute.y).toBeCloseTo(defaultMaxHeight - defaultPager.AABBBounds.height(), 4); + + // hugContent:翻页器紧贴内容下缘,分页视口收缩到实际内容高 + const hugPager = (hugLegend as any)._pagerComponent; + const contentHeight = (hugLegend as any)._itemsContainer.AABBBounds.height(); + expect(hugPager.attribute.y).toBeCloseTo(contentHeight + 10, 4); + expect((hugLegend as any)._itemContext.clipContainer.attribute.height).toBeCloseTo(contentHeight, 4); + + // 整体包围盒随内容收缩 + expect(hugLegend.AABBBounds.height()).toBeLessThan(defaultLegend.AABBBounds.height()); + }); +}); diff --git a/packages/vrender-components/src/legend/discrete/discrete.ts b/packages/vrender-components/src/legend/discrete/discrete.ts index 472473df4..3ee786c8e 100644 --- a/packages/vrender-components/src/legend/discrete/discrete.ts +++ b/packages/vrender-components/src/legend/discrete/discrete.ts @@ -809,6 +809,8 @@ export class DiscreteLegend extends LegendBase { const { pager } = this.attribute; const { totalPage, isHorizontal } = this._itemContext; const position = (pager && (pager as LegendPagerAttributes).position) || 'middle'; + const hugContent = !!(pager && (pager as LegendPagerAttributes).hugContent); + const pagerSpace = (pager && (pager as LegendPagerAttributes).space) ?? DEFAULT_PAGER_SPACE; (this._pagerComponent as Pager).setTotal(totalPage); if (isHorizontal) { @@ -820,8 +822,13 @@ export class DiscreteLegend extends LegendBase { } else { y = renderStartY + compHeight / 2 - this._pagerComponent.AABBBounds.height() / 2; } + let x = compWidth - this._pagerComponent.AABBBounds.width(); + if (hugContent) { + // 分页器紧贴内容右缘,内容占满可用空间时退化为原有的贴右缘布局 + x = Math.max(0, Math.min(this._itemsContainer.AABBBounds.width() + pagerSpace, x)); + } this._pagerComponent.setAttributes({ - x: compWidth - this._pagerComponent.AABBBounds.width(), + x, y }); } else { @@ -833,9 +840,14 @@ export class DiscreteLegend extends LegendBase { } else { x = (compWidth - this._pagerComponent.AABBBounds.width()) / 2; } + let y = compHeight - this._pagerComponent.AABBBounds.height(); + if (hugContent) { + // 分页器紧贴内容下缘,内容占满可用空间时退化为原有的贴下缘布局 + y = Math.max(0, Math.min(renderStartY + this._itemsContainer.AABBBounds.height() + pagerSpace, y)); + } this._pagerComponent.setAttributes({ x, - y: compHeight - this._pagerComponent.AABBBounds.height() + y }); } } @@ -1095,11 +1107,23 @@ export class DiscreteLegend extends LegendBase { } } + // hugContent 时分页裁剪视口收缩到实际内容尺寸:定宽(高)的 clip 分组会把图例包围盒 + // 固定在整个可用空间上,导致「图例项 + 分页器」无法作为整块被外部布局居中 + const hugContent = !!(pager as LegendPagerAttributes).hugContent; + let clipWidth = isHorizontal ? contentWidth : compWidth; + let clipHeight = isHorizontal ? compHeight : contentHeight; + if (hugContent) { + if (isHorizontal) { + clipWidth = Math.max(0, Math.min(clipWidth, itemsContainer.AABBBounds.width())); + } else { + clipHeight = Math.max(0, Math.min(clipHeight, itemsContainer.AABBBounds.height())); + } + } const clipGroup = graphicCreator.group({ x: 0, y: renderStartY, - width: isHorizontal ? contentWidth : compWidth, - height: isHorizontal ? compHeight : contentHeight, + width: clipWidth, + height: clipHeight, clip: true, pickable: false }); diff --git a/packages/vrender-components/src/legend/discrete/type.ts b/packages/vrender-components/src/legend/discrete/type.ts index 4ca0def55..7d48f8bd4 100644 --- a/packages/vrender-components/src/legend/discrete/type.ts +++ b/packages/vrender-components/src/legend/discrete/type.ts @@ -44,6 +44,14 @@ export type LegendPagerAttributes = Omit & * @default 'middle' */ position?: 'start' | 'middle' | 'end'; + /** + * 是否让分页器紧贴图例内容排布。默认 false,分页器固定在图例可用空间的末端, + * 图例项与分页器呈两端对齐;开启后分页器紧跟最后一个图例项(间距为 `space`), + * 同时分页裁剪视口收缩到实际内容尺寸,使「图例项 + 分页器」的包围盒随内容 + * 自适应,便于外部布局把整块内容居中放置。 + * @default false + */ + hugContent?: boolean; }; /**