ag-grid-enterprise
Version:
ag-Grid Enterprise Features
336 lines (279 loc) • 14.8 kB
text/typescript
import {
Autowired,
Bean,
ColDef,
ColGroupDef,
Column,
ColumnController,
GridOptionsWrapper,
NumberSequence,
Utils
} from "ag-grid-community";
export interface PivotColDefServiceResult {
pivotColumnGroupDefs: (ColDef | ColGroupDef)[];
pivotColumnDefs: ColDef[];
}
export class PivotColDefService {
private columnController: ColumnController;
private gridOptionsWrapper: GridOptionsWrapper;
public createPivotColumnDefs(uniqueValues: any): PivotColDefServiceResult {
// this is passed to the columnController, to configure the columns and groups we show
let pivotColumnGroupDefs: (ColDef | ColGroupDef)[] = [];
// this is used by the aggregation stage, to do the aggregation based on the pivot columns
let pivotColumnDefs: ColDef[] = [];
let pivotColumns = this.columnController.getPivotColumns();
let valueColumns = this.columnController.getValueColumns();
let levelsDeep = pivotColumns.length;
let columnIdSequence = new NumberSequence();
this.recursivelyAddGroup(pivotColumnGroupDefs, pivotColumnDefs, 1, uniqueValues, [], columnIdSequence, levelsDeep, pivotColumns);
this.addRowGroupTotals(pivotColumnGroupDefs, pivotColumnDefs, valueColumns, pivotColumns, columnIdSequence);
this.addPivotTotalsToGroups(pivotColumnGroupDefs, pivotColumnDefs, columnIdSequence);
// we clone, so the colDefs in pivotColumnsGroupDefs and pivotColumnDefs are not shared. this is so that
// any changes the user makes (via processSecondaryColumnDefinitions) don't impact the internal aggregations,
// as these use the col defs also
let pivotColumnDefsClone: ColDef[] = pivotColumnDefs.map(colDef => Utils.cloneObject(colDef));
return {
pivotColumnGroupDefs: pivotColumnGroupDefs,
pivotColumnDefs: pivotColumnDefsClone
};
}
// parentChildren - the list of colDefs we are adding to
// @index - how far the column is from the top (also same as pivotKeys.length)
// @uniqueValues - the values for which we should create a col for
// @pivotKeys - the keys for the pivot, eg if pivoting on {Language,Country} then could be {English,Ireland}
private recursivelyAddGroup(parentChildren: (ColGroupDef | ColDef)[],
pivotColumnDefs: ColDef[],
index: number,
uniqueValues: any,
pivotKeys: string[],
columnIdSequence: NumberSequence,
levelsDeep: number,
primaryPivotColumns: Column[]): void {
Utils.iterateObject(uniqueValues, (key: string, value: any) => {
let newPivotKeys = pivotKeys.slice(0);
newPivotKeys.push(key);
let createGroup = index !== levelsDeep;
if (createGroup) {
const groupDef: ColGroupDef = {
children: [],
headerName: key,
pivotKeys: newPivotKeys,
columnGroupShow: 'open',
groupId: 'pivot' + columnIdSequence.next()
};
parentChildren.push(groupDef);
this.recursivelyAddGroup(groupDef.children, pivotColumnDefs, index + 1, value, newPivotKeys, columnIdSequence, levelsDeep, primaryPivotColumns);
} else {
const measureColumns = this.columnController.getValueColumns();
const valueGroup: ColGroupDef = {
children: [],
headerName: key,
pivotKeys: newPivotKeys,
columnGroupShow: 'open',
groupId: 'pivot' + columnIdSequence.next()
};
// if no value columns selected, then we insert one blank column, so the user at least sees columns
// rendered. otherwise the grid would render with no columns (just empty groups) which would give the
// impression that the grid is broken
if (measureColumns.length === 0) {
// this is the blank column, for when no value columns enabled.
const colDef = this.createColDef(null, '-', newPivotKeys, columnIdSequence);
valueGroup.children.push(colDef);
pivotColumnDefs.push(colDef);
} else {
measureColumns.forEach(measureColumn => {
const columnName: string = this.columnController.getDisplayNameForColumn(measureColumn, 'header');
const colDef = this.createColDef(measureColumn, columnName, newPivotKeys, columnIdSequence);
colDef.columnGroupShow = 'open';
valueGroup.children.push(colDef);
pivotColumnDefs.push(colDef);
});
}
parentChildren.push(valueGroup);
}
});
// sort by either user provided comparator, or our own one
const colDef = primaryPivotColumns[index - 1].getColDef();
const userComparator = colDef.pivotComparator;
const comparator = this.headerNameComparator.bind(this, userComparator);
parentChildren.sort(comparator);
}
private addPivotTotalsToGroups(pivotColumnGroupDefs: (ColDef | ColGroupDef)[],
pivotColumnDefs: ColDef[],
columnIdSequence: NumberSequence) {
if (!this.gridOptionsWrapper.getPivotColumnGroupTotals()) return;
let insertAfter = this.gridOptionsWrapper.getPivotColumnGroupTotals() === 'after';
let valueCols = this.columnController.getValueColumns();
let aggFuncs = valueCols.map(valueCol => valueCol.getAggFunc());
// don't add pivot totals if there is less than 1 aggFunc or they are not all the same
if (!aggFuncs || aggFuncs.length < 1 || !this.sameAggFuncs(aggFuncs)) {
// console.warn('ag-Grid: aborting adding pivot total columns - value columns require same aggFunc');
return;
}
// arbitrarily select a value column to use as a template for pivot columns
let valueColumn = valueCols[0];
pivotColumnGroupDefs.forEach((groupDef: (ColGroupDef | ColDef)) => {
this.recursivelyAddPivotTotal(groupDef, pivotColumnDefs, columnIdSequence, valueColumn, insertAfter);
});
}
private recursivelyAddPivotTotal(groupDef: (ColGroupDef | ColDef),
pivotColumnDefs: ColDef[],
columnIdSequence: NumberSequence,
valueColumn: Column,
insertAfter: boolean): string[] | null {
let group = <ColGroupDef>groupDef;
if (!group.children) {
const def: ColDef = <ColDef>groupDef;
return def.colId ? [def.colId] : null;
}
let colIds: string[] = [];
// need to recurse children first to obtain colIds used in the aggregation stage
group.children
.forEach((grp: ColGroupDef) => {
let childColIds = this.recursivelyAddPivotTotal(grp, pivotColumnDefs, columnIdSequence, valueColumn, insertAfter);
if (childColIds) {
colIds = colIds.concat(childColIds);
}
});
// only add total colDef if there is more than 1 child node
if (group.children.length > 1) {
//create total colDef using an arbitrary value column as a template
let totalColDef = this.createColDef(valueColumn, 'Total', groupDef.pivotKeys, columnIdSequence);
totalColDef.pivotTotalColumnIds = colIds;
totalColDef.aggFunc = valueColumn.getAggFunc();
// add total colDef to group and pivot colDefs array
let children = (<ColGroupDef>groupDef).children;
insertAfter ? children.push(totalColDef) : children.unshift(totalColDef);
pivotColumnDefs.push(totalColDef);
}
return colIds;
}
private addRowGroupTotals(pivotColumnGroupDefs: (ColDef | ColGroupDef)[],
pivotColumnDefs: ColDef[],
valueColumns: Column[],
pivotColumns: Column[],
columnIdSequence: NumberSequence) {
if (!this.gridOptionsWrapper.getPivotRowTotals()) return;
let insertAfter = this.gridOptionsWrapper.getPivotRowTotals() === 'after';
// order of row group totals depends on position
let valueCols = insertAfter ? valueColumns.slice() : valueColumns.slice().reverse();
for (let i = 0; i < valueCols.length; i++) {
let valueCol = valueCols[i];
let colIds: any[] = [];
pivotColumnGroupDefs.forEach((groupDef: (ColGroupDef | ColDef)) => {
colIds = colIds.concat(this.extractColIdsForValueColumn(groupDef, valueCol));
});
let levelsDeep = pivotColumns.length;
this.createRowGroupTotal(pivotColumnGroupDefs, pivotColumnDefs, 1, [], columnIdSequence, levelsDeep, pivotColumns, valueCol, colIds, insertAfter);
}
}
private extractColIdsForValueColumn(groupDef: (ColGroupDef | ColDef), valueColumn: Column): string[] {
let group = <ColGroupDef>groupDef;
if (!group.children) {
let colDef = (<ColDef>group);
return colDef.pivotValueColumn === valueColumn && colDef.colId ? [colDef.colId] : [];
}
let colIds: string[] = [];
group.children
.forEach((grp: ColGroupDef) => {
this.extractColIdsForValueColumn(grp, valueColumn);
let childColIds = this.extractColIdsForValueColumn(grp, valueColumn);
colIds = colIds.concat(childColIds);
});
return colIds;
}
private createRowGroupTotal(parentChildren: (ColGroupDef | ColDef)[],
pivotColumnDefs: ColDef[],
index: number,
pivotKeys: string[],
columnIdSequence: NumberSequence,
levelsDeep: number,
primaryPivotColumns: Column[],
valueColumn: Column,
colIds: string[],
insertAfter: boolean): void {
let newPivotKeys = pivotKeys.slice(0);
let createGroup = index !== levelsDeep;
if (createGroup) {
let groupDef: ColGroupDef = {
children: [],
pivotKeys: newPivotKeys,
groupId: 'pivot' + columnIdSequence.next()
};
insertAfter ? parentChildren.push(groupDef) : parentChildren.unshift(groupDef);
this.createRowGroupTotal(groupDef.children, pivotColumnDefs, index + 1, newPivotKeys, columnIdSequence, levelsDeep, primaryPivotColumns, valueColumn, colIds, insertAfter);
} else {
let measureColumns = this.columnController.getValueColumns();
let valueGroup: ColGroupDef = {
children: [],
pivotKeys: newPivotKeys,
groupId: 'pivot' + columnIdSequence.next()
};
if (measureColumns.length === 0) {
let colDef = this.createColDef(null, '-', newPivotKeys, columnIdSequence);
valueGroup.children.push(colDef);
pivotColumnDefs.push(colDef);
} else {
let columnName: string = this.columnController.getDisplayNameForColumn(valueColumn, 'header');
let colDef = this.createColDef(valueColumn, columnName, newPivotKeys, columnIdSequence);
colDef.pivotTotalColumnIds = colIds;
valueGroup.children.push(colDef);
pivotColumnDefs.push(colDef);
}
insertAfter ? parentChildren.push(valueGroup) : parentChildren.unshift(valueGroup);
}
}
private createColDef(valueColumn: Column | null, headerName: any, pivotKeys: string[] | undefined, columnIdSequence: NumberSequence): ColDef {
let colDef: ColDef = {};
if (valueColumn) {
let colDefToCopy = valueColumn.getColDef();
Utils.assign(colDef, colDefToCopy);
// even if original column was hidden, we always show the pivot value column, otherwise it would be
// very confusing for people thinking the pivot is broken
colDef.hide = false;
}
colDef.headerName = headerName;
colDef.colId = 'pivot_' + columnIdSequence.next();
// pivot columns repeat over field, so it makes sense to use the unique id instead. For example if you want to
// assign values to pinned bottom rows using setPinnedBottomRowData the value service will use this colId.
colDef.field = colDef.colId;
colDef.pivotKeys = pivotKeys;
colDef.pivotValueColumn = valueColumn;
colDef.suppressFilter = true;
return colDef;
}
private sameAggFuncs(aggFuncs: any[]) {
if (aggFuncs.length == 1) return true;
//check if all aggFunc's match
for (let i = 1; i < aggFuncs.length; i++) {
if (aggFuncs[i] !== aggFuncs[0]) return false;
}
return true;
}
private headerNameComparator(userComparator: (a: string | undefined, b: string | undefined) => number, a: ColGroupDef | ColDef, b: ColGroupDef | ColDef): number {
if (userComparator) {
return userComparator(a.headerName, b.headerName);
} else {
if (a.headerName && !b.headerName) {
return 1;
} else if (!a.headerName && b.headerName) {
return -1;
}
// slightly naff here - just to satify typescript
// really should be &&, but if so ts complains
// the above if/else checks would deal with either being falsy, so at this stage if either are falsy, both are
// ..still naff though
if (!a.headerName || !b.headerName) {
return 0;
}
if (a.headerName < b.headerName) {
return -1;
} else if (a.headerName > b.headerName) {
return 1;
} else {
return 0;
}
}
}
}