!!!###!!!title=3.2 透视表表头结构——VisActor/VTable 社区贡献者文档!!!###!!!!!!###!!!description=---title: 3.2 透视表表头结构 key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM---!!!###!!!

概述

透视表表头布局复杂,同时存在了列表头和行表头。本节将介绍透视表表头的各个模块,以及各个模块之前是如何关联的,同时介绍各个模块的生成方式。

场景树相关

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)

本文档由以下人员修正整理

玄魂