概述
透视表表头布局复杂,同时存在了列表头和行表头。本节将介绍透视表表头的各个模块,以及各个模块之前是如何关联的,同时介绍各个模块的生成方式。
场景树相关
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)