diff --git a/common/changes/@visactor/vchart/fix-title-layout-number-size_2026-09-09.json b/common/changes/@visactor/vchart/fix-title-layout-number-size_2026-09-09.json new file mode 100644 index 0000000000..ed15f1e07c --- /dev/null +++ b/common/changes/@visactor/vchart/fix-title-layout-number-size_2026-09-09.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "fix: resolve title width/height/minWidth/maxWidth/minHeight/maxHeight as ILayoutNumber against the chart view rect", + "type": "patch" + } + ], + "packageName": "@visactor/vchart", + "email": "chendaxin.tk@bytedance.com" +} diff --git a/docs/assets/option/en/component/title.md b/docs/assets/option/en/component/title.md index e767ebb56b..93ca17e5f9 100644 --- a/docs/assets/option/en/component/title.md +++ b/docs/assets/option/en/component/title.md @@ -4,6 +4,8 @@ Chart title configuration. +Starting from version 2.1.7, `width`, `height`, `minWidth`, `maxWidth`, `minHeight` and `maxHeight` support `ILayoutNumber`. Earlier versions only support numeric values. + ### visible(boolean) = true Whether to display the title. @@ -47,21 +49,29 @@ Optional values: - 'right' - 'bottom' -### minWidth(number) +### minWidth(ILayoutNumber) + +Title's minimum width. + +{{ use: common-layout-number }} + +### maxWidth(ILayoutNumber) + +Title's maximum width. When the text exceeds the maximum width, it will automatically be truncated. -Title's minimum width, in pixels. +{{ use: common-layout-number }} -### maxWidth(number) +### minHeight(ILayoutNumber) -Title's maximum width, in pixels. When the text exceeds the maximum width, it will automatically be truncated. +Title's minimum height. -### minHeight(number) +{{ use: common-layout-number }} -Title's minimum height, in pixels. +### maxHeight(ILayoutNumber) -### maxHeight(number) +Title's maximum height. When the text exceeds the maximum height, it will automatically be truncated. -Title's maximum height, in pixels. +{{ use: common-layout-number }} ### innerPadding(Object|number) = 0 diff --git a/docs/assets/option/zh/component/title.md b/docs/assets/option/zh/component/title.md index 6b5f70e63a..d1510f10f1 100644 --- a/docs/assets/option/zh/component/title.md +++ b/docs/assets/option/zh/component/title.md @@ -4,6 +4,8 @@ 图表标题配置。 +从 2.1.7 版本开始,`width`、`height`、`minWidth`、`maxWidth`、`minHeight` 和 `maxHeight` 支持 `ILayoutNumber`;此前版本仅支持数值。 + ### visible(boolean) = true 是否显示标题。 @@ -47,21 +49,29 @@ - 'right' - 'bottom' -### minWidth(number) +### minWidth(ILayoutNumber) + +标题最小宽度。 + +{{ use: common-layout-number }} + +### maxWidth(ILayoutNumber) + +标题最大宽度。当文字超过最大宽度时,会自动省略。 -标题最小宽度,像素值。 +{{ use: common-layout-number }} -### maxWidth(number) +### minHeight(ILayoutNumber) -标题最大宽度,像素值。当文字超过最大宽度时,会自动省略。 +标题最小高度。 -### minHeight(number) +{{ use: common-layout-number }} -标题最小高度,像素值。 +### maxHeight(ILayoutNumber) -### maxHeight(number) +标题最大高度。当文字超过最大高度时,会自动省略。 -标题最大高度,像素值。 +{{ use: common-layout-number }} ### innerPadding(Object|number) = 0 diff --git a/packages/vchart/__tests__/unit/component/title/title.test.ts b/packages/vchart/__tests__/unit/component/title/title.test.ts index 380c4c867d..a8a97b19cb 100644 --- a/packages/vchart/__tests__/unit/component/title/title.test.ts +++ b/packages/vchart/__tests__/unit/component/title/title.test.ts @@ -20,9 +20,21 @@ const ctx: any = { getChart: () => ({ getSpec: () => ({}) }), + getChartViewRect: () => ({ width: 800, height: 600 }), getRegionsInIndex: (): any[] => [] }; +const layoutRect = { width: 500, height: 500, x: 0, y: 0 }; + +const createTitle = (spec: any) => { + const title = new Title(spec as any, ctx); + title.created(); + title.init({} as any); + return title; +}; + +const getTitleAttribute = (title: Title) => (title as any)._titleComponent.attribute; + describe('Title Component Repro', () => { it('should not throw error when only subtext is set', () => { const spec = { @@ -35,11 +47,80 @@ describe('Title Component Repro', () => { title.created(); title.init({}); - // Simulate layout - const layoutRect = { width: 500, height: 500, x: 0, y: 0 }; - expect(() => { title.getBoundsInRect(layoutRect); }).not.toThrow(); }); }); + +describe('Title size spec as ILayoutNumber', () => { + it('should resolve percent `maxHeight` against the chart view rect', () => { + const title = createTitle({ text: 'title', maxHeight: '50%' }); + title.getBoundsInRect(layoutRect as any); + + expect(getTitleAttribute(title).maxHeight).toBe(300); + }); + + it('should evaluate function `maxHeight` with the chart view rect', () => { + let received: any; + const title = createTitle({ + text: 'title', + maxHeight: (rect: any) => { + received = rect; + return 72; + } + }); + title.getBoundsInRect(layoutRect as any); + + expect(getTitleAttribute(title).maxHeight).toBe(72); + expect(received).toEqual({ width: 800, height: 600 }); + }); + + it('should resolve percent `minHeight` against the chart view rect', () => { + const title = createTitle({ text: 'title', minHeight: '10%' }); + title.getBoundsInRect(layoutRect as any); + + expect(getTitleAttribute(title).minHeight).toBe(60); + }); + + it('should take percent `height` as the layout height', () => { + const title = createTitle({ text: 'title', height: '25%' }); + const bounds = title.getBoundsInRect(layoutRect as any); + + expect(getTitleAttribute(title).height).toBe(150); + expect(bounds.y2 - bounds.y1).toBe(150); + }); + + it('should fold percent `maxWidth` once, against the chart view rect', () => { + const title = createTitle({ text: 'title', maxWidth: '30%' }); + title.getBoundsInRect(layoutRect as any); + + expect(getTitleAttribute(title).maxWidth).toBe(240); + }); + + it('should still clamp `maxWidth` by the layout rect', () => { + const title = createTitle({ text: 'title', maxWidth: '90%' }); + title.getBoundsInRect(layoutRect as any); + + expect(getTitleAttribute(title).maxWidth).toBe(500); + }); + + it('should keep numeric size config unchanged', () => { + const title = createTitle({ text: 'title', maxHeight: 60, minWidth: 100 }); + title.getBoundsInRect(layoutRect as any); + + expect(getTitleAttribute(title).maxHeight).toBe(60); + expect(getTitleAttribute(title).minWidth).toBe(100); + }); + + it('should leave unset size config undefined', () => { + const title = createTitle({ text: 'title' }); + title.getBoundsInRect(layoutRect as any); + + const attribute = getTitleAttribute(title); + expect(attribute.width).toBeUndefined(); + expect(attribute.height).toBeUndefined(); + expect(attribute.minHeight).toBeUndefined(); + expect(attribute.maxHeight).toBeUndefined(); + }); +}); diff --git a/packages/vchart/src/component/title/interface/spec.ts b/packages/vchart/src/component/title/interface/spec.ts index d5f851bad2..e8b0678173 100644 --- a/packages/vchart/src/component/title/interface/spec.ts +++ b/packages/vchart/src/component/title/interface/spec.ts @@ -1,6 +1,6 @@ import type { IComponent } from '../../interface'; import type { ITextGraphicAttribute, IRichTextCharacter, ITextAttribute } from '@visactor/vrender-core'; -import type { IOrientType, IPadding } from '../../../typings'; +import type { ILayoutNumber, IOrientType, IPadding } from '../../../typings'; import type { IComponentSpec } from '../../base/interface'; interface ITitleSpecWithoutText extends Omit { @@ -22,29 +22,29 @@ interface ITitleSpecWithoutText extends Omit { */ y?: number; /** - * 标题宽度 + * 标题宽度,支持像素值、百分比与回调,百分比与回调的基准是图表视图区域 */ - width?: number; + width?: ILayoutNumber; /** - * 标题高度 + * 标题高度,支持像素值、百分比与回调,百分比与回调的基准是图表视图区域 */ - height?: number; + height?: ILayoutNumber; /** - * 最小宽度,像素值 + * 最小宽度,支持像素值、百分比与回调 */ - minWidth?: number; + minWidth?: ILayoutNumber; /** - * 最大宽度,像素值。当文字超过最大宽度时,会自动省略。 + * 最大宽度,支持像素值、百分比与回调。当文字超过最大宽度时,会自动省略。 */ - maxWidth?: number; + maxWidth?: ILayoutNumber; /** - * 最小高度,像素值 + * 最小高度,支持像素值、百分比与回调 */ - minHeight?: number; + minHeight?: ILayoutNumber; /** - * 最大高度,像素值 + * 最大高度,支持像素值、百分比与回调。当文字超过最大高度时,会自动省略。 */ - maxHeight?: number; + maxHeight?: ILayoutNumber; /** * 标题的边距留白 */ diff --git a/packages/vchart/src/component/title/title.ts b/packages/vchart/src/component/title/title.ts index b0051d3a4a..94a5ef682f 100644 --- a/packages/vchart/src/component/title/title.ts +++ b/packages/vchart/src/component/title/title.ts @@ -2,7 +2,7 @@ import { LayoutLevel, LayoutZIndex } from '../../constant/layout'; import { Factory } from '../../core/factory'; import type { IModelSpecInfo } from '../../model/interface'; import type { IRegion } from '../../region/interface'; -import type { IPoint, IOrientType, ILayoutType, ILayoutRect } from '../../typings'; +import type { IPoint, IOrientType, ILayoutType, ILayoutRect, ILayoutNumber } from '../../typings'; import { calcLayoutNumber, isValidOrient } from '../../util/space'; import { BaseComponent } from '../base/base-component'; // eslint-disable-next-line no-duplicate-imports @@ -15,7 +15,7 @@ import type { TitleAttrs } from '@visactor/vrender-components'; import type { IGraphic, IGroup, INode } from '@visactor/vrender-core'; import type { Maybe } from '@visactor/vutils'; // eslint-disable-next-line no-duplicate-imports -import { isEqual, isValidNumber, pickWithout, isValid } from '@visactor/vutils'; +import { isEqual, isValidNumber, pickWithout, isValid, isNil } from '@visactor/vutils'; import { getSpecInfo } from '../util'; import { title } from '../../theme/builtin/common/component/title'; @@ -110,14 +110,24 @@ export class Title extends BaseComponent i }; } + /** + * 尺寸配置按 ILayoutNumber 契约解析,百分比与回调都以图表视图区域为基准,与 layout-item 一致。 + * 未配置时返回 undefined,避免把 0 当作用户设定的尺寸。 + */ + private _calcSpecSize(value: ILayoutNumber, isHorizontal: boolean): number | undefined { + if (isNil(value)) { + return undefined; + } + const chartViewRect = this._option.getChartViewRect(); + return calcLayoutNumber(value, isHorizontal ? chartViewRect.width : chartViewRect.height, chartViewRect); + } + private _getTitleLayoutRect() { const titleBounds = this._titleComponent.AABBBounds; - const width = this._spec.width ? this._spec.width : isValidNumber(titleBounds.width()) ? titleBounds.width() : 0; - const height = this._spec.height - ? this._spec.height - : isValidNumber(titleBounds.height()) - ? titleBounds.height() - : 0; + const specWidth = this._calcSpecSize(this._spec.width, true); + const specHeight = this._calcSpecSize(this._spec.height, false); + const width = specWidth ? specWidth : isValidNumber(titleBounds.width()) ? titleBounds.width() : 0; + const height = specHeight ? specHeight : isValidNumber(titleBounds.height()) ? titleBounds.height() : 0; return { width, height @@ -133,9 +143,12 @@ export class Title extends BaseComponent i return { visible: false }; } const layoutRect = this.getLayoutRect(); - const titleWidth = calcLayoutNumber(this._spec.width, layoutRect.width, null, layoutRect.width); - const titleMaxWidth = calcLayoutNumber(this._spec.maxWidth, layoutRect.width, null, layoutRect.width); - const maxWidth = Math.max(Math.min(titleWidth, titleMaxWidth, layoutRect.width), 0); + const titleWidth = this._calcSpecSize(this._spec.width, true); + const titleMaxWidth = this._calcSpecSize(this._spec.maxWidth, true); + const maxWidth = Math.max( + Math.min(titleWidth ?? layoutRect.width, titleMaxWidth ?? layoutRect.width, layoutRect.width), + 0 + ); const hasText = isValid(this._spec.text) && this._spec.text !== ''; const hasSubtext = isValid(this._spec.subtext) && this._spec.subtext !== ''; @@ -147,11 +160,12 @@ export class Title extends BaseComponent i subtext: hasSubtext ? this._spec.subtext : undefined, x: this._spec.x ?? 0, y: this._spec.y ?? 0, - height: this._spec.height, - minWidth: this._spec.minWidth, + width: titleWidth, + height: this._calcSpecSize(this._spec.height, false), + minWidth: this._calcSpecSize(this._spec.minWidth, true), maxWidth, - minHeight: this._spec.minHeight, - maxHeight: this._spec.maxHeight, + minHeight: this._calcSpecSize(this._spec.minHeight, false), + maxHeight: this._calcSpecSize(this._spec.maxHeight, false), padding: this._spec.innerPadding, align: this._spec.align ?? 'left', verticalAlign: this._spec.verticalAlign ?? 'top',