概述
透视表表头布局复杂,同时存在了列表头和行表头。本节将介绍透视表表头的各个模块,以及各个模块之前是如何关联的,同时介绍各个模块的生成方式。
场景树相关
VTable 是以场景树的形式来管理表格的,对于下面这样一张透视表,关于表头部分,存在了三大块:

-
ColHeaderGroup:列表头 Group,负责管理透视表列表头部分;
-
RowHeaderGroup: 行表头 Group,负责管理透视表行表头部分;
-
CornerHeaderGroup:角表头 Group;
布局结构
结构划分
对透视表表头来说,有着四个核心的结构,内部根据这四个结构为基础来组成表头的骨架。

分别是 RowTree 行维度树、ColumnTree 列维度树、CornerHeader 角表头、indicators 指标
- 行维度树
行维度树为用户传入的维度树配置,后期会根据行维度树创建对应的行维度树实例 rowDimensionTree,用来管理列头的展开收起状态,分页配置。以及行表头单元标识矩阵的生成都依靠于 行维度树。

- 列维度树
列维度树为用户传入的维度树配置,用于表示列表头的层级关系,后期需要根据该结构生成对应的标识矩阵以及负责表头的布局生成、创建 columnDimensionTree 实例;

- 角表头
角表头的展示形式比较特殊,存在 三种形式
- 'row' 行维度名称作为角头单元格内容

- 'column' 列维度名称作为角头单元格内容

- 'all' 角头单元格内容为行维度名称和列维度名称的拼接

- 指标
行维度树和列维度树的生成都会收到指标的影响,假设指标存在 [ 销售额,利润 ] ,那么在维度树生成的过程中,会根据指标的位置进行调整。
假设指标定义在列的最后一行,在生成列维度树的时候,对于每个叶子节点,都会在下方再插入指标列

标识矩阵
为了能够快速精准的定位到单元格对应的列或行信息,透视表内部引入了标识矩阵的概念。
标识矩阵为二维矩阵,负责表头的布局结构生成,以及定位单元格列定义,单元格的样式以及展示值生成等逻辑。
列头单元标识矩阵
_columnHeaderCellIds
-
二维数组结构,存储列头每个单元格的全局唯一ID
-
每个元素对应列头区域的一个单元格
-
层级结构通过嵌套数组实现(如
[[1,1,1], [2,3,4]]表示两行三列的列头) -
通过该矩阵能够快速定位到某一个单元格的行列路径信息
-
列表头拖拽的过程实际上就是改变的该字段
-
包括表格需要生成的 colCount 计算也是依赖于该字段
先通过列维度树,递归生成对应的 _columnHeaderCellIds 。生成行表头的时候,按照先遍历列,再遍历行的规则,通过行列号获取对应单元格的信息,生成当前的单元格,遍历完成后就生成了对应的列表头。

行头单元标识矩阵
_rowHeaderCellIds_FULL和 _rowHeaderCellIds
-
二维数组结构,存储行头每个单元格的全局唯一ID;
-
_rowHeaderCellIds_FULL负责存储全量的标识矩阵,而_rowHeaderCellIds只负责当前页所展示的行表头; -
在动态分页的过程中,就是通过改变
_rowHeaderCellIds来实现分页的操作; -
能够通过标识矩阵,快速获取某个行头单元格的路径信息;
-
影响到表格需要生成行数 colCount 的计算;
_columnHeaderCellIds 生成完成后,紧接着的就是 _rowHeaderCellIds_FULL的生成,与 _columnHeaderCellIds 的生成逻辑不同的点在于,行头单元矩阵的生成,是对行列进行转置过后的。

角头单元标识矩阵
_cornerHeaderCellIds
在行头矩阵和列头矩阵生成过后,就是角头单元标识矩阵的生成。_cornerHeaderCellIds 负责
-
存储行列维度交叉区域的单元格ID
-
动态响应行列维度变化(当行列维度为0时自动清空)
由于角表头的特殊性,存在着三种形式的 _cornerHeaderCellIds:
- 'row' 行维度名称作为角头单元格内容

- 'column' 列维度名称作为角头单元格内容

- 'all' 角头单元格内容为行维度名称和列维度名称的拼接

表头对象映射
光有标识矩阵还不能满足生成行列角表头的生成,因为不能获取单元格对应的定义数据。
为此,透视表内部维护了一份表头的对象映射,表示单元格的唯一 ID 与当前单元格定义的映射表,通过布局矩阵中的单元格ID(如_columnHeaderCellIds中的数值),可以实现 O(1) 时间复杂度的查询。
_headerObjectMap 负责管理所有的行列表头的映射;

比如说想要获取单元格展示值时,仅需提供 col,row ;然后从标识矩阵中获取唯一 ID,再根据 ID 去_headerObjectMap 获取,即可以实现获取单元格展示值的功能。
// packages\vtable\src\layout\pivot-header-layout.ts getHeader(col: number, row: number): HeaderData | SeriesNumberColumnData { // ... const id = this.getCellId(col, row); return this._headerObjectMap[id as number] ?? { id: undefined, field: '', headerType: 'text', define: undefined }; //... }
模块实现机制
行/列表头标识矩阵
这里是简化后的生成逻辑,是一个深度优先遍历的过程
// packages\vtable\src\layout\pivot-header-layout.ts const _headerObjects = []; // 表头对象的映射 const _headerCellIds = []; let colIndex = 0; // 表示叶子节点的个数 const columnHeaderObjs = {}; // function _addHeaders(_headerCellIds, row, header, roots, results) { const _this = this; function _newRow(row) { const newRow = (_headerCellIds[row] = []); if (colIndex === 0) { return newRow; } const prev = _headerCellIds[row - 1]; for (let col = 0; col < prev?.length; col++) { newRow[col] = prev[col]; } return newRow; } if (!_headerCellIds[row]) { _newRow(row); } for (let i = 0; i < header.length; i++) { const hd = header[i]; dealHeader(hd, _headerCellIds, results, roots, row); } } function dealHeader(hd, _headerCellIds, results, roots, row) { const id = hd.id; const cell = { id, title: hd.value, field: hd.dimensionKey }; results[id] = cell; _headerObjects[id] = cell; for (let r = row - 1; r >= 0; r--) { _headerCellIds[r][colIndex] = roots[r]; } _headerCellIds[row][colIndex] = id; if (hd.children?.length >= 1) { _addHeaders(_headerCellIds, row + 1, hd.children ?? [], [...roots, id], results); } else { for (let r = row + 1; r < _headerCellIds.length; r++) { _headerCellIds[r][colIndex] = id; } colIndex++; } } _addHeaders(_headerCellIds, 0, columnsTree, [], columnHeaderObjs);
- 前置准备

- 具体流程

- 示例
我们以上面提供的行维度树形结构为例,看下具体发生了什么:
- 初始状态
_headerCellIds = [] colIndex = 0
- 处理东北地区(id=1)
// 调用 _addHeaders(row=0) _headerCellIds = [ [1] // row0 ] colIndex=0 // 发现子节点,递归调用 _addHeaders(row=1)
- 处理邮寄方式一级(id=2)
_headerCellIds = [ [1], // row0 [2] // row1 ] colIndex=0 // 发现子节点,递归调用 _addHeaders(row=2)
- 处理销售额(id=3)
// 处理叶子节点 _headerCellIds = [ [1], // row0 [2], // row1 [3] // row2 ] colIndex=1 // 填充下方行(如果有更多行)
- 处理利润(id=4),向上回填父级路径
_headerCellIds = [ [1, 1], // row0 [2, 2], // row1 [3, 4] // row2 ] colIndex=2 // 返回上级继续处理
- 同样的方式,递归,处理邮寄方式二级(id=5),向上回填父级路径
_headerCellIds = [ [1, 1, 1], // row0 [2, 2, 5], // row1 [3, 4, 5] // row2 ] colIndex=2 // 处理子节点(id=6,7)...
- 处理邮寄方式三级(id=8)
_headerCellIds = [ [1, 1, 1, 1, 1, 1], // row0 [2, 2, 5, 5, 8, 8], // row1 [3, 4, 6, 7, 9, 10] // row2 ] colIndex=6 // 完成东北地区处理
- 处理华北地区(id=11)
_headerCellIds = [ [...原东北列..., 11,11,11,11,11,11], // row0 [...原东北列...,12,12,15,15,18,18], // row1 [...原东北列...,13,14,16,17,19,20] // row2 ] colIndex=12
- 最终状 态(中南地区处理完成后)
_headerCellIds = [ [1,1,1,1,1,1, 11,11,11,11,11,11, 21,21,21,21,21,21], // row0 [2,2,5,5,8,8, 12,12,15,15,18,18, 22,22,25,25,28,28], // row1 [3,4,6,7,9,10,13,14,16,17,19,20,23,24,26,27,29,30] // row2 ]
- 行表头矩阵
行表头矩阵的生成与列表头的流程大体相同,只不过在最后多了一步转置的操作。
角表头标识矩阵
角表头的标识矩阵比基本表格表头的生成逻辑简单, 不需要考虑递归,只需要遍历行列维度即可。
- 源码
// packages\vtable\src\layout\pivot-header-layout.ts private _addCornerHeaders( colDimensionKeys: string[] | null, rowDimensionKeys: string[] | null, dimensions: (string | IDimension)[] ) { const results: HeaderData[] = []; if (this.cornerSetting.titleOnDimension === 'all') { if (this.indicatorsAsCol) { let indicatorAtIndex = -1; if (colDimensionKeys) { colDimensionKeys.forEach((dimensionKey: string, key: number) => { const cell: HeaderData = { // ... }; results[id] = cell; this._headerObjects[id] = cell; if (!this._cornerHeaderCellFullPathIds[key]) { this._cornerHeaderCellFullPathIds[key] = []; for (let r = 0; r < this.rowHeaderLevelCount; r++) { this._cornerHeaderCellFullPathIds[key][r] = id; } }); } if (rowDimensionKeys) { rowDimensionKeys.forEach((dimensionKey: string, key: number) => { const id = ++this.sharedVar.seqId; const cell: HeaderData = { // ... }; results[id] = cell; this._headerObjects[id] = cell; if (!this._cornerHeaderCellFullPathIds[indicatorAtIndex]) { this._cornerHeaderCellFullPathIds[indicatorAtIndex] = []; } this._cornerHeaderCellFullPathIds[indicatorAtIndex][key] = id; }); } } else { let indicatorAtIndex = -1; if (rowDimensionKeys) { rowDimensionKeys.forEach((dimensionKey: string, key: number) => { if (dimensionKey === this.indicatorDimensionKey) { indicatorAtIndex = key; } const id = ++this.sharedVar.seqId; const dimensionInfo: IDimension = dimensions.find(dimension => typeof dimension === 'string' ? false : dimension.dimensionKey === dimensionKey ) as IDimension; const cell: HeaderData = { id, // ... }; results[id] = cell; this._headerObjects[id] = cell; for (let r = 0; r < this.columnHeaderLevelCount; r++) { if (!this._cornerHeaderCellFullPathIds[r]) { this._cornerHeaderCellFullPathIds[r] = []; } this._cornerHeaderCellFullPathIds[r][key] = id; } }); } if (colDimensionKeys) { colDimensionKeys.forEach((dimensionKey: string, key: number) => { const id = ++this.sharedVar.seqId; const dimensionInfo: IDimension = dimensions.find(dimension => typeof dimension === 'string' ? false : dimension.dimensionKey === dimensionKey ) as IDimension; const cell: HeaderData = { id, // ... }; results[id] = cell; this._headerObjects[id] = cell; this._cornerHeaderCellFullPathIds[key][indicatorAtIndex] = id; }); } } } else if (this.cornerSetting.titleOnDimension === 'row' || this.cornerSetting.titleOnDimension === 'column') { const dimensionKeys = this.cornerSetting?.titleOnDimension === 'row' ? rowDimensionKeys : colDimensionKeys; if (dimensionKeys) { dimensionKeys.forEach((dimensionKey: string, key: number) => { const id = ++this.sharedVar.seqId; const cell: HeaderData = { id, // ... }; results[id] = cell; this._headerObjects[id] = cell; if (this.cornerSetting.titleOnDimension === 'column') { if (!this._cornerHeaderCellFullPathIds[key]) { this._cornerHeaderCellFullPathIds[key] = []; } for (let r = 0; r < this.rowHeaderLevelCount; r++) { this._cornerHeaderCellFullPathIds[key][r] = id; } } else if (this.cornerSetting.titleOnDimension === 'row') { for (let r = 0; r < this.columnHeaderLevelCount; r++) { if (!this._cornerHeaderCellFullPathIds[r]) { this._cornerHeaderCellFullPathIds[r] = []; } this._cornerHeaderCellFullPathIds[r][key] = id; } } }); } } else { const id = ++this.sharedVar.seqId; const cell: HeaderData = { id, // ... } }; results[id] = cell; this._headerObjects[id] = cell; for (let r = 0; r < this.columnHeaderLevelCount; r++) { for (let j = 0; j < this.rowHeaderLevelCount; j++) { if (!this._cornerHeaderCellFullPathIds[r]) { this._cornerHeaderCellFullPathIds[r] = []; } this._cornerHeaderCellFullPathIds[r][j] = id; } } } return results; }
- 前置流程

- 大致流程

表头对象映射
在前面的流程中,会不断地往 _headerObjects 中塞入对应的单元格定义节点,那么只需要调用 reduce , 将能将数组的形式转变成 Map 的形式,完成基础表头对象的映射。
// packages\vtable\src\layout\pivot-header-layout.ts this._headerObjectMap = this._headerObjects.reduce((o, e) => { o[e.id as number] = e; return o; }, {} as { [key: LayoutObjectId]: HeaderData });

整体表头生成逻辑

本文档由以下人员提供
taiiiyang( https://github.com/taiiiyang)