igniteui-angular-sovn
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
419 lines (379 loc) • 18.7 kB
text/typescript
import { CurrentResourceStrings } from '../../core/i18n/resources';
import { IDataCloneStrategy } from '../../data-operations/data-clone-strategy';
import { DataUtil, GridColumnDataType } from '../../data-operations/data-util';
import { FilteringLogic } from '../../data-operations/filtering-expression.interface';
import { FilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree';
import { ISortingExpression } from '../../data-operations/sorting-strategy';
import { PivotGridType } from '../common/grid.interface';
import { IGridSortingStrategy, IgxSorting } from '../common/strategy';
import { IgxPivotAggregate, IgxPivotDateAggregate, IgxPivotNumericAggregate, IgxPivotTimeAggregate } from './pivot-grid-aggregate';
import { IPivotAggregator, IPivotConfiguration, IPivotDimension, IPivotGridRecord, IPivotKeys, IPivotValue, PivotDimensionType } from './pivot-grid.interface';
export class PivotUtil {
// go through all children and apply new dimension groups as child
public static processGroups(recs: IPivotGridRecord[], dimension: IPivotDimension, pivotKeys: IPivotKeys, cloneStrategy: IDataCloneStrategy) {
for (const rec of recs) {
// process existing children
if (rec.children && rec.children.size > 0) {
// process hierarchy in dept
rec.children.forEach((values) => {
this.processGroups(values, dimension, pivotKeys, cloneStrategy);
});
}
// add children for current dimension
const hierarchyFields = PivotUtil
.getFieldsHierarchy(rec.records, [dimension], PivotDimensionType.Row, pivotKeys, cloneStrategy);
const siblingData = PivotUtil
.processHierarchy(hierarchyFields, pivotKeys, 0);
rec.children.set(dimension.memberName, siblingData);
}
}
public static flattenGroups(data: IPivotGridRecord[], dimension: IPivotDimension, expansionStates, defaultExpand: boolean, parent?: IPivotDimension, parentRec?: IPivotGridRecord) {
for (let i = 0; i < data.length; i++) {
const rec = data[i];
const field = dimension.memberName;
if (!field) {
continue;
}
let recordsData = rec.children.get(field);
if (!recordsData && parent) {
// check parent
recordsData = rec.children.get(parent.memberName);
if (recordsData) {
dimension = parent;
}
}
if (parentRec) {
parentRec.dimensionValues.forEach((value, key) => {
if (parent.memberName !== key) {
rec.dimensionValues.set(key, value);
const dim = parentRec.dimensions.find(x => x.memberName === key);
rec.dimensions.unshift(dim);
}
});
}
const expansionRowKey = PivotUtil.getRecordKey(rec, dimension);
const isExpanded = expansionStates.get(expansionRowKey) === undefined ?
defaultExpand :
expansionStates.get(expansionRowKey);
const shouldExpand = isExpanded || !dimension.childLevel || !rec.dimensionValues.get(dimension.memberName);
if (shouldExpand && recordsData) {
if (dimension.childLevel) {
this.flattenGroups(recordsData, dimension.childLevel, expansionStates, defaultExpand, dimension, rec);
} else {
// copy parent values and dims in child
recordsData.forEach(x => {
rec.dimensionValues.forEach((value, key) => {
if (dimension.memberName !== key) {
x.dimensionValues.set(key, value);
const dim = rec.dimensions.find(y => y.memberName === key);
x.dimensions.unshift(dim);
}
});
});
}
data.splice(i + 1, 0, ...recordsData);
i += recordsData.length;
}
}
}
public static assignLevels(dims) {
for (const dim of dims) {
let currDim = dim;
let lvl = 0;
while (currDim.childLevel) {
currDim.level = lvl;
currDim = currDim.childLevel;
lvl++;
}
currDim.level = lvl;
}
}
public static getFieldsHierarchy(data: any[], dimensions: IPivotDimension[],
dimensionType: PivotDimensionType, pivotKeys: IPivotKeys, cloneStrategy: IDataCloneStrategy): Map<string, any> {
const hierarchy = new Map<string, any>();
for (const rec of data) {
const vals = dimensionType === PivotDimensionType.Column ?
this.extractValuesForColumn(dimensions, rec, pivotKeys) :
this.extractValuesForRow(dimensions, rec, pivotKeys, cloneStrategy);
for (const [_key, val] of vals) { // this should go in depth also vals.children
if (hierarchy.get(val.value) != null) {
this.applyHierarchyChildren(hierarchy, val, rec, pivotKeys);
} else {
hierarchy.set(val.value, cloneStrategy.clone(val));
this.applyHierarchyChildren(hierarchy, val, rec, pivotKeys);
}
}
}
return hierarchy;
}
public static sort(data: IPivotGridRecord[], expressions: ISortingExpression[], sorting: IGridSortingStrategy = new IgxSorting()): any[] {
data.forEach(rec => {
const children = rec.children;
if (children) {
children.forEach(x => {
this.sort(x, expressions, sorting);
});
}
});
return DataUtil.sort(data, expressions, sorting);
}
public static extractValueFromDimension(dim: IPivotDimension, recData: any) {
return dim.memberFunction ? dim.memberFunction.call(null, recData) : recData[dim.memberName];
}
public static getDimensionDepth(dim: IPivotDimension): number {
let lvl = 0;
while (dim.childLevel) {
lvl++;
dim = dim.childLevel;
}
return lvl;
}
public static extractValuesForRow(dims: IPivotDimension[], recData: any, pivotKeys: IPivotKeys, cloneStrategy: IDataCloneStrategy) {
const values = new Map<string, any>();
for (const col of dims) {
if (recData[pivotKeys.level] && recData[pivotKeys.level] > 0) {
const childData = recData[pivotKeys.records];
return this.getFieldsHierarchy(childData, [col], PivotDimensionType.Row, pivotKeys, cloneStrategy);
}
const value = this.extractValueFromDimension(col, recData);
const objValue = {};
objValue['value'] = value;
objValue['dimension'] = col;
if (col.childLevel) {
const childValues = this.extractValuesForRow([col.childLevel], recData, pivotKeys, cloneStrategy);
objValue[pivotKeys.children] = childValues;
}
values.set(value, objValue);
}
return values;
}
public static extractValuesForColumn(dims: IPivotDimension[], recData: any, pivotKeys: IPivotKeys, path = []) {
const vals = new Map<string, any>();
let lvlCollection = vals;
const flattenedDims = this.flatten(dims);
for (const col of flattenedDims) {
const value = this.extractValueFromDimension(col, recData);
path.push(value);
const newValue = path.join(pivotKeys.columnDimensionSeparator);
const newObj = { value: newValue, expandable: col.expandable, children: null, dimension: col };
if (!newObj.children) {
newObj.children = new Map<string, any>();
}
lvlCollection.set(newValue, newObj);
lvlCollection = newObj.children;
}
return vals;
}
public static flatten(arr, lvl = 0) {
const newArr = arr.reduce((acc, item) => {
item.level = lvl;
acc.push(item);
if (item.childLevel) {
item.expandable = true;
acc = acc.concat(this.flatten([item.childLevel], lvl + 1));
}
return acc;
}, []);
return newArr;
}
public static applyAggregations(rec: IPivotGridRecord, hierarchies, values, pivotKeys: IPivotKeys) {
if (hierarchies.size === 0) {
// no column groups
const aggregationResult = this.aggregate(rec.records, values);
this.applyAggregationRecordData(aggregationResult, undefined, rec, pivotKeys);
return;
}
hierarchies.forEach((hierarchy) => {
const children = hierarchy[pivotKeys.children];
if (children && children.size > 0) {
this.applyAggregations(rec, children, values, pivotKeys);
const childRecords = this.collectRecords(children, pivotKeys);
hierarchy[pivotKeys.aggregations] = this.aggregate(childRecords, values);
this.applyAggregationRecordData(hierarchy[pivotKeys.aggregations], hierarchy.value, rec, pivotKeys);
} else if (hierarchy[pivotKeys.records]) {
hierarchy[pivotKeys.aggregations] = this.aggregate(hierarchy[pivotKeys.records], values);
this.applyAggregationRecordData(hierarchy[pivotKeys.aggregations], hierarchy.value, rec, pivotKeys);
}
});
}
protected static applyAggregationRecordData(aggregationData: any, groupName: string, rec: IPivotGridRecord, pivotKeys: IPivotKeys) {
const aggregationKeys = Object.keys(aggregationData);
if (aggregationKeys.length > 1) {
aggregationKeys.forEach((key) => {
const aggregationKey = groupName ? groupName + pivotKeys.columnDimensionSeparator + key : key;
rec.aggregationValues.set(aggregationKey, aggregationData[key]);
});
} else if (aggregationKeys.length === 1) {
const aggregationKey = aggregationKeys[0];
rec.aggregationValues.set(groupName || aggregationKey, aggregationData[aggregationKey]);
}
}
public static aggregate(records, values: IPivotValue[]) {
const result = {};
for (const pivotValue of values) {
const aggregator = PivotUtil.getAggregatorForType(pivotValue.aggregate, pivotValue.dataType);
if (!aggregator) {
throw CurrentResourceStrings.GridResStrings.igx_grid_pivot_no_aggregator.replace("{0}", pivotValue.member);
}
result[pivotValue.member] = aggregator(records.map(r => r[pivotValue.member]), records);
}
return result;
}
public static getAggregatorForType(aggregate: IPivotAggregator, dataType: GridColumnDataType) {
let aggregator = aggregate.aggregator;
if (aggregate.aggregatorName) {
let aggregators = IgxPivotNumericAggregate.aggregators();
if (!dataType || dataType === 'date' || dataType === 'dateTime') {
aggregators = aggregators.concat(IgxPivotDateAggregate.aggregators())
} else if (dataType === 'time') {
aggregators = aggregators.concat(IgxPivotTimeAggregate.aggregators());
}
aggregator = aggregators.find(x => x.key === aggregate.aggregatorName)?.aggregator;
}
return aggregator;
}
public static processHierarchy(hierarchies, pivotKeys, level = 0, rootData = false): IPivotGridRecord[] {
const flatData: IPivotGridRecord[] = [];
hierarchies.forEach((h, key) => {
const field = h.dimension.memberName;
const rec: IPivotGridRecord = {
dimensionValues: new Map<string, string>(),
aggregationValues: new Map<string, string>(),
children: new Map<string, IPivotGridRecord[]>(),
dimensions: [h.dimension]
};
rec.dimensionValues.set(field, key);
if (h[pivotKeys.records]) {
rec.records = this.getDirectLeafs(h[pivotKeys.records]);
}
rec.level = level;
flatData.push(rec);
if (h[pivotKeys.children] && h[pivotKeys.children].size > 0) {
const nestedData = this.processHierarchy(h[pivotKeys.children],
pivotKeys, level + 1, rootData);
rec.records = this.getDirectLeafs(nestedData);
rec.children.set(field, nestedData);
}
});
return flatData;
}
public static getDirectLeafs(records: IPivotGridRecord[]) {
let leafs = [];
for (const rec of records) {
if (rec.records) {
const data = rec.records.filter(x => !x.records && leafs.indexOf(x) === -1);
leafs = leafs.concat(data);
} else {
leafs.push(rec);
}
}
return leafs;
}
public static getRecordKey(rec: IPivotGridRecord, currentDim: IPivotDimension,) {
const parentFields = [];
const currentDimIndex = rec.dimensions.findIndex(x => x.memberName === currentDim.memberName) + 1;
const prevDims = rec.dimensions.slice(0, currentDimIndex);
for (const prev of prevDims) {
const prevValue = rec.dimensionValues.get(prev.memberName);
parentFields.push(prevValue);
}
return parentFields.join('-');
}
public static buildExpressionTree(config: IPivotConfiguration) {
const allDimensions = (config?.rows || []).concat((config?.columns || [])).concat(config?.filters || []).filter(x => x !== null && x !== undefined);
const enabledDimensions = allDimensions.filter(x => x && x.enabled);
const expressionsTree = new FilteringExpressionsTree(FilteringLogic.And);
// add expression trees from all filters
PivotUtil.flatten(enabledDimensions).forEach((x: IPivotDimension) => {
if (x.filter && x.filter.filteringOperands) {
expressionsTree.filteringOperands.push(...x.filter.filteringOperands);
}
});
return expressionsTree;
}
private static collectRecords(children, pivotKeys: IPivotKeys) {
let result = [];
children.forEach(value => result = result.concat(value[pivotKeys.records]));
return result;
}
private static applyHierarchyChildren(hierarchy, val, rec, pivotKeys: IPivotKeys) {
const recordsKey = pivotKeys.records;
const childKey = pivotKeys.children;
const childCollection = val[childKey];
const hierarchyValue = hierarchy.get(val.value);
if (Array.isArray(hierarchyValue[childKey])) {
hierarchyValue[childKey] = new Map<string, any>();
}
if (!childCollection || childCollection.size === 0) {
const dim = hierarchyValue.dimension;
const isValid = this.extractValueFromDimension(dim, rec) === val.value;
if (isValid) {
if (hierarchyValue[recordsKey]) {
hierarchyValue[recordsKey].push(rec);
} else {
hierarchyValue[recordsKey] = [rec];
}
}
} else {
const hierarchyChild = hierarchyValue[childKey];
for (const [_key, child] of childCollection) {
let hierarchyChildValue = hierarchyChild.get(child.value);
if (!hierarchyChildValue) {
hierarchyChild.set(child.value, child);
hierarchyChildValue = child;
}
if (hierarchyChildValue[recordsKey]) {
const copy = Object.assign({}, rec);
if (rec[recordsKey]) {
// not all nested children are valid
const nestedValue = hierarchyChildValue.value;
const dimension = hierarchyChildValue.dimension;
const validRecs = rec[recordsKey].filter(x => this.extractValueFromDimension(dimension, x) === nestedValue);
copy[recordsKey] = validRecs;
}
hierarchyChildValue[recordsKey].push(copy);
} else {
hierarchyChildValue[recordsKey] = [rec];
}
if (child[childKey] && child[childKey].size > 0) {
this.applyHierarchyChildren(hierarchyChild, child, rec, pivotKeys);
}
}
}
}
public static getAggregateList(val: IPivotValue, grid: PivotGridType): IPivotAggregator[] {
if (!val.aggregateList) {
let defaultAggr = this.getAggregatorsForValue(val, grid);
const isDefault = defaultAggr.find(
(x) => x.key === val.aggregate.key
);
// resolve custom aggregations
if (!isDefault && grid.data[0][val.member] !== undefined) {
// if field exists, then we can apply default aggregations and add the custom one.
defaultAggr.unshift(val.aggregate);
} else if (!isDefault) {
// otherwise this is a custom aggregation that is not compatible
// with the defaults, since it operates on field that is not in the data
// leave only the custom one.
defaultAggr = [val.aggregate];
}
val.aggregateList = defaultAggr;
}
return val.aggregateList;
}
public static getAggregatorsForValue(value: IPivotValue, grid: PivotGridType): IPivotAggregator[] {
const dataType = value.dataType || grid.resolveDataTypes(grid.data[0][value.member]);
switch (dataType) {
case GridColumnDataType.Number:
case GridColumnDataType.Currency:
return IgxPivotNumericAggregate.aggregators();
case GridColumnDataType.Date:
case GridColumnDataType.DateTime:
return IgxPivotDateAggregate.aggregators();
case GridColumnDataType.Time:
return IgxPivotTimeAggregate.aggregators();
default:
return IgxPivotAggregate.aggregators();
}
}
}