树形展示
需求
PivotTable 的一大特点就是树形的 rowHeader和columnHeader。用户能根据以下配置定义树的展示形式:
-
rowHierarchyType / columnHierarchyType:树展示模式 -
grid (同时支持 row 和 column)

- tree (仅支持 row)

- grid-tree (同时支持 row 和 column)

-
indicatorsAsCol:指标是否作为列表头展示,默认为 true -
rowExpandLevel / columnExpandLevel:默认展开层数 -
当自定义
rowTree / columnTree,对每个节点可用node.hierarchyState设置节点的折叠状态
问题
由上面的需求,我们可能会有一些疑问❓:
-
怎么渲染出树形
rowHeader / columnHeadr,需要什么数据? -
不同的
rowHierarchyType / columnHierarchyType,会有不同的合并单元格、展开逻辑,怎么处理比较优雅? -
布局算法是怎么处理这几种
HierarchyType的
源码
在 7.2 的「自动组织维度树」小节中,我们知道了 Dataset 模块的 setRecords方法中,会根据从原始数据中收集到的维度成员rowKeys,**调用 ****ArrToTree****组装好维度树,储存在 ****Dataset.rowHeaderTree**中。
后续 PivotTable 会根据rowHeaderTree继续做一些处理,渲染树形表头,我们一起来看下这整个链路的细节
Dataset.rowHeaderTree / colHeaderTree
-
如果用户传自定义树
customRowTree/colHeaderTree,就直接赋值给dataset.rowHeaderTree / colHeaderTree -
否则就用
ArrToTree和ArrToTree1将维度成员rowKeys和colKeys转为树形结构,再进行赋值
// packages/vtable/src/dataset/dataset.ts export class Dataset { ... setRecords(records: any[] | Record<string, any[]>) { ... if (this.customRowTree) { this.rowHeaderTree = this.customRowTree; } else { if (this.rowHierarchyType === 'tree') { this.rowHeaderTree = this.ArrToTree1(...) } else { this.rowHeaderTree = this.ArrToTree(...) } } if (this.customColTree) { this.colHeaderTree = this.customColTree; } else { this.colHeaderTree = this.ArrToTree(...) } } }
DimensionTree
- 会从
dataset.rowHeaderTree / colHeaderTree或用户自定义表头树,作为参数传给DimensionTree类,实例化生成rowDimensionTree / columnDimensionTree
// packages/vtable/src/PivotTable.ts export class PivotTable extends BaseTable implements PivotTableAPI { constructor(...) { ... const keysResults = parseColKeyRowKeyForPivotTable(this, options); let { columnDimensionTree, rowDimensionTree } = keysResults; ... if (!options.columnTree) { **columnDimensionTree = new DimensionTree(** (this.dataset.colHeaderTree as ITreeLayoutHeadNode[]) ?? [], ... ); } if (!options.rowTree) { **rowDimensionTree = new DimensionTree(** (this.dataset.rowHeaderTree as ITreeLayoutHeadNode[]) ?? [], ... ) } } }
-
DimensionTree类的constructor函数中,核心逻辑在this.setTreeNode(this.tree, 0, this.tree)。setTreeNode是一个递归函数,会遍历树,对每个节点都做setTreeNode处理 -
生成节点
id -
根据
**hierarchyType**配置和**node.hierarchyState**,更新节点的**level**属性(后续将用于布局),更新DimensionTree的totalLevel和size属性

PivotHeaderLayoutMap
**this.internalProps.layoutMap = new PivotHeaderLayoutMap**(
this,
this.dataset,
columnDimensionTree,
rowDimensionTree
);
}
}
我们来看下 `PivotHeaderLayoutMap` 类做了哪些事情:
1. **确定合并单元格、节点折叠状态的逻辑**。下面这个4个属性会决定 `cornerHear`, `columnHear`, `rowHeader`的展示内容、合并单元格逻辑
```Typescript
// packages/vtable/src/layout/pivot-header-layout.ts
export class PivotHeaderLayoutMap implements LayoutMapAPI {
/**下面四份代表实际展示的 如果隐藏了某部分表头 那这里就会相比上面的数组少了隐藏掉的id 例如收hideIndicatorName影响*/
_cornerHeaderCellIds: number[][] = [];
private _columnHeaderCellIds: number[][] = [];
private _rowHeaderCellIds: number[][] = [];
private _rowHeaderCellIds_FULL: number[][] = []; //分页需求新增 为了保存全量的id 当页的是_rowHeaderCellIds
// 记录单元格 HeaderData 对象
cornerHeaderObjs: HeaderData[];
columnHeaderObjs: HeaderData[] = [];
rowHeaderObjs: HeaderData[] = [];
...
}
- 当
rowHierarchyType为grid的时候,_rowHeaderCellIds这个二维数组分别指定单元格对应的唯一Id,Id相同则为合并单元格情况。如下左图中Id:23为合并情况,Id:27为不合并的情况

- 当
rowHierarchyType为tree的时候,所有的维度会归到同一列展示,_rowHeaderCellIds会如下图:

- 并且
node.hierarchyState会记录节点的折叠状态


- 可以看到具体生成行头、列头单元格数据的逻辑在
this._addHeaders()、this._addHeadersForGridTreeMode()和this._addHeadersForTreeMode()中
// packages/vtable/src/layout/pivot-header-layout.ts
export class PivotHeaderLayoutMap implements LayoutMapAPI {
...
constructor(...) {
// 生成列表头单元格
this._generateColHeaderIds();
// 生成行表头单元格
this._generateRowHeaderIds();
}
_generateRowHeaderIds() {
if (this.rowDimensionTree.tree.children?.length >= 1) {
if (this.rowHierarchyType === 'tree') {
**this._addHeadersForTreeMode(...)**
} else if (this.rowHierarchyType === 'grid-tree') {
const startRow = 0;
**this._addHeadersForGridTreeMode(...)**
} else {
**this._addHeaders(...)**
}
}
}
- 三个
this._addHeadersXX()方法逻辑类似,都会和dealHeaderXX()方法组合成递归逻辑,遍历树,生成**HeaderData**类型的单元格数据,并做适当的存储
- 生成
cornerHeadr单元格数据;设置列宽
// packages/vtable/src/layout/pivot-header-layout.ts
export class PivotHeaderLayoutMap implements LayoutMapAPI {
...
constructor(...) {
this.cornerHeaderObjs = this._addCornerHeaders(
colDimensionKeys,
rowDimensionKeys,
this.columnsDefine.concat(...this.rowsDefine, ...extensionRowDimensions)
);
...
this.setColumnWidths();
}
}
创建场景树 & 渲染
创建场景树,发布事件,完结撒花!
scenegraph.createSceneGraph()实际属于**渲染引擎 **(packages/vtable/src/scenegraph/scenegraph.ts),不属于本章讨论范围,这里不做过多分析
// packages/vtable/src/PivotTable.ts
export class PivotTable extends BaseTable implements PivotTableAPI {
constructor(...) {
...
// 生成单元格场景树
this.scenegraph.createSceneGraph();
// 为了确保用户监听得到这个事件 这里做了异步 确保vtable实例已经初始化完成
setTimeout(() => {
this.fireListeners(TABLE_EVENT_TYPE.INITIALIZED, null);
}, 0);
}
}