Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
8 changes: 6 additions & 2 deletions docs/assets/option/en/component/legend-discrete.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 legend's available layout area `{ x, y, width, height }`, `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 legend's available layout area `{ x, y, width, height }`, `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
Expand Down
8 changes: 6 additions & 2 deletions docs/assets/option/zh/component/legend-discrete.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 为图例可用的布局区域 `{ x, y, 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` 为图例可用的布局区域 `{ x, y, width, height }`,`orient` 为图例解析后的方位(未配置 `orient` 时默认 `'left'`),`id` 为图例 id。

### lazyload(boolean)

自 1.12.12 版本开始支持
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { getLegendAttributes } from '../../../../src/component/legend/discrete/util';

describe('Discrete legend getLegendAttributes maxRow/maxCol', () => {
const rect = { x: 0, y: 0, 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');
});
});
37 changes: 36 additions & 1 deletion packages/vchart/src/component/legend/discrete/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -193,6 +195,26 @@ export type ILegendScrollbar = {
} & Omit<LegendScrollbarAttributes, 'railStyle' | 'sliderStyle'>;

/** 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';
/**
Expand Down Expand Up @@ -231,7 +253,20 @@ export type IDiscreteLegendSpec = ILegendCommonSpec & {
* 默认筛选的数据范围
*/
defaultSelected?: string[];
} & Omit<DiscreteLegendAttrs, 'layout' | 'title' | 'items' | 'item' | 'pager'>;
/**
* 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<DiscreteLegendAttrs, 'layout' | 'title' | 'items' | 'item' | 'pager' | 'maxRow' | 'maxCol'>;

// theme 主题相关配置
export type IDiscreteLegendCommonTheme = Omit<
Expand Down
2 changes: 1 addition & 1 deletion packages/vchart/src/component/legend/discrete/legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export class DiscreteLegend extends BaseLegend<IDiscreteLegendSpec> {
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
Expand Down
23 changes: 20 additions & 3 deletions packages/vchart/src/component/legend/discrete/util.ts
Original file line number Diff line number Diff line change
@@ -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 = {},
Expand Down Expand Up @@ -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);
Expand Down
Loading