Layout Elements
Layout elements inherit the layout information of the chart module and are responsible for participating in the layout logic. Its implementation is at this repository address
packages/vchart/src/layout/layout-item.ts
Below is a detailed explanation of the layout element's code
Receiving Layout Configuration
In the code, the layout element reads the layout configuration from the spec through the setAttrFromSpec lifecycle, and additionally calculates the pixel values in the layout configuration during onLayoutStart.
// line: 191 setAttrFromSpec(spec: ILayoutItemSpec, chartViewRect: ILayoutRect) { this._spec = spec; this.layoutType = spec.layoutType ?? this.layoutType; this.layoutLevel = spec.layoutLevel ?? this.layoutLevel; this.layoutOrient = spec.orient ?? this.layoutOrient; this._setLayoutAttributeFromSpec(spec, chartViewRect); this.layoutClip = spec.clip ?? this.layoutClip; } onLayoutStart(layoutRect: IRect, viewRect: ILayoutRect, ctx: any) { // 在 layoutStart 时重新 计算 spec 中的布局属性值 // 确保 resize 后,这些值保持正确的px值。 this._setLayoutAttributeFromSpec(this._spec, viewRect); }
The logic for calculating layout pixel values includes percentage strings, function configurations, etc. The unified calculation logic for layout values is here. \r\n\r
// packages/vchart/src/util/space.ts
export function calcLayoutNumber(
v: ILayoutNumber | undefined,
size: number,
callOp?: ILayoutRect, //如果是函数类型的话,函数的参数
defaultValue: number = 0
) {
if (isNumber(v)) {
return v;
}
if (isPercent(v)) {
return (Number(v.substring(0, v.length - 1)) * size) / 100;
}
if (isFunction(v)) {
return v(callOp);
}
if (isObject(v)) {
return size * (v.percent ?? 0) + (v.offset ?? 0);
}
return defaultValue;
}
Communicating with the Chart Module
The layout logic will obtain the actual drawing size of the chart module in a given space by calling the computeBoundsInRect method of LayoutItem.
LayoutItem Obtaining Drawing Size
// line: 365 computeBoundsInRect(rect: ILayoutRect): ILayoutRect { // 保留布局使用的rect this._lastComputeRect = rect; // 一些情况下不需要计算 if ( (this.layoutType === 'region-relative' || this.layoutType === 'region-relative-overlap') && ((this._layoutRectLevelMap.width === USER_LAYOUT_RECT_LEVEL && (this.layoutOrient === 'left' || this.layoutOrient === 'right')) || (this._layoutRectLevelMap.height === USER_LAYOUT_RECT_LEVEL && (this.layoutOrient === 'bottom' || this.layoutOrient === 'top'))) ) { return this._layoutRect; } // 将布局空间限制到 spec 设置内 // 避免操作到元素本身的 aabbbounds const bounds = { ...this._model.getBoundsInRect(this.setRectInSpec(rect), rect) }; // 用户设置了布局元素宽高的场景下,内部布局结果的 bounds 不能直接作为图表布局bounds this.changeBoundsBySetting(bounds); // 保留当前模块的布局超出内容,用来处理自动缩进 // 当前 bounds 需要有实际宽高 if (this.autoIndent && bounds.x2 - bounds.x1 > 0 && bounds.y2 - bounds.y1 > 0) { this._lastComputeOutBounds.x1 = Math.ceil(-bounds.x1); this._lastComputeOutBounds.x2 = Math.ceil(bounds.x2 - rect.width); this._lastComputeOutBounds.y1 = Math.ceil(-bounds.y1); this._lastComputeOutBounds.y2 = Math.ceil(bounds.y2 - rect.height); } // 返回的布局大小也要限制到 spec 设置内 let result = this.setRectInSpec(boundsInRect(bounds, rect)); if (this._option.transformLayoutRect) { result = this._option.transformLayoutRect(result); } return result; }
The chart module here needs to implement the interface to get bounds
export interface ILayoutModel extends IModel {
getBoundsInRect: (rect: ILayoutRect, fullRect: ILayoutRect) => IBoundsLike;
}
Chart Module Obtaining Layout Information
The chart module obtains the layout space information by holding layoutItem
// 下面是部分类型定义 export interface ILayoutItem { readonly type: string; /** * 标记这个布局Item的方向(left->right, right->left, top->bottom, bottom->top) */ directionStr?: 'l2r' | 'r2l' | 't2b' | 'b2t'; layoutClip: boolean; layoutType: ILayoutType; layoutBindRegionID: number | number[]; layoutOrient: IOrientType; /** 是否自动缩进 */ autoIndent: boolean; alignSelf?: 'start' | 'end' | 'middle'; /** paddding */ layoutPaddingLeft: number; layoutPaddingTop: number; layoutPaddingRight: number; layoutPaddingBottom: number; /** offset */ layoutOffsetX: number; layoutOffsetY: number; /** 布局优先级,越大越先处理 */ layoutLevel: number; getLayoutStartPoint: () => ILayoutPoint; getLayoutRect: () => ILayoutRect; }
Chart Module
The chart module will obtain layout information and update its own graphic position during the onLayoutEnd lifecycle.
// packages/vchart/src/model/layout-model.ts // line: 56 onLayoutEnd(ctx: any): void { super.onLayoutEnd(ctx); // diff layoutRect this.updateLayoutAttribute(); // ... other code }
Layout Logic
In VChart, the definition of layout logic is actually just a function that receives layout elements and updates their layout properties.
// 类型定义 export type LayoutCallBack = ( chart: any, item: ILayoutItem[], chartLayoutRect: IRect, chartViewBox: IBoundsLike ) => void; // VChart 通过接口 setLayout 设置自定义布局 export interface IVChart { /** * 设置自定义布局 */ setLayout: (layout: LayoutCallBack) => void; // other }
Placeholder Layout
The most commonly used layout logic is the method of placing layout elements on the canvas one by one according to type and priority.
The calculation of placeholders is done by initializing the top, bottom, left, and right boundaries at the start of the layout, and then reducing the boundaries after each item is laid out, gradually narrowing the layoutable area.
Initialization
protected _layoutInit(_chart: IChart, items: ILayoutItem[], chartLayoutRect: IRect, chartViewBox: IBoundsLike) {
this._chartLayoutRect = chartLayoutRect;
this._chartViewBox = chartViewBox;
this.leftCurrent = chartLayoutRect.x;
this.topCurrent = chartLayoutRect.y;
this.rightCurrent = chartLayoutRect.x + chartLayoutRect.width;
this.bottomCurrent = chartLayoutRect.height + chartLayoutRect.y;
// 越大越先处理,进行排序调整,利用原地排序特性,排序会受 level 和传进来的数组顺序共同影响
items.sort((a, b) => b.layoutLevel - a.layoutLevel);
}
Layout Execution
// packages/vchart/src/layout/base-layout.ts // line: 91 layoutItems(_chart: IChart, items: ILayoutItem[], chartLayoutRect: IRect, chartViewBox: IBoundsLike): void { // 布局初始化 this._layoutInit(_chart, items, chartLayoutRect, chartViewBox); // 先布局 normal 类型的元素 this._layoutNormalItems(items); // 开始布局 region 相关元素 // 为了自动缩进能力先保存一下当前的布局空间 const layoutTemp: LayoutSideType = { left: this.leftCurrent, top: this.topCurrent, right: this.rightCurrent, bottom: this.bottomCurrent }; // 将 reion 相关元素分组 const { regionItems, relativeItems, relativeOverlapItems, allRelatives,overlapItems } = this._groupItems(items); // 有元素开启了自动缩进 // TODO:目前只有普通占位布局下的 region-relative 元素支持 // 主要考虑常规元素超出画布一般为用户个性设置,而且可以设置padding规避裁剪,不需要使用自动缩进 this.layoutRegionItems(regionItems, relativeItems, relativeOverlapItems, overlapItems); // 缩进计算 this._processAutoIndent(regionItems, relativeItems, relativeOverlapItems, overlapItems, allRelatives, layoutTemp); // 最后布局绝对定位元素 this.layoutAbsoluteItems(items.filter(x => x.layoutType === 'absolute')); }
item placeholder: Below is the logic for a regular element placeholder. First, pass the current layout range to item. After item completes the layout, reduce the space in the corresponding direction.
protected layoutNormalItems(normalItems: ILayoutItem[]): void {
normalItems.forEach(item => {
const layoutRect = this.getItemComputeLayoutRect(item);
const rect = item.computeBoundsInRect(layoutRect);
item.setLayoutRect(rect);
if (item.layoutOrient === 'left') {
item.setLayoutStartPosition({
x: this.leftCurrent + item.layoutOffsetX + item.layoutPaddingLeft,
y: this.topCurrent + item.layoutOffsetY + item.layoutPaddingTop
});
this.leftCurrent += rect.width + item.layoutPaddingLeft + item.layoutPaddingRight;
} else if (item.layoutOrient === 'top') {
item.setLayoutStartPosition({
x: this.leftCurrent + item.layoutOffsetX + item.layoutPaddingLeft,
y: this.topCurrent + item.layoutOffsetY + item.layoutPaddingTop
});
this.topCurrent += rect.height + item.layoutPaddingTop + item.layoutPaddingBottom;
} else if (item.layoutOrient === 'right') {
item.setLayoutStartPosition({
x: this.rightCurrent + item.layoutOffsetX - rect.width - item.layoutPaddingRight,
y: this.topCurrent + item.layoutOffsetY + item.layoutPaddingTop
});
this.rightCurrent -= rect.width + item.layoutPaddingLeft + item.layoutPaddingRight;
} else if (item.layoutOrient === 'bottom') {
item.setLayoutStartPosition({
x: this.leftCurrent + item.layoutOffsetX + item.layoutPaddingRight,
y: this.bottomCurrent + item.layoutOffsetY - rect.height - item.layoutPaddingBottom
});
this.bottomCurrent -= rect.height + item.layoutPaddingTop + item.layoutPaddingBottom;
}
});
}
Grid Layout
The grid layout method is to first establish row and column layout information arrays according to the layout configuration and configuration items.
Initialization
// packages/vchart/src/layout/grid-layout/grid-layout.ts
type GridSize = {
value: number;
isUserSetting: boolean;
isLayoutSetting: boolean;
};
export class GridLayout implements IBaseLayout {
// 行列信息
protected _col: number = 1;
protected _row: number = 1;
// 存储行列的大小和配置信息
protected _colSize: GridSize[];
protected _rowSize: GridSize[];
// 每一行,每一列都有一个数组存储它对应的布局 item
protected _colElements: ILayoutItem[][];
protected _rowElements: ILayoutItem[][];
constructor(gridInfo: IGridLayoutSpec, ctx: utilFunctionCtx) {
this.standardizationSpec(gridInfo);
this._gridInfo = gridInfo;
this._col = gridInfo.col;
this._row = gridInfo.row;
this._colSize = new Array(this._col).fill(null);
this._rowSize = new Array(this._row).fill(null);
this._colElements = new Array(this._col).fill([]);
this._rowElements = new Array(this._row).fill([]);
this._onError = ctx?.onError;
this.initUserSetting();
}
protected initUserSetting() {
// 先对用户设置的宽高进行设置
this._gridInfo.colWidth &&
this.setSizeFromUserSetting(this._gridInfo.colWidth, this._colSize, this._col, this._chartLayoutRect.width);
this._gridInfo.rowHeight &&
this.setSizeFromUserSetting(this._gridInfo.rowHeight, this._rowSize, this._row, this._chartLayoutRect.height);
// 其余位置默认填充0
this._colSize.forEach((c, i) => {
if (!c) {
this._colSize[i] = {
value: 0,
isUserSetting: false,
isLayoutSetting: false
};
}
});
this._rowSize.forEach((r, i) => {
if (!r) {
this._rowSize[i] = {
value: 0,
isUserSetting: false,
isLayoutSetting: false
};
}
});
}
// other
}
Layout Execution
In grid layout, elements are placed into the layout information according to certain attributes, configured to their designated row and column positions. Then, in the order of columns first and rows second, each element undergoes layout calculation and is placed into the prepared row and column layout information.
After the first column layout is completed, only the column width is determined, and then the row layout is performed. After the first round of layout is completed, the column elements undergo a second layout, allowing them to readjust their layout attributes based on the width. Only after this are all layout information finalized, and at this point, all elements are positioned once.
layoutItems(_chart: IChart, items: ILayoutItem[], chartLayoutRect: IRect, chartViewBox: IBoundsLike): void {
this._chartLayoutRect = chartLayoutRect;
this._chartViewBox = chartViewBox;
// 先清空旧布局信息
this.clearLayoutSize();
// 越大越先处理,进行排序调整,利用原地排序特性,排序会受 level 和传进来的数组顺序共同影响
items.sort((a, b) => b.layoutLevel - a.layoutLevel);
// 剔除 region 后,其余元素先布局运算
const normalItems = items.filter(item => item.layoutType === 'normal' && item.getModelVisible() !== false);
const normalItemsCol = normalItems.filter(item => isColItem(item));
const normalItemsRow = normalItems.filter(item => !isColItem(item));
normalItems.forEach(item => {
this.layoutOneItem(item, 'user', false);
});
// region 和 region 关联元素
const regionsRelative = items.filter(x => x.layoutType === 'region-relative');
const regionsRelativeCol = regionsRelative.filter(item => isColItem(item));
const regionsRelativeRow = regionsRelative.filter(item => !isColItem(item));
// 先进行 col 方向布局
regionsRelativeCol.forEach(item => this.layoutOneItem(item, 'user', false));
// 然后得到最终 col 信息 此时已经是最终 col 信息
this.layoutGrid('col');
// 再使用宽度信息辅助row方向排序
// 此时普通占位元素,会因为布局宽度影响最终布局高度
normalItemsRow.forEach(item => this.layoutOneItem(item, 'colGrid', false));
regionsRelativeRow.forEach(item => {
this.layoutOneItem(item, 'colGrid', false);
});
// 然后得到最终 row 信息
this.layoutGrid('row');
// 统一水平方向元素高度
regionsRelativeRow.forEach(item => {
this.layoutOneItem(item, 'grid', false);
});
// 再使用宽度信息,第二次次对 col 方向布局
normalItemsCol.forEach(item => this.layoutOneItem(item, 'grid', false));
regionsRelativeCol.forEach(item => {
// 此时从布局逻辑可知,item的layoutRect会发生,将item的layoutTag设置为true
this.layoutOneItem(item, 'grid', true);
});
this.layoutGrid('col');
// region
items.filter(x => x.layoutType === 'region').forEach(item => this.layoutOneItem(item, 'grid', false));
// 再找出 absolute 元素,无需排序,在 compiler 层需要排序放置
this.layoutAbsoluteItems(items.filter(x => x.layoutType === 'absolute'));
// 最后基于grid 设置位置
items
.filter(x => x.layoutType !== 'absolute')
.forEach(item => {
item.setLayoutStartPosition(this.getItemPosition(item));
});
}
The layout logic of a single element remains consistent, using the same method \r\n\r
protected layoutOneItem(item: ILayoutItem, sizeType: 'user' | 'grid' | 'colGrid' | 'rowGrid', ignoreTag: boolean) {
const sizeCallRow =
sizeType === 'rowGrid' || sizeType === 'grid' ? this.getSizeFromGrid.bind(this) : this.getSizeFromUser.bind(this);
const sizeCallCol =
sizeType === 'colGrid' || sizeType === 'grid' ? this.getSizeFromGrid.bind(this) : this.getSizeFromUser.bind(this);
// 先获取 item 的 grid 信息
const gridSpec = this.getItemGridInfo(item);
// 设置空间
const computeRect = {
width:
(sizeCallCol(gridSpec, 'col') ?? this._chartLayoutRect.width) -
item.layoutPaddingLeft -
item.layoutPaddingRight,
height:
(sizeCallRow(gridSpec, 'row') ?? this._chartLayoutRect.height) -
item.layoutPaddingTop -
item.layoutPaddingBottom
};
// 计算尺寸
const rect = item.computeBoundsInRect(computeRect);
if (!isValidNumber(rect.width)) {
rect.width = computeRect.width;
}
if (!isValidNumber(rect.height)) {
rect.height = computeRect.height;
}
// 更新最终尺寸
item.setLayoutRect(sizeType !== 'grid' ? rect : computeRect);
// 设置大小到grid
this.setItemLayoutSizeToGrid(item, gridSpec);
}
}