!!!###!!!title=3.2 Pivot Table Header Structure——VisActor/VTable Contributing Documents!!!###!!!!!!###!!!description=---title: 3.2 Pivot Table Header Structure key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM---!!!###!!!

Overview

The layout of the pivot table headers is complex, with both column headers and row headers. This section will introduce the various modules of the pivot table headers, how these modules are interconnected, and the methods of generating each module.

VTable manages tables in the form of a scene tree. For a pivot table like the one below, there are three main sections regarding the header:

  • ColHeaderGroup: List header Group, responsible for managing the list header part of the pivot table;

  • RowHeaderGroup: Row header Group, responsible for managing the row header part of the pivot table;

  • CornerHeaderGroup: Corner header Group;

Layout Structure

Structure Division

For pivot table headers, there are four core structures that form the framework of the header based on these structures.

They are RowTree row dimension tree, ColumnTree column dimension tree, CornerHeader corner header, indicators.

  • Row Dimension Tree

The row dimension tree is the dimension tree configuration provided by the user. Later, a corresponding row dimension tree instance rowDimensionTree will be created to manage the expand and collapse state of column headers, pagination configuration. The generation of the row header cell identification matrix also relies on the row dimension tree.

  • Column Dimension Tree

The column dimension tree is the dimension tree configuration provided by the user, used to represent the hierarchical relationship of the list header. Later, it is necessary to generate the corresponding identification matrix and be responsible for the layout generation of the header, creating the columnDimensionTree instance.

  • Corner Header

The display format of corner headers is quite special, and there are three forms

  • 'row' as the row dimension name for the header cell content
  • 'column' column dimension name as corner cell content
  • 'all' corner cell content is a concatenation of row dimension name and column dimension name
  • Metrics

The generation of row dimension trees and column dimension trees will be affected by the metrics. Assuming the metrics are [ Sales, Profit ], the dimension trees will be adjusted based on the position of the metrics during the generation process.

Assuming the metric is defined in the last row of the column, when generating the column dimension tree, a metric column will be inserted below each leaf node.

Identity Matrix

In order to quickly and accurately locate the column or row information corresponding to a cell, the concept of an identification matrix is introduced within the pivot table.

The identity matrix is a two-dimensional matrix responsible for generating the layout structure of the header, as well as defining the positioning of cell columns, generating cell styles, and displaying values.

Column Header Identity Matrix

_columnHeaderCellIds

  • Two-dimensional array structure, storing the globally unique ID of each cell in the column header

  • Each element corresponds to a cell in the column header area

  • Hierarchical structure is implemented through nested arrays (e.g., [[1,1,1], [2,3,4]] represents two rows and three columns of column headers)

  • Through this matrix, you can quickly locate the row and column path information of a specific cell

  • The process of dragging the list header is actually changing this field

  • The calculation of colCount required for generating tables also depends on this field

First, recursively generate the corresponding _columnHeaderCellIds through the column dimension tree. When generating row headers, follow the rule of traversing columns first and then rows to obtain the information of the corresponding cell through the row and column numbers, generate the current cell, and after completing the traversal, the corresponding list header is generated.

Row Header Unit Identity Matrix

_rowHeaderCellIds_FULL and _rowHeaderCellIds

  • Two-dimensional array structure, storing the globally unique ID of each cell in the row header;

  • _rowHeaderCellIds_FULL is responsible for storing the full identifier matrix, while _rowHeaderCellIds is only responsible for the row headers displayed on the current page;

  • In the process of dynamic pagination, pagination is achieved by changing _rowHeaderCellIds;

  • Able to quickly obtain the path information of a row header cell through the identification matrix;

  • Affects the calculation of the number of rows colCount that need to be generated for the table;

_columnHeaderCellIds generation is completed, followed by the generation of _rowHeaderCellIds_FULL. The difference from the generation logic of _columnHeaderCellIds is that the generation of the row header cell matrix is transposed from the rows and columns.

Corner Unit Identity Matrix

_cornerHeaderCellIds

After the row header matrix and column header matrix are generated, the corner header cell identification matrix is generated. _cornerHeaderCellIds is responsible for

  • Store the cell ID of the cross area of row and column dimensions

  • Dynamically respond to changes in row and column dimensions (automatically clear when row and column dimensions are 0)

Due to the particularity of corner headers, there are three forms of cornerHeaderCellIds:

  • 'row' as the row dimension name in the header cell content
  • 'column' column dimension name as corner cell content
  • 'all' corner cell content is a concatenation of row dimension name and column dimension name

Header Object Mapping

Having only the identity matrix is not enough to generate the row and column headers because the definition data corresponding to the cells cannot be obtained.

To achieve this, the pivot table internally maintains an object mapping of the header, representing a mapping table of the unique ID of the cell and the current cell definition. Through the cell ID in the layout matrix (such as the values in _columnHeaderCellIds), an O(1) time complexity query can be achieved.

_headerObjectMap is responsible for managing the mapping of all row list headers;

For example, if you want to get the display value of a cell, you only need to provide col, row; then get the unique ID from the identification matrix, and then use the ID to get it from headerObjectMap, which can achieve the function of obtaining the display value of the cell.

// 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 };
    //...
}    

Module Implementation Mechanism

Row/List Header Identification Matrix

Here is the simplified generation logic, which is a depth-first traversal process

// 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);    

  • Prerequisites
  • Specific process

  • Example

Let's take the row dimension tree structure provided above as an example to see what exactly happened:

  • Initial state
_headerCellIds = []
colIndex = 0    

  • Handle the Northeast region (id=1)
// 调用 _addHeaders(row=0)
_headerCellIds = [
  [1]  // row0
]
colIndex=0
// 发现子节点,递归调用 _addHeaders(row=1)    

  • Handle mailing method level one (id=2)
_headerCellIds = [
  [1],  // row0
  [2]   // row1
]
colIndex=0
// 发现子节点,递归调用 _addHeaders(row=2)    

  • Process sales (id=3)
// 处理叶子节点
_headerCellIds = [
  [1],  // row0
  [2],  // row1
  [3]   // row2
]
colIndex=1
// 填充下方行(如果有更多行)    

  • Process profit (id=4), backfill the parent path upwards
_headerCellIds = [
  [1, 1],  // row0
  [2, 2],  // row1
  [3, 4]   // row2
]
colIndex=2
// 返回上级继续处理    

  • In the same way, recursively handle the secondary mailing method (id=5), and backfill the parent path upwards
_headerCellIds = [
  [1, 1, 1],  // row0
  [2, 2, 5],  // row1
  [3, 4, 5]   // row2
]
colIndex=2
// 处理子节点(id=6,7)...    

  • Handle mailing method level three (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
// 完成东北地区处理    

  • Process North China region (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    

  • Final state (after processing in the Central South region)
_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
]    

  • Row header matrix

The process of generating a row header matrix is generally similar to that of a list header, except for an additional step of transposition at the end.

Corner Header Identification Matrix

The identification matrix of the corner header is simpler than the generation logic of the basic table header, as it does not require recursion and only needs to traverse the row and column dimensions.

  • Source code
// 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;
  }    

  • Pre-process
  • General Process

Header Object Mapping

In the previous process, corresponding cell definition nodes are continuously inserted into _headerObjects. Therefore, you only need to call reduce to transform the array into a Map, completing the mapping of the basic header object.

// 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 });
    

Overall Header Generation Logic

This document is provided by the following personnel

taiiiyang( https://github.com/taiiiyang)

This document was revised and organized by the following personnel

玄魂