布局元素
布局元素承接图表模块的布局信息,同时负责参与布局逻辑。它的实现在仓库这个地址
packages/vchart/src/layout/layout-item.ts
下面详细介绍布局元素的详细代码
接收布局配置
在代码的中,布局元素通过 setAttrFromSpec 生命周期读取spec中的布局配置,并且在 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); }
计算布局像素值的逻辑包括百分比字符串,函数配置等,布局数值的统一计算逻辑在这里
// 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; }
与图表模块通信
布局逻辑会通过调用 LayoutItem 的 computeBoundsInRect 方法获取图表模块在给定空间下的实际绘图大小
LayoutItem 获取绘图大小
// 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; }
这里图表模块需要实现得到bounds的接口
export interface ILayoutModel extends IModel { getBoundsInRect: (rect: ILayoutRect, fullRect: ILayoutRect) => IBoundsLike; }
图表模块获取布局信息
图表模块通过持有 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; }
图表模块
图表模块会在 onLayoutEnd 这个生命周期中获取布局信息并更新自身图形位置
// packages/vchart/src/model/layout-model.ts // line: 56 onLayoutEnd(ctx: any): void { super.onLayoutEnd(ctx); // diff layoutRect this.updateLayoutAttribute(); // ... other code }
布局逻辑
在 VChart 中,布局逻辑的定义其实只是一个接收布局元素,并更新它们布局属性的函数
// 类型定义 export type LayoutCallBack = ( chart: any, item: ILayoutItem[], chartLayoutRect: IRect, chartViewBox: IBoundsLike ) => void; // VChart 通过接口 setLayout 设置自定义布局 export interface IVChart { /** * 设置自定义布局 */ setLayout: (layout: LayoutCallBack) => void; // other }