!!!###!!!title=7.2 透视表及透视图数据模块——VisActor/VTable 社区贡献者文档!!!###!!!!!!###!!!description=---title: 7.2 透视表及透视图数据模块 key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM--- !!!###!!!

概述

数据处理是数据可视化的核心步骤之一。这一节将介绍 PivotTable 怎么组织、处理数据,让数据能支持高效渲染 PivotTable 的同时,又兼备 PivotTable 数据分析能力

自动组织维度树

需求背景

沿用我们 的图:

假设我们想实现这样一个多维表格,一般来说,我们期望业务方传进来的参数是:

  • 组织好的维度树 RowTree、ColumnTree(类似 中的 timeTreechannelTree

  • 在各个维度下、各个指标的具体数据 Records

理论上能实现,但缺点也很明显:需要业务方自行将数据组装成这个结构,接入成本较高。我们期望业务方只要传简洁的 Records 加一些简单的配置,我们就能自行解析数据、渲染成多维表格。比如传进来的 Records 是这种从 db 里查到的原始数据:

const records = [
    {
        channel: "线上",
        platform: "淘宝",
        shop: "淘宝旗舰店",
        month: 3,
        day: 2,
        curr_price: 3999
    },
    {
        channel: "线上",
        platform: "京东",
        shop: "京东三方店",
        month: 3,
        day: 3,
        origin_price: 4399,
    },
    ...
]    

目标:将原始数据集`Records`,通过数据处理,得到支持透视表形式显示的数据结构

实现思路

分析

带着上述背景和目标,可能容易产生一些疑问:

  • 怎么通过原始数据生成 rowTreecolumnTree

  • 答:分组聚合。类似 SQL 中的 group,理论上我们可以从 records 中通过类似 group 的方式梳理出各个维度的值(eg. 分组聚合出 platform 维度下有 "淘宝" | "京东" | "抖音" 三个维度值)

  • 怎么保证时间复杂度最低,在 records 数据量大的情况下追求性能?

思路

  • 约定用户传进来的数据 & 数据结构
const datasetOptions = {
  // 原始数据
  records: [
    {
      channel: '线上',
      platform: '淘宝',
      shop: '淘宝期间店',
      month: 3,
      week: 1,
      day: 3,
      origin_price: 4399,
    },
    {
      channel: '线上',
      platform: '淘宝',
      shop: '淘宝三方店',
      month: 3,
      week: 1,
      day: 4,
      curr_price: 4099
    }
    ...
  ],
  // rowTree 和 columnTree 中各维度层次在原始数据中的key
  columns: ['channel', 'platform', 'shop'],
  rows: ['month', 'week', 'day'],
  // 指标在 原始数据 中的key
  indicatorKeys: ['origin_price', 'curr_price']
};
    

  • 任务

  • 收集维度成员(eg. platform 维度下有 "淘宝" | "京东" | "抖音"三个成员)

  • 组装出 rowTreecolumnTree

  • 渲染时,从 records 中快速到单元格对应数据(如下图)

单从已知的任务看,理论上:

  • 遍历一次 records 是能完成 “收集维度成员” 这个任务的;根据收集好的维度成员和用户传进来的columnsrowsindicatorKeys,理论上是能组装出 rowTree、columnTree 的

  • 但是怎么知道这些维度的父子关系呢?我怎么知道shop 维度其实是 platform维度的子维度?

  • 让用户传 columns的时候,父维度排序上比子维度靠前即可,eg:

// ❌ bad 
const options = {
  columns: ['channel', 'shop', 'platform'],
  ...
};

// ✅ good
const options = {
  columns: ['channel', 'platform', 'shop'],
  ...
};    

  • 但“渲染时,从 records 中快速查到单元格对应数据”这个比较令人头疼,假设我们知道了单元格的 行维度 + 列维度,要实现 getCellValue(col: number, row: number) 函数。难道又要再遍历一次records?这可就太麻烦了

  • 最高效的方法:借助 **hash map** 的能力,将查的时间复杂度降低到 O(1)。那怎么设计 hash map的结构呢?

其实数据区就是一个二维矩阵,则用 (row, col) 能定位每个单元格的位置。所以如果我们有一个二维 hash map 的,其结构大概如下,就能用来查单元格数据了

// HashMap 的第一层 key 为 row,第二层 key 为 col
type HashMap = Record<string, Record<string, IndicatorValue[]>>

// 指标值
type IndicatorValue = {
    indicatorKey: string;
    value: string;
}    

在我们这个需求中,怎么定义hash map两层 key 的结构呢?为了保证唯一性,我们可以用rowTreecolumnTree 中从 root叶子节点path组成的字符串做 key(如下图和代码)

// 指标值
type IndicatorValue = {
    indicatorKey: string;
    value: string;
}

// TreeMap 的第一层 key 为 row-path,第二层 key 为 col-path
type TreeMap = Record<string, Record<string, IndicatorValue[]>>

// 数据维度 tree 对象
const tree: TreeMap = {
    '3-1-3': {
        '线上-淘宝-淘宝旗舰店': [
            {
                indicatorKey: "origin_price",
                value: '4299'       
            }
            {
                indicatorKey: "curr_price",
                value: '3999'       
            }
        ]
    }
}    

数据解析流程

有了上面的分析,我们捋一下数据解析流程

遍历

遍历数据过程中需要维护的变量:

// 列维度成员组成的数组
colKeys: string[][]
// 行维度成员组成的数组
rowKeys: string[][]    

遍历数据的过程中也会生成维度 tree 对象(也就是上文说的hash map; 为了统一称呼,下文统一称其为“维度 tree 对象”)

// 数据维度 tree 对象
// 第一层 key 实际就是 colKeys 的元素,再 join 得到的字符串
// 第二层 key 为 rowKeys 的元素经过 join 得到的字符串
tree: Record<string, Record<string, IndicatorValue[]>> = {
    '3-1-3': {
        '线上-淘宝-淘宝旗舰店': [
            {
                indicatorKey: "origin_price",
                value: '4299'       
            }
            {
                indicatorKey: "curr_price",
                value: '3999'       
            }
        ]
    }
}    

查找

有了维度 tree 对象之后,渲染时,就能用它从 records 中快速查到单元格对应数据了

源码

按上述分析流程,我们一起看下源码是怎么实现的

代码入口:`packages/vtable/src/dataset/dataset.ts` 下面的代码经过简化处理
  • setRecords:数据处理的入口方法

  • processRecords:处理数据,遍历所有条目

  • processRecord:处理单条数据,我们刚分析流程基本都在这个函数中实现了

遍历

在之前的设想中,我们设想维度 path 会是 '线上-淘宝-淘宝旗舰店' 这种,会有问题,因为维度成员也可能含有 '-' 这个字符串。
源码中用 String.fromCharCode(0) 作为维度 path 的分隔符,即 \u0000。在 JavaScript 中,\u0000 表示 Unicode 编码为 U+0000 的字符,也就是 **空字符(Null Character)​,**这个字符通常用于表示字符串的结束或占位符,但在实际呈现时,它通常不会被显示出来。在这里主要用来保证维度 path 字符串的唯一性

class Dataset {
    colKeys: string[][] = [];
    rowKeys: string[][] = [];
    private colFlatKeys: Record<string, number> = {}; // 记录某个colKey已经被添加到colKeys
    private rowFlatKeys: Record<string, number> = {}; // 记录某个rowKey已经被添加到rowKeys
    tree: Record<string, Record<string, Aggregator[]>> = {};
    
    stringJoinChar = String.fromCharCode(0); // 维度 path 的分隔符
    
    setRecords(records: any[] | Record<string, any[]>) {
        this.processRecords();
        
        ...
    }
    
    // 处理数据, 遍历所有条目
    private processRecords() {
        ...
        for (let i = 0, len = this.records.length; i < len; i++) {
            const record = this.records[i];
            
            ...
            this.processRecord(record);
        }
    }
    
    // 处理单条数据
    private processRecord(record: any, assignedIndicatorKey?: string) {
        ...
      
        const colKeys: { colKey: string[]; indicatorKey: string | number }[] = [];
        const rowKeys: { rowKey: string[]; indicatorKey: string | number }[] = [];
        
        // 收集维度成员
        const rowKey: string[] = [];
        rowKeys.push({ rowKey, indicatorKey: assignedIndicatorKey });
        for (let l = 0, len1 = this.rows.length; l < len1; l++) {
            const rowAttr = this.rows[l];
            if (rowAttr in record) {
                this.rowsHasValue[l] = true;
                **rowKey.push(record[rowAttr]);**
            }
        }
        
        const colKey: string[] = [];
        colKeys.push({ colKey, indicatorKey: assignedIndicatorKey });
        for (let n = 0, len2 = this.columns.length; n < len2; n++) {
            const colAttr = this.columns[n];
            if (colAttr in record) {
                this.columnsHasValue[n] = true;
                **colKey.push(record[colAttr]);**
            }
        }
        
        for (let row_i = 0; row_i < rowKeys.length; row_i++) {
            const rowKey = rowKeys[row_i].rowKey;
            ...
            
            for (let col_j = 0; col_j < colKeys.length; col_j++) {
                const colKey = colKeys[col_j].colKey;
                
                // 生成 flatRowKey,将用于维度tree对象的key
                **const flatRowKey = rowKey.join(this.stringJoinChar);**
**                const flatColKey = colKey.join(this.stringJoinChar);**
                
                ...
                
                if (rowKey.length !== 0) {
                  if (!this.rowFlatKeys[flatRowKey]) {
                    **this.rowKeys.push(rowKey);**
                    this.rowFlatKeys[flatRowKey] = 1;
                  }
                }
                if (colKey.length !== 0) {
                  if (!this.colFlatKeys[flatColKey]) {
                    **this.colKeys.push(colKey);**
                    this.colFlatKeys[flatColKey] = 1;
                  }
                }
        
                if (!this.tree[flatRowKey]) {
                  this.tree[flatRowKey] = {};
                }
                
                // 生成维度 tree 对象
                if (!this.tree[flatRowKey]?.[flatColKey]) {
                  this.tree[flatRowKey][flatColKey] = [];
                }
                
                const toComputeIndicatorKeys = this.indicatorKeysIncludeCalculatedFieldDependIndicatorKeys;
                for (let i = 0; i < toComputeIndicatorKeys.length; i++) {
                    let needAddToAggregator = false;
                    
                    ...
                    
                    // 生成维度 tree 对象
                    if (needAddToAggregator) {
                        **this.tree[flatRowKey]?.[flatColKey]?.[i].push(record);**
                    }
                }
                ...
                
            }
        }
        
    }
}    

组装生成

  • ArrToTreeArrToTree1:将rowKeyscolKeys 转为树形结构
private ArrToTree1(
    arr: string[][],
    rows: string[],
    indicators: (string | IIndicator)[] | undefined,
    ...
  ): {
     {
       value: string; 
       dimensionKey: string;
       children: any[] | undefined
     } 
  }[] {
    const result: any[] = []; // 结果
    const concatStr = this.stringJoinChar; // 连接符(随便写,保证key唯一性就OK)
    const map = new Map(); // 存储根节点 主要提升性能
    
    function addList(list: any, isGrandTotal: boolean) {
      const path: any[] = []; // 路径
      let node: any; // 当前节点
      list.forEach((value: any, index: number) => {
        path.push(value);
        const flatKey = path.join(concatStr);
        //id的值可以每次生成一个新的 这里用的path作为id 方便layout对象获取
        let item: { value: string; dimensionKey: string; children: any[] | undefined } = map.get(flatKey); // 当前节点
        if (!item) {
          item = {
            value,
            dimensionKey: rows[index],
            //树的叶子节点补充指标
            children:
              index === list.length - 1 && (indicators?.length ?? 0) >= 1
                ? indicators?.map(indicator => {
                    if (typeof indicator === 'string') {
                      return {
                        indicatorKey: indicator,
                        value: indicator
                      };
                    }
                    return {
                      indicatorKey: indicator.indicatorKey,
                      value: indicator.title
                    };
                  })
                : []
          };

          map.set(flatKey, item); // 存储路径对应的节点
          if (node) {
            node.children.push(item);
          } else {
            if (showGrandTotalsOnTop && isGrandTotal) {
              result.unshift(item);
            } else {
              result.push(item);
            }
          }
        }
        node = item; // 更新当前节点
      });
    }

    arr.forEach(item => addList(item, false));
    ...
    
    return result;
}    

查找

PivotTable可以通过 pivotTable.getCellValue 等方法获取从dataset模块获取单元格的值,此类方法最终都会调用 dataset.getAggregator 方法

可以看到是通过 flatRowKey + flatColKey + indicatorIndex直接在维度tree对象读取,非常方便,时间复杂度几乎可以视为 O(1)

getAggregator(
    rowKey: string[] | string = [],
    colKey: string[] | string = [],
    indicator: string,
    considerChangedValue: boolean = true,
    indicatorPosition?: { position: 'col' | 'row'; index?: number }
  ): IAggregator {
    const indicatorIndex = this.indicatorKeys.indexOf(indicator);

    ...
    
    const agg = this.tree[flatRowKey]?.[flatColKey]?.[indicatorIndex];
    
    return agg
}    

数据更新的情况

  • 增:树形展示场景下,如果需要动态插入子节点的数据,可能会用到setTreeNodeChildren接口 -> 调用 addRecords 接口 -> 触发 processRecord

  • 改:在编辑表格场景下,单元格的值可能会更新,会调用 pivotTable.changeCellValuespivotTable.changeCellValue更改单元格数据

  • 上述方法除了会触发宽高重新计算外,在数据处理上,最后会触发 dataset.changeRecordFieldValue 方法(如下代码)。可以看到会先更新 records;然后再调 this.processRecords() 重新开始遍历 records,重新生成维度tree对象

changeRecordFieldValue(fieldName: string, oldValue: string | number, value: string | number) {
  ...
  
  for (let i = 0, len = this.records.length; i < len; i++) {
      const record = this.records[i];
      if (record[fieldName] === oldValue) {
        **record[fieldName] = value;**
      }
  }
  
  this.rowFlatKeys = {};
  this.colFlatKeys = {};
  this.tree = {};
  **this.processRecords();**
}    

数据分析

需求背景

多维表格的核心功能之一就是数据分析,能帮助用户分析出各种场景指标以及对比,帮助业务分析推动决策。以下是 PivotTable 具备的数据分析能力

需求分析

  • 在上小节「自动组织维度树」中,我们设想的维度tree对象值为IndicatorValue[]类型。为了实现聚合、计算字段的功能,维度tree对象值的数据结构需要重新设计。应该是什么数据结构呢?如何实现遍历 **Records** 的时候边统计这些聚合数据呢?这其实是这一小节的核心设计之一
// 我们之前设想据维度 tree 对象
const tree = {
    '3-1-3': {
        '线上-淘宝-淘宝旗舰店': [
            {
                indicatorKey: "origin_price",
                value: '4299'       
            }
            {
                indicatorKey: "curr_price",
                value: '3999'       
            }
        ]
    }
}    

  • 过滤、派生字段功能在遍历Records之前就能实现。根据约定的过滤规则,先将不需要的数据从Records剔除,不影响后面的其他计算
计算字段和派生字段有什么区别?两张都是根据原始数据衍生算出的数据。 派生字段:根据原始数据衍生算出的**维度**。eg. 有维度 date 字段,值为“2025-02-03”,期望衍生出维度 year、month、week、day 计算字段:根据原始数据衍生算出的**指标**。eg. 有指标“原价”和“实际价格”,期望衍生出指标“优惠力度”
1. 汇总是多维表格中很常用的功能。可能会在**遍历**`**Records**`**之后实现**,因为需要聚合、计算字段这些有值之后,我们才能做汇总

根据上面的分析,为了实现数据分析功能,数据解析流程可能会变更如下:

源码 & 实现

带着上述分析和疑问,我们一起看下源码是怎么实现的

代码入口:`packages/vtable/src/dataset/dataset.ts` 下面的代码经过简化处理

这些逻辑基本还是分布在 setRecordsprocessRecordsprocessRecord 这三个方法中

  • setRecords:数据处理的入口方法

  • processRecords:处理数据,遍历所有条目

  • processRecord:处理单条数据

过滤

源码如下,应该还好理解

export class Dataset {
    // 过滤规则
    filterRules?: FilterRules;
    
    // 明细数据
    records?: any[] | Record<string, any[]>;
    filteredRecords?: any[] | Record<string, any[]>;
    
    // 处理数据, 遍历所有条目
    private processRecords() {
        let isNeedFilter = false;
        if ((this.filterRules?.length ?? 0) >= 1) {
          isNeedFilter = true;
        }
        
        for (let i = 0, len = this.records.length; i < len; i++) {
            const record = this.records[i];
            // 如果 this.filterRecord(record) 为false,这条原始数据就被过滤掉了,不进入后面的数据处理流程
            if (!isNeedFilter || **this.filterRecord(record)**) {
                (this.filteredRecords as any[]).push(record);
                this.processRecord(record);
            }
          }
    }
    
    // 遍历过滤规则,有一条命中就会被过滤掉
    private filterRecord(record: any): boolean {
        let isReserved = true;
        if (this.filterRules) {
            for (let i = 0; i < this.filterRules.length; i++) {
                const filterRule = this.filterRules[i];
                if (filterRule.filterKey) {
                    const filterValue = record[filterRule.filterKey];
                    if (filterRule.filteredValues?.indexOf(filterValue) === -1) {
                        isReserved = false;
                        break;
                    }
                } else if (!filterRule.filterFunc?.(record)) {
                    isReserved = false;
                    break;
                }
            }
        }
        return isReserved;
    }
}    

派生字段

export class Dataset {
    // 派生字段规则
    derivedFieldRules?: DerivedFieldRules;
    
    // 处理单条数据
    private processRecord(record: any, assignedIndicatorKey?: string) {
        this.derivedFieldRules?.forEach((derivedFieldRule: DerivedFieldRule, i: number) => {
            if (derivedFieldRule.fieldName && derivedFieldRule.derivedFunc) {
                // 根据派生字段规则的 fieldName 和 函数,生成字段数据,写入 record 中
                record[derivedFieldRule.fieldName] = derivedFieldRule.derivedFunc(record);
            }
        });
    
    }
}    

聚合

Aggregator类
  • 根据不同的聚合类型AggregationType,基于Aggregator类实现各自的聚合类
export enum AggregationType {
  RECORD = 'RECORD',
  NONE = 'NONE', //不做聚合 只获取其中一条数据作为节点的record 取其field
  SUM = 'SUM',
  MIN = 'MIN',
  MAX = 'MAX',
  AVG = 'AVG',
  COUNT = 'COUNT',
  CUSTOM = 'CUSTOM',
  RECALCULATE = 'RECALCULATE' // 计算字段
}

// packages/vtable/src/ts-types/dataset/aggregation.ts
export interface IAggregator {
  records: any[];  // 缓存聚合值的records集合,为后续跟踪提供数据依据
  value: () => any; // 获取聚合值
  push: (record: any) => void; // 将数据记录添加到聚合器中,用于计算聚合值
  deleteRecord: (record: any) => void; // 从聚合器中删除记录,并更新聚合值。eg. 调用vtable的删除接口deleteRecords会调用该接口
  updateRecord: (oldRecord: any, newRecord: any) => void; // 更新数据记录,并更新聚合值。eg. 调用接口updateRecords会调用该接口
  recalculate: () => any; // 重新计算聚合值。eg. 目前复制粘贴单元格值会调用该方法。
  formatValue?: (col?: number, row?: number, table?: BaseTableAPI) => any; // 格式化后的聚合值
  formatFun?: () => any; // 格式化函数
  clearCacheValue: () => any; // 清空缓存值
  reset: () => void; // 重置聚合器
}

export abstract class Aggregator implements IAggregator {
  isAggregator?: boolean = true;
  isRecord?: boolean = true; //是否需要维护records 将数据源都记录下来
  records: any[] = [];
  type?: string;
  key: string;
  field?: string | string[];
  formatFun?: any;
  _formatedValue?: any;

  constructor(config: { key: string; field: string | string[]; formatFun?: any; isRecord?: boolean }) {
    this.key = config.key;
    this.field = config.field;
    this.formatFun = config.formatFun;
    this.isRecord = config.isRecord ?? this.isRecord;
  }
  abstract push(record: any): void;
  abstract deleteRecord(record: any): void;
  abstract updateRecord(oldRecord: any, newRecord: any): void;
  abstract value(): any;
  abstract recalculate(): any;
  clearCacheValue() {
    this._formatedValue = undefined;
  }
  formatValue(col?: number, row?: number, table?: BaseTableAPI) {
     ...
  }
  reset() {
    this.records = [];
    this.clearCacheValue();
  }
}

// 基于 Aggregator 实现各自的聚合类
export class SumAggregator extends Aggregator {
    ...
}
export class CountAggregator extends Aggregator {
    ...
}
...    

  • 维度tree对象的值,其实就是Aggregator[]
tree: Record<string, Record<string, Aggregator[]>> = {};    

我们选AggregationType.SUMAggregationType.RECALCULATE(计算字段)来具体分析下实现流程

SumAggregator

大概流程如下:

// packages/vtable/src/ts-types/dataset/aggregation.ts
export const registeredAggregators: {
  [key: string]: {
    new (args: {
      key?: string;
      field: string | string[];
      aggregationFun?: any;
      formatFun?: any;
      isRecord?: boolean;
      needSplitPositiveAndNegative?: boolean;
      calculateFun?: any;
      dependAggregators?: any;
      dependIndicatorKeys?: string[];
    }): Aggregator;
  };
} = {};

// packages/vtable/src/dataset/dataset.ts
export class Dataset {
    // 聚合规则
    aggregationRules?: AggregationRules;
    
     // 将聚合类型注册收集到 registeredAggregators 对象,方便后面调用
    registerAggregator(type: string, aggregator: any) {
        registeredAggregators[type] = aggregator;
    }
    
    // 将聚合类型注册。 在 constructor 一开始就会执行
    registerAggregators() {
        this.registerAggregator(AggregationType.RECORD, RecordAggregator);
        this.registerAggregator(AggregationType.SUM, SumAggregator);
        this.registerAggregator(AggregationType.COUNT, CountAggregator);
        this.registerAggregator(AggregationType.MAX, MaxAggregator);
        this.registerAggregator(AggregationType.MIN, MinAggregator);
        this.registerAggregator(AggregationType.AVG, AvgAggregator);
        this.registerAggregator(AggregationType.NONE, NoneAggregator);
        this.registerAggregator(AggregationType.RECALCULATE, RecalculateAggregator);
        this.registerAggregator(AggregationType.CUSTOM, CustomAggregator);
    }
    
     
    // 处理单条数据
    private processRecord(record: any, assignedIndicatorKey?: string) {
        ...
        const toComputeIndicatorKeys = this.indicatorKeysIncludeCalculatedFieldDependIndicatorKeys;
        for (let i = 0; i < toComputeIndicatorKeys.length; i++) {
            
            ...
            // aggRule 有可能为空,具体看是什么指标字段
            const aggRule = this.getAggregatorRule(toComputeIndicatorKeys[i]);
            let needAddToAggregator = false;
            
            ...
            
            // 如果这个 indicatorKey 在 record 中,就会触发下面的逻辑
            toComputeIndicatorKeys[i] in record && (needAddToAggregator = true);
            if (!this.tree[flatRowKey]?.[flatColKey]?.[i] && needAddToAggregator) {
                // 若这个 indicatorKey 没指定 aggRule,就默认是 AggregationType.SUM
                // 往维度tree对象的值中添加 Aggregator 实例
                this.tree[flatRowKey][flatColKey][i] = new registeredAggregators[
                    **aggRule?.aggregationType ?? AggregationType.SUM**
                ]({
                    key: toComputeIndicatorKeys[i],
                    field: aggRule?.field ?? toComputeIndicatorKeys[i],
                    aggregationFun: aggRule?.aggregationFun,
                    formatFun:
                        aggRule?.formatFun ??
                        (
                          this.indicators?.find((indicator: string | IIndicator) => {
                              if (typeof indicator !== 'string') {
                                  return indicator.indicatorKey === toComputeIndicatorKeys[i];
                              }
                              return false;
                          }) as IIndicator
                        )?.format
              });
            }
            
            if (needAddToAggregator) {
                // 并调用 Aggregator 实例的 push 方法,往 Aggregator.records 中存原始数据
                this.tree[flatRowKey]?.[flatColKey]?.[i].push(record);
            }
        }
    }
}

// packages/vtable/src/ts-types/dataset/aggregation.ts
export class SumAggregator extends Aggregator {
    type: string = AggregationType.SUM;
    sum = 0;
    ...
    
    push(record: any): void {
        if (record) {
            if (this.isRecord && this.records) {
                if (record.isAggregator) {
                    this.records.push(...record.records);
                } else {
                    this.records.push(record);
            }
        }
        
        ...
        const value = parseFloat(record[this.field]);
        this.sum += value;
        if (this.needSplitPositiveAndNegativeForSum) {
          if (value > 0) {
            this.positiveSum += value;
          } else if (value < 0) {
            this.nagetiveSum += value;
          }
        }
    
        this.clearCacheValue();
    }
    
    // 获取 sum 值
    value() {
        return this.records?.length >= 1 ? this.sum : undefined;
    }
  
  ...
}    

计算字段

计算字段的 Aggregator类RecalculateAggregator。处理流程也和 SumAggregator 的流程很类似

export class Dataset {
    // 计算字段规则
    calculatedFieldRules?: CalculateddFieldRules;
    /** 计算字段 */
    calculatedFiledKeys?: string[];
    calculatedFieldDependIndicatorKeys?: string[];
    
    // 处理单条数据
    private processRecord(record: any, assignedIndicatorKey?: string) {
        ...
        const toComputeIndicatorKeys = this.indicatorKeysIncludeCalculatedFieldDependIndicatorKeys;
        for (let i = 0; i < toComputeIndicatorKeys.length; i++) {
            // 遍历计算字段key
            if (this.calculatedFiledKeys.indexOf(toComputeIndicatorKeys[i]) >= 0) {
                // 找到计算字段对应的计算规则
                const calculatedFieldRule = this.calculatedFieldRules?.find(rule => rule.key === toComputeIndicatorKeys[i]);
                
                if (!this.tree[flatRowKey]?.[flatColKey]?.[i]) {
                    // 往 维度tree 添加新 RECALCULATE Aggregator 实例
                    this.tree[flatRowKey][flatColKey][i] = new registeredAggregators[AggregationType.RECALCULATE]({
                    key: toComputeIndicatorKeys[i],
                    field: toComputeIndicatorKeys[i],
                    isRecord: true,
                    formatFun: (
                      this.indicators?.find((indicator: string | IIndicator) => {
                        if (typeof indicator !== 'string') {
                          return indicator.indicatorKey === toComputeIndicatorKeys[i];
                        }
                        return false;
                      }) as IIndicator
                    )?.format,
                    calculateFun: calculatedFieldRule?.calculateFun,
                    dependAggregators: this.tree[flatRowKey][flatColKey],
                    dependIndicatorKeys: calculatedFieldRule?.dependIndicatorKeys
                  });
                }
                
                // 将依赖的原始数据 record 存进 RECALCULATE Aggregator.records 中
                this.tree[flatRowKey]?.[flatColKey]?.[i].push(record);
            }
        }
    }
}

// packages/vtable/src/ts-types/dataset/aggregation.ts
export class RecalculateAggregator extends Aggregator {
    type: string = AggregationType.RECALCULATE;
    isRecord?: boolean = true;
    declare field?: string;
    calculateFun: Function;
    fieldValue?: any;
    dependAggregators: Aggregator[];
    dependIndicatorKeys: string[];
    
    ...
    
    push(record: any): void {
        if (record && this.isRecord && this.records) {
            if (record.isAggregator) {
                this.records.push(...record.records);
            } else {
                this.records.push(record);
            }
        }
        this.clearCacheValue();
    }
    
    // 获取计算字段的值
    value() {
        if (!this.fieldValue) {
            // 获取依赖的 Aggregator 的值
            const aggregatorValue = _getDependAggregatorValues(this.dependAggregators, this.dependIndicatorKeys);
            // 再用 calculateFun 算出计算字段的值
            this.fieldValue = this.calculateFun?.(aggregatorValue, this.records, this.field);
        }
        return this.fieldValue;
    }
}    

汇总

这个处理相当麻烦 ...

export class Dataset {
    // 汇总配置
    totals?: Totals;
    // 全局统计各指标的极值
    indicatorStatistics: { max: Aggregator; min: Aggregator; total: Aggregator }[] = [];
    // 缓存rows对应每个值是否为汇总字段
    private rowsIsTotal: boolean[] = [];
    private colsIsTotal: boolean[] = [];
    private colGrandTotalLabel: string;
    private colSubTotalLabel: string;
    private rowGrandTotalLabel: string;
    private rowSubTotalLabel: string;
    // 记录用户传入的汇总数据
    totalRecordsTree: Record<string, Record<string, Aggregator[]>> = {};
    
    setRecords(records: any[] | Record<string, any[]>) {
        ...
        // 处理汇总. 在 this.processRecords() 之后;在排序之前
        this.totalStatistics();
    }
    
    
    // 汇总小计
    totalStatistics() {
        // 如果 row 或 column有汇总配置
        if (...) {
            const rowTotalKeys: string[] = [];
            
            // 遍历维度 tree 中的每个行维度、列维度
            Object.keys(that.tree).forEach(flatRowKey => {
                const rowKey = flatRowKey.split(this.stringJoinChar);
                Object.keys(that.tree[flatRowKey]).forEach(flatColKey => {
                    // 如果 row 有小计
                    if (...) {
                        for (let i = 0, len = that.totals?.row?.subTotalsDimensions?.length; i < len; i++) {
                            // 取有小计配置的 row 维度
                            const dimension = that.totals.row.subTotalsDimensions[i];
                            const dimensionIndex = that.rows.indexOf(dimension);
                            
                            const rowTotalKey = rowKey.slice(0, dimensionIndex + 1);
                            if (this.rowHierarchyType !== 'tree') {
                                // 如果是tree的情况则不追加小计单元格值
                                rowTotalKey.push(that.rowSubTotalLabel);
                            }
                            
                            if (!this.tree[flatRowTotalKey]) {
                                this.tree[flatRowTotalKey] = {};
                                rowTotalKeys.push(flatRowTotalKey);
                            }
                            if (!this.tree[flatRowTotalKey][flatColKey]) {
                                this.tree[flatRowTotalKey][flatColKey] = [];
                            }
                            
                            // 和之前处理聚合的逻辑类似
                            // 会往维度tree该行列维度中添加 Aggreator 实例
                            const toComputeIndicatorKeys = this.indicatorKeysIncludeCalculatedFieldDependIndicatorKeys;
                            for (let i = 0; i < toComputeIndicatorKeys.length; i++) {
                                if (!this.tree[flatRowTotalKey][flatColKey][i]) {
                                    if (this.calculatedFiledKeys.indexOf(toComputeIndicatorKeys[i]) >= 0) {
                                    
                                        ...
                                        const aggRule = this.getAggregatorRule(toComputeIndicatorKeys[i]);
                                        this.tree[flatRowTotalKey][flatColKey][i] = new registeredAggregators[                                            aggRule?.aggregationType ?? AggregationType.SUM                                        ]({
                                            key: toComputeIndicatorKeys[i],
                                            field: aggRule?.field ?? toComputeIndicatorKeys[i],
                                            formatFun:
                                              aggRule?.formatFun ??
                                              (
                                                this.indicators?.find((indicator: string | IIndicator) => {
                                                  if (typeof indicator !== 'string') {
                                                    return indicator.indicatorKey === toComputeIndicatorKeys[i];
                                                  }
                                                  return false;
                                                }) as IIndicator
                                              )?.format
                                        });
                                    }
                                 }
                                 // 这一步有点意思,会往维度tree中 flatRowTotalKey 维度的 aggreator 添加所有flatRowKey下的 aggreator
                                 if (flatRowTotalKey !== flatRowKey) {
                                    this.tree[flatRowTotalKey][flatColKey][i].push(that.tree[flatRowKey]?.[flatColKey]?.[i]);
                                 }
                            }
                        }
                    }
                    
                    // 如果 row 有总计配置, 也做类似的处理
                    if (that.totals?.row?.showGrandTotals || this.columns.length === 0) {
                        ...
                    }
                    
                    colCompute(flatRowKey, flatColKey);
                })
                
                // 增加出来的rowTotalKeys 再遍历一次 汇总小计的小计 如 东北小计(row)-办公用品小计(col)所指单元格的值
                rowTotalKeys.forEach(flatRowKey => {
                    Object.keys(that.tree[flatRowKey]).forEach(flatColKey => {
                        // 计算每一行的所有列的汇总值
                        colCompute(flatRowKey, flatColKey);
                    })
                })
                
            })
            
            for (const flatRowKey in that.totalRecordsTree) {
                for (const flatColKey in that.totalRecordsTree[flatRowKey]) {
                    // 计算每一行的所有列的汇总值
                    colCompute(flatRowKey, flatColKey);
                }
            }
        }
    }
}      

排序

colKeysrowKeys分别排序

export class Dataset {
    // 排序规则
    sortRules?: SortRules;
    
    colKeys: string[][] = [];
    rowKeys: string[][] = [];
    // 存储下未排序即初始normal下rowKeys和colKeys
    colKeys_normal: string[][] = [];
    rowKeys_normal: string[][] = [];
    
    setRecords(records: any[] | Record<string, any[]>) {
        this.processRecords(); // 这里收集了维度成员
        
        ...
        this.rowKeys_normal = this.rowKeys.slice();
        this.colKeys_normal = this.colKeys.slice();
        
        this.sortKeys();
    }
    
    // 根据排序规则 对维度keys排序
    sortKeys() {
        this.colKeys = this.colKeys_normal.slice();
        this.rowKeys = this.rowKeys_normal.slice();
        if (!this.sorted) {
            this.sorted = true;
            
            // 排序
            this.rowKeys.sort(this.arrSort(this.rows, true));
            const sortfun = this.arrSort(this.columns, false);
            this.colKeys.sort(sortfun);
        }
    }
    
    // 综合配置的多条排序规则,生成生成排序函数
    arrSort(fieldArr: string[], isRow: boolean) {
        ...
    }
}    

数据解析流程最终版

核心是维度tree对象Aggregator类的设计

相关资料

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

玄魂