From e0c327ff7860c73abe4d1ab70782eba5d5b6a8ca Mon Sep 17 00:00:00 2001 From: "chendaxin.tk" Date: Fri, 19 Jun 2026 12:41:45 +0800 Subject: [PATCH] feat: support function type for `maxRow` / `maxCol` of discrete legend Allow a discrete legend's `maxRow` / `maxCol` to be a callback `(ctx: { rect, orient, id }) => number`, evaluated during layout in `getLegendAttributes` against the legend's allocated rect, so the row / column count can adapt to the available space instead of a layout-blind static number. Numeric values keep working unchanged. --- ...end-maxrow-maxcol-fn_2026-06-19-00-47.json | 11 +++ .../option/en/component/legend-discrete.md | 8 +- .../option/zh/component/legend-discrete.md | 8 +- .../component/legend/discrete-legend.test.ts | 81 +++++++++++++++++++ .../component/legend/discrete/interface.ts | 37 ++++++++- .../src/component/legend/discrete/legend.ts | 2 +- .../src/component/legend/discrete/util.ts | 23 +++++- 7 files changed, 161 insertions(+), 9 deletions(-) create mode 100644 common/changes/@visactor/vchart/feat-discrete-legend-maxrow-maxcol-fn_2026-06-19-00-47.json create mode 100644 packages/vchart/__tests__/unit/component/legend/discrete-legend.test.ts diff --git a/common/changes/@visactor/vchart/feat-discrete-legend-maxrow-maxcol-fn_2026-06-19-00-47.json b/common/changes/@visactor/vchart/feat-discrete-legend-maxrow-maxcol-fn_2026-06-19-00-47.json new file mode 100644 index 0000000000..f290803977 --- /dev/null +++ b/common/changes/@visactor/vchart/feat-discrete-legend-maxrow-maxcol-fn_2026-06-19-00-47.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "feat: support function type for `maxRow` / `maxCol` of the discrete legend, evaluated during layout against the legend's allocated rect, so the row / column count can adapt to the available space", + "type": "minor", + "packageName": "@visactor/vchart" + } + ], + "packageName": "@visactor/vchart", + "email": "chendaxin.tk@bytedance.com" +} diff --git a/docs/assets/option/en/component/legend-discrete.md b/docs/assets/option/en/component/legend-discrete.md index f24cc851dd..890444206c 100644 --- a/docs/assets/option/en/component/legend-discrete.md +++ b/docs/assets/option/en/component/legend-discrete.md @@ -60,18 +60,22 @@ Whether to reverse the ordering of the legend items, the default is not. The overall maximum width of the legend, which determines whether horizontally laid out legends (with an orientation attribute of `'left'` | `'right'`) are automatically line-breaking. -### maxCol(number) +### maxCol(number|Function) Effective only when `orient` is `'left'` | `'right'`, indicates the maximum number of columns for the legend item, the legend item beyond the maximum number of columns will be hidden. +Since version `2.0.23`, a callback `(ctx) => number` is also supported, evaluated during layout so the column count can adapt to the space allocated to the legend (it is re-evaluated on every layout pass, so it also updates automatically on resize). The callback receives `ctx` as `{ rect, orient, id }`: `rect` is the layout size allocated to the legend `{ width, height }` (no position), `orient` is the legend's resolved orientation (defaults to `'left'` when `orient` is unset), and `id` is the legend id. + ### maxHeight(number) The maximum height of the legend as a whole, which determines whether vertically laid out legends (with an orientation attribute of `'top'` | `'bottom'`) are automatically line-breaking. -### maxRow(number) +### maxRow(number|Function) Effective only when `orient` is `'top'` | `'bottom'`, indicates the maximum number of rows for the legend item, the legend item beyond the maximum number of rows will be hidden. +Since version `2.0.23`, a callback `(ctx) => number` is also supported, evaluated during layout so the row count can adapt to the space allocated to the legend (it is re-evaluated on every layout pass, so it also updates automatically on resize). The callback receives `ctx` as `{ rect, orient, id }`: `rect` is the layout size allocated to the legend `{ width, height }` (no position), `orient` is the legend's resolved orientation (defaults to `'left'` when `orient` is unset), and `id` is the legend id. + ### lazyload(boolean) Supported since version 1.12.12 diff --git a/docs/assets/option/zh/component/legend-discrete.md b/docs/assets/option/zh/component/legend-discrete.md index de6eac98a5..885512b661 100644 --- a/docs/assets/option/zh/component/legend-discrete.md +++ b/docs/assets/option/zh/component/legend-discrete.md @@ -60,18 +60,22 @@ 图例整体的最大宽度,决定水平布局的图例(orient 属性为 `'left'` | `'right'`)是否自动换行。 -### maxCol(number) +### maxCol(number|Function) 仅当 `orient` 为 `'left'` | `'right'` 时生效,表示图例项的最大列数,超出最大列数的图例项会被隐藏。 +自 `2.0.23` 版本开始支持传入回调 `(ctx) => number`,在布局阶段根据图例分配到的空间动态计算列数(会在每次布局时重新求值,因此 resize 时也会自动更新)。回调入参 `ctx` 为 `{ rect, orient, id }`:`rect` 为图例分配到的布局尺寸 `{ width, height }`(不含位置),`orient` 为图例解析后的方位(未配置 `orient` 时默认 `'left'`),`id` 为图例 id。 + ### maxHeight(number) 图例整体的最大高度,决定垂直布局的图例(orient 属性为 `'top'` | `'bottom'`)是否自动换行。 -### maxRow(number) +### maxRow(number|Function) 仅当 `orient` 为 `'top'` | `'bottom'` 时生效,表示图例项的最大行数,超出最大行数的图例项会被隐藏。 +自 `2.0.23` 版本开始支持传入回调 `(ctx) => number`,在布局阶段根据图例分配到的空间动态计算行数(会在每次布局时重新求值,因此 resize 时也会自动更新)。回调入参 `ctx` 为 `{ rect, orient, id }`:`rect` 为图例分配到的布局尺寸 `{ width, height }`(不含位置),`orient` 为图例解析后的方位(未配置 `orient` 时默认 `'left'`),`id` 为图例 id。 + ### lazyload(boolean) 自 1.12.12 版本开始支持 diff --git a/packages/vchart/__tests__/unit/component/legend/discrete-legend.test.ts b/packages/vchart/__tests__/unit/component/legend/discrete-legend.test.ts new file mode 100644 index 0000000000..9601349cf0 --- /dev/null +++ b/packages/vchart/__tests__/unit/component/legend/discrete-legend.test.ts @@ -0,0 +1,81 @@ +import { getLegendAttributes } from '../../../../src/component/legend/discrete/util'; + +describe('Discrete legend getLegendAttributes maxRow/maxCol', () => { + const rect = { width: 200, height: 80 }; + + test('should evaluate function `maxRow` against the layout rect', () => { + const attrs = getLegendAttributes( + { + type: 'discrete', + orient: 'bottom', + id: 'l1', + maxRow: (ctx: any) => Math.floor(ctx.rect.height / 20) + } as any, + rect as any + ); + + expect(attrs.maxRow).toBe(4); + }); + + test('should evaluate function `maxCol` and pass rect / orient / id in the context', () => { + let received: any; + const attrs = getLegendAttributes( + { + type: 'discrete', + orient: 'right', + id: 'l2', + maxCol: (ctx: any) => { + received = ctx; + return 3; + } + } as any, + rect as any + ); + + expect(attrs.maxCol).toBe(3); + expect(received.rect).toBe(rect); + expect(received.orient).toBe('right'); + expect(received.id).toBe('l2'); + }); + + test('should keep numeric `maxRow` / `maxCol` unchanged', () => { + const attrs = getLegendAttributes({ type: 'discrete', maxRow: 2, maxCol: 1 } as any, rect as any); + + expect(attrs.maxRow).toBe(2); + expect(attrs.maxCol).toBe(1); + }); + + test('should pass the resolved `left` orient to the callback when `spec.orient` is unset', () => { + let received: any; + getLegendAttributes( + { + type: 'discrete', + maxCol: (ctx: any) => { + received = ctx; + return 1; + } + } as any, + rect as any + ); + + expect(received.orient).toBe('left'); + }); + + test('should prefer the layout-resolved orient over `spec.orient`', () => { + let received: any; + getLegendAttributes( + { + type: 'discrete', + orient: 'left', + maxRow: (ctx: any) => { + received = ctx; + return 1; + } + } as any, + rect as any, + 'bottom' + ); + + expect(received.orient).toBe('bottom'); + }); +}); diff --git a/packages/vchart/src/component/legend/discrete/interface.ts b/packages/vchart/src/component/legend/discrete/interface.ts index e971b5d3e4..65bcf030a5 100644 --- a/packages/vchart/src/component/legend/discrete/interface.ts +++ b/packages/vchart/src/component/legend/discrete/interface.ts @@ -8,6 +8,8 @@ import type { } from '@visactor/vrender-components'; import type { ILegendCommonSpec, NoVisibleMarkStyle } from '../interface'; import type { IFormatMethod, StringOrNumber } from '../../../typings'; +import type { ILayoutRect } from '../../../typings/layout'; +import type { IOrientType } from '../../../typings/space'; import type { IBaseScale } from '@visactor/vscale'; import type { IGlobalScale } from '../../../scale/interface'; import type { ComponentThemeWithDirection } from '../../interface'; @@ -193,6 +195,26 @@ export type ILegendScrollbar = { } & Omit; /** spec */ +/** + * The layout context passed to a `maxRow` / `maxCol` callback. The callback is evaluated during + * layout, so the row / column count can be derived from the space actually allocated to the legend. + * @since 2.0.23 + */ +export interface IDiscreteLegendArrangeContext { + /** the layout rect allocated to the legend in the current layout */ + rect: ILayoutRect; + /** the resolved orient the legend lays out with (defaults to `'left'` when `spec.orient` is unset) */ + orient: IOrientType; + /** the id of the legend */ + id?: StringOrNumber; +} + +/** + * The max row / col count of a discrete legend. Either a fixed number, or a callback that returns a + * number, evaluated during layout against {@link IDiscreteLegendArrangeContext}. + */ +export type DiscreteLegendArrangeCount = number | ((ctx: IDiscreteLegendArrangeContext) => number); + export type IDiscreteLegendSpec = ILegendCommonSpec & { type?: 'discrete'; /** @@ -231,7 +253,20 @@ export type IDiscreteLegendSpec = ILegendCommonSpec & { * 默认筛选的数据范围 */ defaultSelected?: string[]; -} & Omit; + /** + * The maximum number of rows displayed (for horizontal legend). Besides a fixed number, a + * callback `(ctx) => number` is also supported, which is evaluated during layout so the row + * count can adapt to the space allocated to the legend. + * @since 2.0.23 + */ + maxRow?: DiscreteLegendArrangeCount; + /** + * The maximum number of columns displayed (for vertical legend). Besides a fixed number, a + * callback `(ctx) => number` is also supported (see {@link maxRow}). + * @since 2.0.23 + */ + maxCol?: DiscreteLegendArrangeCount; +} & Omit; // theme 主题相关配置 export type IDiscreteLegendCommonTheme = Omit< diff --git a/packages/vchart/src/component/legend/discrete/legend.ts b/packages/vchart/src/component/legend/discrete/legend.ts index 4627e0f895..3359ab63a7 100644 --- a/packages/vchart/src/component/legend/discrete/legend.ts +++ b/packages/vchart/src/component/legend/discrete/legend.ts @@ -182,7 +182,7 @@ export class DiscreteLegend extends BaseLegend { layout, items: this._getLegendItems(), zIndex: this.layoutZIndex, - ...getLegendAttributes(this._spec, rect), + ...getLegendAttributes(this._spec, rect, this.layoutOrient), // maxWidth 和 maxHeight 已经在布局模块处理了,所以 rect 的优先级最高 maxWidth: rect.width, maxHeight: rect.height diff --git a/packages/vchart/src/component/legend/discrete/util.ts b/packages/vchart/src/component/legend/discrete/util.ts index e761324b52..c86d415a6f 100644 --- a/packages/vchart/src/component/legend/discrete/util.ts +++ b/packages/vchart/src/component/legend/discrete/util.ts @@ -1,13 +1,14 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { cloneDeep, isEmpty, isValid } from '@visactor/vutils'; -import { isPercent } from '../../../util/space'; +import { cloneDeep, isEmpty, isFunction, isValid } from '@visactor/vutils'; +import { isPercent, isValidOrient } from '../../../util/space'; import { mergeSpec } from '@visactor/vutils-extension'; import { transformComponentStyle, transformToGraphic } from '../../../util/style'; import { transformLegendTitleAttributes } from '../util'; import type { IDiscreteLegendSpec, ILegendScrollbar, IPager } from './interface'; import type { ILayoutRect } from '../../../typings/layout'; +import type { IOrientType } from '../../../typings/space'; -export function getLegendAttributes(spec: IDiscreteLegendSpec, rect: ILayoutRect) { +export function getLegendAttributes(spec: IDiscreteLegendSpec, rect: ILayoutRect, layoutOrient?: IOrientType) { const { title: titleSpec = {}, item: itemSpec = {}, @@ -45,6 +46,22 @@ export function getLegendAttributes(spec: IDiscreteLegendSpec, rect: ILayoutRect const attrs: any = restSpec; + // `maxRow` / `maxCol` may be a callback, evaluated here during layout against the legend's + // available `rect` so the row / column count can adapt to the space (e.g. allow more rows on + // a tall-and-narrow legend). The callback receives the layout context and returns a number. + // Use the layout-resolved orient (falling back to the spec orient default rule) so the callback + // sees the orient the legend actually lays out with, not the raw `spec.orient` which is + // `undefined` when the user omits it. + if (isFunction(attrs.maxRow) || isFunction(attrs.maxCol)) { + const resolvedOrient = isValidOrient(layoutOrient) ? layoutOrient : isValidOrient(orient) ? orient : 'left'; + if (isFunction(attrs.maxRow)) { + attrs.maxRow = attrs.maxRow({ rect, orient: resolvedOrient, id }); + } + if (isFunction(attrs.maxCol)) { + attrs.maxCol = attrs.maxCol({ rect, orient: resolvedOrient, id }); + } + } + // transform title if (title.visible) { attrs.title = transformLegendTitleAttributes(title);