@lumeer/pivot 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +35 -0
- package/README.md +24 -0
- package/ng-package.json +7 -0
- package/package.json +14 -0
- package/src/lib/directives/lmr-templates.directive.ts +11 -0
- package/src/lib/lmr-pivot-table.component.html +69 -0
- package/src/lib/lmr-pivot-table.component.scss +69 -0
- package/src/lib/lmr-pivot-table.component.ts +108 -0
- package/src/lib/lmr-pivot-table.module.ts +28 -0
- package/src/lib/pipes/contrast-color.pipe.ts +33 -0
- package/src/lib/pipes/pivot-data-empty.pipe.ts +36 -0
- package/src/lib/pipes/pivot-table-value.pipe.ts +32 -0
- package/src/lib/util/lmr-pivot-config.ts +68 -0
- package/src/lib/util/lmr-pivot-constants.ts +13 -0
- package/src/lib/util/lmr-pivot-data.ts +57 -0
- package/src/lib/util/lmr-pivot-table.ts +38 -0
- package/src/lib/util/pivot-data-converter.spec.ts +647 -0
- package/src/lib/util/pivot-data-converter.ts +803 -0
- package/src/lib/util/pivot-table-converter.spec.ts +1045 -0
- package/src/lib/util/pivot-table-converter.ts +1118 -0
- package/src/lib/util/pivot-util.ts +92 -0
- package/src/public-api.ts +11 -0
- package/tsconfig.lib.json +14 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +14 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Lumeer: Modern Data Definition and Processing Platform
|
|
3
|
+
*
|
|
4
|
+
* Copyright (C) since 2017 Lumeer.io, s.r.o. and/or its affiliates.
|
|
5
|
+
*
|
|
6
|
+
* This program is free software: you can redistribute it and/or modify
|
|
7
|
+
* it under the terms of the GNU General Public License as published by
|
|
8
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
* (at your option) any later version.
|
|
10
|
+
*
|
|
11
|
+
* This program is distributed in the hope that it will be useful,
|
|
12
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
* GNU General Public License for more details.
|
|
15
|
+
*
|
|
16
|
+
* You should have received a copy of the GNU General Public License
|
|
17
|
+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
+
*/
|
|
19
|
+
import {
|
|
20
|
+
Constraint,
|
|
21
|
+
ConstraintData,
|
|
22
|
+
DataAggregationType,
|
|
23
|
+
NumberConstraint,
|
|
24
|
+
PercentageConstraint,
|
|
25
|
+
UnknownConstraint,
|
|
26
|
+
aggregateDataValues,
|
|
27
|
+
isValueAggregation, DataResource,
|
|
28
|
+
} from '@lumeer/data-filters';
|
|
29
|
+
import {
|
|
30
|
+
deepObjectCopy,
|
|
31
|
+
isArray,
|
|
32
|
+
isNotNullOrUndefined,
|
|
33
|
+
isNullOrUndefined,
|
|
34
|
+
isNumeric, shadeColor,
|
|
35
|
+
toNumber,
|
|
36
|
+
uniqueValues,
|
|
37
|
+
} from '@lumeer/utils';
|
|
38
|
+
|
|
39
|
+
import {LmrPivotData, LmrPivotDataHeader, LmrPivotStemData} from './lmr-pivot-data';
|
|
40
|
+
import {LmrPivotTable, LmrPivotTableCell} from './lmr-pivot-table';
|
|
41
|
+
import {LmrPivotSort, LmrPivotStrings, LmrPivotValueType} from './lmr-pivot-config';
|
|
42
|
+
import {COLOR_GRAY100, COLOR_GRAY200, COLOR_GRAY300, COLOR_GRAY400, COLOR_GRAY500} from './lmr-pivot-constants';
|
|
43
|
+
|
|
44
|
+
interface HeaderGroupInfo {
|
|
45
|
+
background: string;
|
|
46
|
+
indexes: number[];
|
|
47
|
+
level: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ValueTypeInfo {
|
|
51
|
+
sum?: number;
|
|
52
|
+
sumsRows?: number[];
|
|
53
|
+
sumsColumns?: number[];
|
|
54
|
+
defaultConstraint?: Constraint;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class PivotTableConverter {
|
|
58
|
+
public static readonly emptyClass = 'pivot-empty-cell';
|
|
59
|
+
public static readonly dataClass = 'pivot-data-cell';
|
|
60
|
+
public static readonly groupDataClass = 'pivot-data-group-cell';
|
|
61
|
+
public static readonly rowHeaderClass = 'pivot-row-header-cell';
|
|
62
|
+
public static readonly rowGroupHeaderClass = 'pivot-row-group-header-cell';
|
|
63
|
+
public static readonly columnHeaderClass = 'pivot-column-header-cell';
|
|
64
|
+
public static readonly columnGroupHeaderClass = 'pivot-column-group-header-cell';
|
|
65
|
+
|
|
66
|
+
private readonly groupColors = [COLOR_GRAY100, COLOR_GRAY200, COLOR_GRAY300, COLOR_GRAY400, COLOR_GRAY500];
|
|
67
|
+
|
|
68
|
+
private readonly percentageConstraint = new PercentageConstraint({decimals: 2});
|
|
69
|
+
|
|
70
|
+
private data: LmrPivotStemData;
|
|
71
|
+
private strings: LmrPivotStrings;
|
|
72
|
+
private values: any[][];
|
|
73
|
+
private dataResources: DataResource[][][];
|
|
74
|
+
private constraintData: ConstraintData;
|
|
75
|
+
private rowLevels: number;
|
|
76
|
+
private rowsTransformationArray: number[];
|
|
77
|
+
private columnLevels: number;
|
|
78
|
+
private columnsTransformationArray: number[];
|
|
79
|
+
private valueTypeInfo: ValueTypeInfo[];
|
|
80
|
+
private nonStickyRowIndex: number;
|
|
81
|
+
|
|
82
|
+
public createTables(pivotData: LmrPivotData, strings: LmrPivotStrings): LmrPivotTable[] {
|
|
83
|
+
if (!pivotData) {
|
|
84
|
+
return [{cells: []}];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.strings = strings;
|
|
88
|
+
this.constraintData = pivotData.constraintData;
|
|
89
|
+
|
|
90
|
+
return (pivotData.data || []).map(d => {
|
|
91
|
+
if (this.dataAreEmpty(d)) {
|
|
92
|
+
return {cells: []};
|
|
93
|
+
}
|
|
94
|
+
this.updateData(d);
|
|
95
|
+
return this.transformData();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private dataAreEmpty(data: LmrPivotStemData): boolean {
|
|
100
|
+
return (data.rowHeaders || []).length === 0 && (data.columnHeaders || []).length === 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private updateData(data: LmrPivotStemData) {
|
|
104
|
+
const numberOfSums = Math.max(1, (data.valueTitles || []).length);
|
|
105
|
+
this.valueTypeInfo = getValuesTypeInfo(data.values, data.valueTypes, numberOfSums);
|
|
106
|
+
this.data = preparePivotData(data, this.constraintData, this.valueTypeInfo);
|
|
107
|
+
this.nonStickyRowIndex = this.data.rowSticky?.findIndex(sticky => !sticky) || 0;
|
|
108
|
+
this.values = data.values || [];
|
|
109
|
+
this.dataResources = data.dataResources || [];
|
|
110
|
+
this.rowLevels = (data.rowShowSums || []).length;
|
|
111
|
+
this.columnLevels = (data.columnShowSums || []).length + (data.hasAdditionalColumnLevel ? 1 : 0);
|
|
112
|
+
const hasValue = (data.valueTitles || []).length > 0;
|
|
113
|
+
if ((this.data.rowHeaders || []).length > 0) {
|
|
114
|
+
this.rowsTransformationArray = createTransformationMap(
|
|
115
|
+
this.data.rowHeaders,
|
|
116
|
+
this.rowShowSums,
|
|
117
|
+
this.columnLevels,
|
|
118
|
+
1
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
this.rowsTransformationArray = hasValue ? [this.columnLevels] : [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if ((this.data.columnHeaders || []).length > 0) {
|
|
125
|
+
this.columnsTransformationArray = createTransformationMap(
|
|
126
|
+
this.data.columnHeaders,
|
|
127
|
+
this.columnShowSums,
|
|
128
|
+
this.rowLevels,
|
|
129
|
+
numberOfSums
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
this.columnsTransformationArray = hasValue ? [this.rowLevels] : [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private get rowShowSums(): boolean[] {
|
|
137
|
+
return this.data.rowShowSums;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private get columnShowSums(): boolean[] {
|
|
141
|
+
return this.data.columnShowSums;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private transformData(): LmrPivotTable {
|
|
145
|
+
const cells = this.initCells();
|
|
146
|
+
const rowGroups = this.fillCellsByRows(cells);
|
|
147
|
+
const columnGroups = this.fillCellsByColumns(cells);
|
|
148
|
+
this.fillCellsByGroupIntersection(cells, rowGroups, columnGroups);
|
|
149
|
+
return {cells};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private fillCellsByRows(cells: LmrPivotTableCell[][]): HeaderGroupInfo[] {
|
|
153
|
+
const rowGroups = [];
|
|
154
|
+
this.iterateAndFillCellsByRows(cells, rowGroups, this.data.rowHeaders, this.columnLevels, this.rowShowSums, 0);
|
|
155
|
+
return rowGroups;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private iterateAndFillCellsByRows(
|
|
159
|
+
cells: LmrPivotTableCell[][],
|
|
160
|
+
rowGroupsInfo: HeaderGroupInfo[],
|
|
161
|
+
headers: LmrPivotDataHeader[],
|
|
162
|
+
startIndex: number,
|
|
163
|
+
showSums: boolean[],
|
|
164
|
+
level: number,
|
|
165
|
+
parentHeader?: LmrPivotDataHeader
|
|
166
|
+
) {
|
|
167
|
+
let currentIndex = startIndex;
|
|
168
|
+
for (const header of headers) {
|
|
169
|
+
const rowSpan = getDirectHeaderChildCount(header, level, showSums);
|
|
170
|
+
cells[currentIndex][level] = {
|
|
171
|
+
value: header.title,
|
|
172
|
+
cssClass: PivotTableConverter.rowHeaderClass,
|
|
173
|
+
isHeader: true,
|
|
174
|
+
stickyStart: this.isRowLevelSticky(level),
|
|
175
|
+
rowSpan,
|
|
176
|
+
colSpan: 1,
|
|
177
|
+
background: this.getHeaderBackground(header, level),
|
|
178
|
+
constraint: header.constraint,
|
|
179
|
+
label: header.attributeName,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (header.children) {
|
|
183
|
+
this.iterateAndFillCellsByRows(
|
|
184
|
+
cells,
|
|
185
|
+
rowGroupsInfo,
|
|
186
|
+
header.children,
|
|
187
|
+
currentIndex,
|
|
188
|
+
showSums,
|
|
189
|
+
level + 1,
|
|
190
|
+
header
|
|
191
|
+
);
|
|
192
|
+
} else if (isNotNullOrUndefined(header.targetIndex)) {
|
|
193
|
+
this.fillCellsForRow(cells, header.targetIndex);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
currentIndex += getHeaderChildCount(header, level, showSums);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (showSums[level]) {
|
|
200
|
+
const background = this.getSummaryBackground(level);
|
|
201
|
+
const summary = level === 0 ? this.strings.summaryString : this.strings.headerSummaryString;
|
|
202
|
+
const columnIndex = Math.max(level - 1, 0);
|
|
203
|
+
let colSpan = this.rowLevels - columnIndex;
|
|
204
|
+
const stickyStart = this.isRowLevelSticky(columnIndex);
|
|
205
|
+
|
|
206
|
+
// split row group header cell because of correct sticky scroll
|
|
207
|
+
if (stickyStart && this.nonStickyRowIndex > 0 && colSpan > 1) {
|
|
208
|
+
const newColspan = this.nonStickyRowIndex - columnIndex;
|
|
209
|
+
|
|
210
|
+
cells[currentIndex][this.nonStickyRowIndex] = {
|
|
211
|
+
value: undefined,
|
|
212
|
+
constraint: undefined,
|
|
213
|
+
label: undefined,
|
|
214
|
+
cssClass: PivotTableConverter.rowGroupHeaderClass,
|
|
215
|
+
isHeader: true,
|
|
216
|
+
rowSpan: 1,
|
|
217
|
+
colSpan: colSpan - newColspan,
|
|
218
|
+
background,
|
|
219
|
+
summary: undefined,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
colSpan = newColspan;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
cells[currentIndex][columnIndex] = {
|
|
226
|
+
value: parentHeader?.title,
|
|
227
|
+
constraint: parentHeader?.constraint,
|
|
228
|
+
label: parentHeader?.attributeName,
|
|
229
|
+
cssClass: PivotTableConverter.rowGroupHeaderClass,
|
|
230
|
+
isHeader: true,
|
|
231
|
+
stickyStart,
|
|
232
|
+
rowSpan: 1,
|
|
233
|
+
colSpan,
|
|
234
|
+
background,
|
|
235
|
+
summary,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const rowIndexes = getTargetIndexesForHeaders(headers);
|
|
239
|
+
const transformedRowIndexes = rowIndexes
|
|
240
|
+
.map(v => this.rowsTransformationArray[v])
|
|
241
|
+
.filter(v => isNotNullOrUndefined(v));
|
|
242
|
+
rowGroupsInfo[currentIndex] = {background, indexes: transformedRowIndexes, level};
|
|
243
|
+
|
|
244
|
+
this.fillCellsForGroupedRow(cells, rowIndexes, currentIndex, background);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private getHeaderBackground(header: LmrPivotDataHeader, level: number): string {
|
|
249
|
+
if (header?.color) {
|
|
250
|
+
return shadeColor(header.color, this.getLevelOpacity(level));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private getLevelOpacity(level: number): number {
|
|
257
|
+
return Math.min(80, 50 + level * 5) / 100;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private isRowLevelSticky(level: number): boolean {
|
|
261
|
+
return this.data?.rowSticky?.[level];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private isColumnLevelSticky(level: number): boolean {
|
|
265
|
+
const maxLevel = Math.min(level, (this.data?.columnSticky?.length ?? Number.MAX_SAFE_INTEGER) - 1);
|
|
266
|
+
return this.data?.columnSticky?.[maxLevel];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private getSummaryBackground(level: number): string {
|
|
270
|
+
const index = Math.min(level, this.groupColors.length - 1);
|
|
271
|
+
return this.groupColors[index];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private fillCellsForRow(cells: LmrPivotTableCell[][], row: number) {
|
|
275
|
+
const rowIndexInCells = this.rowsTransformationArray[row];
|
|
276
|
+
if (isNotNullOrUndefined(rowIndexInCells)) {
|
|
277
|
+
for (let column = 0; column < this.columnsTransformationArray.length; column++) {
|
|
278
|
+
const columnIndexInCells = this.columnsTransformationArray[column];
|
|
279
|
+
if (isNotNullOrUndefined(columnIndexInCells)) {
|
|
280
|
+
const value = this.data.values[row][column];
|
|
281
|
+
const dataResources = this.dataResources?.[row]?.[column] || [];
|
|
282
|
+
const formattedValue = this.aggregateOrFormatSingleValue(value, column);
|
|
283
|
+
const stringValue = isNotNullOrUndefined(formattedValue) ? String(formattedValue) : '';
|
|
284
|
+
cells[rowIndexInCells][columnIndexInCells] = {
|
|
285
|
+
value: stringValue,
|
|
286
|
+
dataResources,
|
|
287
|
+
rowSpan: 1,
|
|
288
|
+
colSpan: 1,
|
|
289
|
+
cssClass: PivotTableConverter.dataClass,
|
|
290
|
+
isHeader: false,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private getValueIndexForColumns(columns: number[]): number {
|
|
298
|
+
return columns[0] % this.data.valueTitles.length;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private formatValueByValueType(value: any, valueIndex: number): any {
|
|
302
|
+
const valueType = (this.data.valueTypes || [])[valueIndex];
|
|
303
|
+
if (!valueType || valueType === LmrPivotValueType.Default) {
|
|
304
|
+
return this.formatValueByConstraint(value, valueIndex);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
[LmrPivotValueType.AllPercentage, LmrPivotValueType.ColumnPercentage, LmrPivotValueType.RowPercentage].includes(valueType)
|
|
309
|
+
) {
|
|
310
|
+
return this.formatValueByPercentage(value);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return this.formatValueByConstraint(value, valueIndex);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private formatGroupedValueByValueType(value: any, rows: number[], columns: number[]): any {
|
|
317
|
+
const valueIndex = columns[0] % this.data.valueTitles.length;
|
|
318
|
+
const valueType = this.data.valueTypes && this.data.valueTypes[valueIndex];
|
|
319
|
+
const valueTypeInfo = this.valueTypeInfo[valueIndex];
|
|
320
|
+
if (!valueTypeInfo || !valueType || valueType === LmrPivotValueType.Default) {
|
|
321
|
+
return this.formatValueByConstraint(value, valueIndex);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (valueType === LmrPivotValueType.AllPercentage) {
|
|
325
|
+
return this.formatValueByPercentage(divideValues(value, valueTypeInfo.sum));
|
|
326
|
+
} else if (valueType === LmrPivotValueType.ColumnPercentage) {
|
|
327
|
+
const columnsDividers = columns.reduce((dividers, column) => {
|
|
328
|
+
dividers.push(valueTypeInfo.sumsColumns[column]);
|
|
329
|
+
return dividers;
|
|
330
|
+
}, []);
|
|
331
|
+
const columnsDivider = aggregateDataValues(DataAggregationType.Sum, columnsDividers);
|
|
332
|
+
return this.formatValueByPercentage(divideValues(value, columnsDivider));
|
|
333
|
+
} else if (valueType === LmrPivotValueType.RowPercentage) {
|
|
334
|
+
const rowsDividers = rows.reduce((dividers, row) => {
|
|
335
|
+
dividers.push(valueTypeInfo.sumsRows[row]);
|
|
336
|
+
return dividers;
|
|
337
|
+
}, []);
|
|
338
|
+
const rowsDivider = aggregateDataValues(DataAggregationType.Sum, rowsDividers);
|
|
339
|
+
return this.formatValueByPercentage(divideValues(value, rowsDivider));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return this.formatValueByConstraint(value, valueIndex);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private formatValueByPercentage(value: any): string {
|
|
346
|
+
return this.percentageConstraint.createDataValue(value).format();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private formatValueByConstraint(value: any, valueIndex: number): any {
|
|
350
|
+
const constraint = this.data.valuesConstraints?.[valueIndex] || this.valueTypeInfo[valueIndex]?.defaultConstraint;
|
|
351
|
+
if (constraint) {
|
|
352
|
+
return constraint.createDataValue(value, this.constraintData).preview();
|
|
353
|
+
}
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private fillCellsForGroupedRow(
|
|
358
|
+
cells: LmrPivotTableCell[][],
|
|
359
|
+
rows: number[],
|
|
360
|
+
rowIndexInCells: number,
|
|
361
|
+
background: string
|
|
362
|
+
) {
|
|
363
|
+
for (let column = 0; column < this.columnsTransformationArray.length; column++) {
|
|
364
|
+
const columnIndexInCells = this.columnsTransformationArray[column];
|
|
365
|
+
if (isNotNullOrUndefined(columnIndexInCells)) {
|
|
366
|
+
const {values, dataResources} = this.getGroupedValuesForRowsAndCols(rows, [column]);
|
|
367
|
+
const formattedValue = this.aggregateAndFormatDataValues(values, rows, [column]);
|
|
368
|
+
cells[rowIndexInCells][columnIndexInCells] = {
|
|
369
|
+
value: String(formattedValue),
|
|
370
|
+
dataResources,
|
|
371
|
+
colSpan: 1,
|
|
372
|
+
rowSpan: 1,
|
|
373
|
+
cssClass: PivotTableConverter.groupDataClass,
|
|
374
|
+
isHeader: false,
|
|
375
|
+
background,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private getGroupedValuesForRowsAndCols(
|
|
382
|
+
rows: number[],
|
|
383
|
+
columns: number[]
|
|
384
|
+
): {values: any[]; dataResources: DataResource[]} {
|
|
385
|
+
const values = [];
|
|
386
|
+
const dataResources = [];
|
|
387
|
+
for (const row of rows) {
|
|
388
|
+
for (const column of columns) {
|
|
389
|
+
const rowColumnValue = this.values[row][column];
|
|
390
|
+
if (isArray(rowColumnValue)) {
|
|
391
|
+
values.push(...rowColumnValue);
|
|
392
|
+
} else {
|
|
393
|
+
values.push(rowColumnValue);
|
|
394
|
+
}
|
|
395
|
+
dataResources.push(...(this.dataResources?.[row]?.[column] || []));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return {values, dataResources};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private fillCellsByColumns(cells: LmrPivotTableCell[][]): HeaderGroupInfo[] {
|
|
402
|
+
const columnGroups = [];
|
|
403
|
+
this.iterateAndFillCellsByColumns(
|
|
404
|
+
cells,
|
|
405
|
+
columnGroups,
|
|
406
|
+
this.data.columnHeaders,
|
|
407
|
+
this.rowLevels,
|
|
408
|
+
this.columnShowSums,
|
|
409
|
+
0
|
|
410
|
+
);
|
|
411
|
+
return columnGroups;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private iterateAndFillCellsByColumns(
|
|
415
|
+
cells: LmrPivotTableCell[][],
|
|
416
|
+
columnGroupsInfo: HeaderGroupInfo[],
|
|
417
|
+
headers: LmrPivotDataHeader[],
|
|
418
|
+
startIndex: number,
|
|
419
|
+
showSums: boolean[],
|
|
420
|
+
level: number,
|
|
421
|
+
parentHeader?: LmrPivotDataHeader
|
|
422
|
+
) {
|
|
423
|
+
let currentIndex = startIndex;
|
|
424
|
+
const numberOfSums = Math.max(1, this.data.valueTitles.length);
|
|
425
|
+
for (const header of headers) {
|
|
426
|
+
const colSpan = getDirectHeaderChildCount(header, level, showSums, numberOfSums);
|
|
427
|
+
cells[level][currentIndex] = {
|
|
428
|
+
value: header.title,
|
|
429
|
+
cssClass: PivotTableConverter.columnHeaderClass,
|
|
430
|
+
isHeader: true,
|
|
431
|
+
rowSpan: 1,
|
|
432
|
+
colSpan,
|
|
433
|
+
stickyTop: this.isColumnLevelSticky(level),
|
|
434
|
+
background: this.getHeaderBackground(header, level),
|
|
435
|
+
constraint: header.constraint,
|
|
436
|
+
label: header.attributeName,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
if (header.children) {
|
|
440
|
+
this.iterateAndFillCellsByColumns(
|
|
441
|
+
cells,
|
|
442
|
+
columnGroupsInfo,
|
|
443
|
+
header.children,
|
|
444
|
+
currentIndex,
|
|
445
|
+
showSums,
|
|
446
|
+
level + 1,
|
|
447
|
+
header
|
|
448
|
+
);
|
|
449
|
+
} else if (isNotNullOrUndefined(header.targetIndex)) {
|
|
450
|
+
this.fillCellsForColumn(cells, header.targetIndex);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
currentIndex += getHeaderChildCount(header, level, showSums, numberOfSums);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (showSums[level]) {
|
|
457
|
+
const background = this.getSummaryBackground(level);
|
|
458
|
+
const summary = level === 0 ? this.strings.summaryString : this.strings.headerSummaryString;
|
|
459
|
+
const numberOfValues = this.data.valueTitles.length;
|
|
460
|
+
const rowIndex = Math.max(level - 1, 0);
|
|
461
|
+
const shouldAddValueHeaders = numberOfValues > 1;
|
|
462
|
+
|
|
463
|
+
cells[rowIndex][currentIndex] = {
|
|
464
|
+
value: parentHeader?.title,
|
|
465
|
+
constraint: parentHeader?.constraint,
|
|
466
|
+
label: parentHeader?.attributeName,
|
|
467
|
+
cssClass: PivotTableConverter.columnGroupHeaderClass,
|
|
468
|
+
isHeader: true,
|
|
469
|
+
stickyTop: this.isColumnLevelSticky(level),
|
|
470
|
+
rowSpan: this.columnLevels - rowIndex - (shouldAddValueHeaders ? 1 : 0),
|
|
471
|
+
colSpan: numberOfSums,
|
|
472
|
+
background,
|
|
473
|
+
summary,
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
if (numberOfValues > 0) {
|
|
477
|
+
for (let i = 0; i < numberOfValues; i++) {
|
|
478
|
+
const columnIndexInCells = currentIndex + i;
|
|
479
|
+
if (shouldAddValueHeaders) {
|
|
480
|
+
const valueTitle = this.data.valueTitles[i];
|
|
481
|
+
cells[this.columnLevels - 1][columnIndexInCells] = {
|
|
482
|
+
value: valueTitle,
|
|
483
|
+
cssClass: PivotTableConverter.columnGroupHeaderClass,
|
|
484
|
+
isHeader: true,
|
|
485
|
+
stickyTop: this.isColumnLevelSticky(level),
|
|
486
|
+
rowSpan: 1,
|
|
487
|
+
colSpan: 1,
|
|
488
|
+
background,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const columnsIndexes = getTargetIndexesForHeaders(headers);
|
|
493
|
+
const valueColumnsIndexes = columnsIndexes.filter(index => index % numberOfValues === i);
|
|
494
|
+
const transformedColumnIndexes = valueColumnsIndexes
|
|
495
|
+
.map(v => this.columnsTransformationArray[v])
|
|
496
|
+
.filter(v => isNotNullOrUndefined(v));
|
|
497
|
+
columnGroupsInfo[columnIndexInCells] = {background, indexes: transformedColumnIndexes, level};
|
|
498
|
+
|
|
499
|
+
this.fillCellsForGroupedColumn(cells, valueColumnsIndexes, columnIndexInCells, background);
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
columnGroupsInfo[currentIndex] = {background, indexes: [], level};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private fillCellsForGroupedColumn(
|
|
508
|
+
cells: LmrPivotTableCell[][],
|
|
509
|
+
columns: number[],
|
|
510
|
+
columnIndexInCells: number,
|
|
511
|
+
background: string
|
|
512
|
+
) {
|
|
513
|
+
for (let row = 0; row < this.rowsTransformationArray.length; row++) {
|
|
514
|
+
const rowIndexInCells = this.rowsTransformationArray[row];
|
|
515
|
+
if (isNotNullOrUndefined(rowIndexInCells)) {
|
|
516
|
+
const {values, dataResources} = this.getGroupedValuesForRowsAndCols([row], columns);
|
|
517
|
+
const formattedValue = this.aggregateAndFormatDataValues(values, [row], columns);
|
|
518
|
+
cells[rowIndexInCells][columnIndexInCells] = {
|
|
519
|
+
value: String(formattedValue),
|
|
520
|
+
dataResources,
|
|
521
|
+
colSpan: 1,
|
|
522
|
+
rowSpan: 1,
|
|
523
|
+
cssClass: PivotTableConverter.groupDataClass,
|
|
524
|
+
isHeader: false,
|
|
525
|
+
background,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private aggregateAndFormatDataValues(values: any[], rows: number[], columns: number[]): any {
|
|
532
|
+
const aggregation = this.aggregationByColumns(columns);
|
|
533
|
+
if (aggregation === DataAggregationType.Join) {
|
|
534
|
+
const valueIndex = this.getValueIndexForColumns(columns);
|
|
535
|
+
const constraint = this.data.valuesConstraints?.[valueIndex] || this.valueTypeInfo[valueIndex]?.defaultConstraint;
|
|
536
|
+
return aggregateDataValues(aggregation, values, constraint, false, this.constraintData);
|
|
537
|
+
}
|
|
538
|
+
const aggregatedValue = aggregateDataValues(aggregation, values);
|
|
539
|
+
return this.formatGroupedValueByValueType(aggregatedValue, rows, columns);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private aggregationByColumns(columns: number[]): DataAggregationType {
|
|
543
|
+
const valueIndex = columns[0] % this.data.valueTitles.length;
|
|
544
|
+
const aggregation = this.data.valueAggregations?.[valueIndex];
|
|
545
|
+
return isValueAggregation(aggregation) ? aggregation : DataAggregationType.Sum;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private fillCellsForColumn(cells: LmrPivotTableCell[][], column: number) {
|
|
549
|
+
const columnIndexInCells = this.columnsTransformationArray[column];
|
|
550
|
+
if (isNotNullOrUndefined(columnIndexInCells)) {
|
|
551
|
+
for (let row = 0; row < this.rowsTransformationArray.length; row++) {
|
|
552
|
+
const rowIndexInCells = this.rowsTransformationArray[row];
|
|
553
|
+
if (isNotNullOrUndefined(rowIndexInCells)) {
|
|
554
|
+
const value = this.data.values[row][column];
|
|
555
|
+
const dataResources = this.dataResources?.[row]?.[column] || [];
|
|
556
|
+
const formattedValue = this.aggregateOrFormatSingleValue(value, column);
|
|
557
|
+
const stringValue = isNotNullOrUndefined(formattedValue) ? String(formattedValue) : '';
|
|
558
|
+
cells[rowIndexInCells][columnIndexInCells] = {
|
|
559
|
+
value: stringValue,
|
|
560
|
+
dataResources,
|
|
561
|
+
rowSpan: 1,
|
|
562
|
+
colSpan: 1,
|
|
563
|
+
cssClass: PivotTableConverter.dataClass,
|
|
564
|
+
isHeader: false,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private aggregateOrFormatSingleValue(value: any, column: number): any {
|
|
572
|
+
const aggregation = this.aggregationByColumns([column]);
|
|
573
|
+
const valueIndex = this.getValueIndexForColumns([column]);
|
|
574
|
+
if (aggregation === DataAggregationType.Join) {
|
|
575
|
+
const constraint = this.data.valuesConstraints?.[valueIndex] || this.valueTypeInfo[valueIndex]?.defaultConstraint;
|
|
576
|
+
return aggregateDataValues(aggregation, [value], constraint, false, this.constraintData);
|
|
577
|
+
}
|
|
578
|
+
return this.formatValueByValueType(value, valueIndex);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private fillCellsByGroupIntersection(
|
|
582
|
+
cells: LmrPivotTableCell[][],
|
|
583
|
+
rowGroupsInfo: HeaderGroupInfo[],
|
|
584
|
+
columnGroupsInfo: HeaderGroupInfo[]
|
|
585
|
+
) {
|
|
586
|
+
const rowsCount = cells.length;
|
|
587
|
+
const columnsCount = (cells[0] && cells[0].length) || 0;
|
|
588
|
+
|
|
589
|
+
for (let i = 0; i < rowGroupsInfo.length; i++) {
|
|
590
|
+
const rowGroupInfo = rowGroupsInfo[i];
|
|
591
|
+
if (rowGroupInfo) {
|
|
592
|
+
for (let j = 0; j < columnGroupsInfo.length; j++) {
|
|
593
|
+
if (columnGroupsInfo[j]) {
|
|
594
|
+
// it's enough to fill group values only from row side
|
|
595
|
+
const {rowsIndexes, columnsIndexes} = this.getValuesIndexesFromCellsIndexes(
|
|
596
|
+
rowGroupInfo.indexes,
|
|
597
|
+
columnGroupsInfo[j].indexes
|
|
598
|
+
);
|
|
599
|
+
const {values, dataResources} = this.getGroupedValuesForRowsAndCols(rowsIndexes, columnsIndexes);
|
|
600
|
+
const formattedValue = this.aggregateAndFormatDataValues(values, rowsIndexes, columnsIndexes);
|
|
601
|
+
cells[i][j] = {
|
|
602
|
+
value: String(formattedValue),
|
|
603
|
+
dataResources,
|
|
604
|
+
colSpan: 1,
|
|
605
|
+
rowSpan: 1,
|
|
606
|
+
cssClass: PivotTableConverter.groupDataClass,
|
|
607
|
+
isHeader: false,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
this.fillRowWithColor(cells, i, rowGroupInfo, columnsCount);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
for (let j = 0; j < columnGroupsInfo.length; j++) {
|
|
617
|
+
if (columnGroupsInfo[j]) {
|
|
618
|
+
this.fillColumnWithColor(cells, j, columnGroupsInfo[j], rowGroupsInfo, rowsCount);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private getValuesIndexesFromCellsIndexes(
|
|
624
|
+
rows: number[],
|
|
625
|
+
columns: number[]
|
|
626
|
+
): {rowsIndexes: number[]; columnsIndexes: number[]} {
|
|
627
|
+
const rowsIndexes = rows
|
|
628
|
+
.map(row => this.rowsTransformationArray.findIndex(tRow => tRow === row))
|
|
629
|
+
.filter(index => index >= 0);
|
|
630
|
+
const columnsIndexes = columns
|
|
631
|
+
.map(column => this.columnsTransformationArray.findIndex(tColumn => tColumn === column))
|
|
632
|
+
.filter(index => index >= 0);
|
|
633
|
+
return {rowsIndexes, columnsIndexes};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private fillRowWithColor(
|
|
637
|
+
cells: LmrPivotTableCell[][],
|
|
638
|
+
row: number,
|
|
639
|
+
rowGroupInfo: HeaderGroupInfo,
|
|
640
|
+
columnsCount: number
|
|
641
|
+
) {
|
|
642
|
+
for (let i = this.rowLevels; i < columnsCount; i++) {
|
|
643
|
+
cells[row][i] && (cells[row][i].background = rowGroupInfo.background);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private fillColumnWithColor(
|
|
648
|
+
cells: LmrPivotTableCell[][],
|
|
649
|
+
column: number,
|
|
650
|
+
columnGroupInfo: HeaderGroupInfo,
|
|
651
|
+
rowGroupsInfo: HeaderGroupInfo[],
|
|
652
|
+
rowCount: number
|
|
653
|
+
) {
|
|
654
|
+
for (let i = this.columnLevels; i < rowCount; i++) {
|
|
655
|
+
const rowGroupInfo = rowGroupsInfo[i];
|
|
656
|
+
if (!rowGroupInfo || rowGroupInfo.level > columnGroupInfo.level) {
|
|
657
|
+
cells[i][column] && (cells[i][column].background = columnGroupInfo.background);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private initCells(): LmrPivotTableCell[][] {
|
|
663
|
+
const rows = this.getRowsCount() + this.columnLevels;
|
|
664
|
+
const columns = this.getColumnsCount() + this.rowLevels;
|
|
665
|
+
|
|
666
|
+
const matrix: LmrPivotTableCell[][] = [];
|
|
667
|
+
for (let i = 0; i < rows; i++) {
|
|
668
|
+
matrix[i] = [];
|
|
669
|
+
for (let j = 0; j < columns; j++) {
|
|
670
|
+
if (i >= this.columnLevels && j >= this.rowLevels) {
|
|
671
|
+
const isDataClass = this.rowsTransformationArray.includes(i) && this.columnsTransformationArray.includes(j);
|
|
672
|
+
matrix[i][j] = {
|
|
673
|
+
value: '',
|
|
674
|
+
dataResources: [],
|
|
675
|
+
cssClass: isDataClass ? PivotTableConverter.dataClass : PivotTableConverter.groupDataClass,
|
|
676
|
+
rowSpan: 1,
|
|
677
|
+
colSpan: 1,
|
|
678
|
+
isHeader: false,
|
|
679
|
+
};
|
|
680
|
+
} else {
|
|
681
|
+
matrix[i][j] = undefined;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (this.rowLevels > 0 && this.columnLevels > 0) {
|
|
687
|
+
for (let i = 0; i < this.columnLevels; i++) {
|
|
688
|
+
for (let j = 0; j < this.rowLevels; j++) {
|
|
689
|
+
matrix[i][j] = {
|
|
690
|
+
value: '',
|
|
691
|
+
cssClass: PivotTableConverter.emptyClass,
|
|
692
|
+
rowSpan: 1,
|
|
693
|
+
colSpan: 1,
|
|
694
|
+
stickyStart: this.isRowLevelSticky(j),
|
|
695
|
+
stickyTop: this.isColumnLevelSticky(i),
|
|
696
|
+
isHeader: false,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return matrix;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private getRowsCount(): number {
|
|
706
|
+
if (this.data.rowHeaders.length === 0 && (this.data.valueTitles || []).length > 0) {
|
|
707
|
+
return 1;
|
|
708
|
+
}
|
|
709
|
+
return getHeadersChildCount(this.data.rowHeaders, this.rowShowSums);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private getColumnsCount(): number {
|
|
713
|
+
if (this.data.columnHeaders.length === 0 && (this.data.valueTitles || []).length > 0) {
|
|
714
|
+
return 1;
|
|
715
|
+
}
|
|
716
|
+
const numberOfSums = Math.max(1, (this.data.valueTitles || []).length);
|
|
717
|
+
return getHeadersChildCount(this.data.columnHeaders, this.columnShowSums, numberOfSums);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function preparePivotData(
|
|
722
|
+
data: LmrPivotStemData,
|
|
723
|
+
constraintData: ConstraintData,
|
|
724
|
+
valueTypeInfo: ValueTypeInfo[]
|
|
725
|
+
): LmrPivotStemData {
|
|
726
|
+
const numberOfSums = Math.max(1, (data.valueTitles || []).length);
|
|
727
|
+
const values = computeValuesByValueType(data.values, data.valueTypes, numberOfSums, valueTypeInfo);
|
|
728
|
+
return sortPivotData({...data, values}, constraintData);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function computeValuesByValueType(
|
|
732
|
+
values: any[][],
|
|
733
|
+
valueTypes: LmrPivotValueType[],
|
|
734
|
+
numValues: number,
|
|
735
|
+
valueTypeInfo: ValueTypeInfo[]
|
|
736
|
+
): any[][] {
|
|
737
|
+
const rowsIndexes = [...Array(values.length).keys()];
|
|
738
|
+
const modifiedValues = deepObjectCopy(values);
|
|
739
|
+
|
|
740
|
+
for (let i = 0; i < numValues; i++) {
|
|
741
|
+
const valueType = valueTypes && valueTypes[i];
|
|
742
|
+
if (!valueType || valueType === LmrPivotValueType.Default) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const columnsCount = (values[0] && values[0].length) || 0;
|
|
747
|
+
const columnIndexes = [...Array(columnsCount).keys()].filter(key => key % numValues === i);
|
|
748
|
+
const info = valueTypeInfo[i];
|
|
749
|
+
|
|
750
|
+
for (const row of rowsIndexes) {
|
|
751
|
+
for (const column of columnIndexes) {
|
|
752
|
+
if (valueType === LmrPivotValueType.AllPercentage) {
|
|
753
|
+
modifiedValues[row][column] = divideValues(values[row][column], info.sum);
|
|
754
|
+
} else if (valueType === LmrPivotValueType.RowPercentage) {
|
|
755
|
+
modifiedValues[row][column] = divideValues(values[row][column], info.sumsRows[row]);
|
|
756
|
+
} else if (valueType === LmrPivotValueType.ColumnPercentage) {
|
|
757
|
+
modifiedValues[row][column] = divideValues(values[row][column], info.sumsColumns[column]);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return modifiedValues;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function getValuesTypeInfo(values: any[][], valueTypes: LmrPivotValueType[], numValues: number): ValueTypeInfo[] {
|
|
767
|
+
const valueTypeInfo = [];
|
|
768
|
+
const rowsIndexes = [...Array(values.length).keys()];
|
|
769
|
+
|
|
770
|
+
for (let i = 0; i < numValues; i++) {
|
|
771
|
+
const valueType = valueTypes && valueTypes[i];
|
|
772
|
+
const columnsCount = (values[0] && values[0].length) || 0;
|
|
773
|
+
const columnIndexes = [...Array(columnsCount).keys()].filter(key => key % numValues === i);
|
|
774
|
+
|
|
775
|
+
valueTypeInfo[i] = getValueTypeInfo(values, valueType, rowsIndexes, columnIndexes);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return valueTypeInfo;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function getValueTypeInfo(values: any[][], type: LmrPivotValueType, rows: number[], columns: number[]): ValueTypeInfo {
|
|
782
|
+
const containsDecimal = containsDecimalValue(values, rows, columns);
|
|
783
|
+
const valueTypeInfo: ValueTypeInfo = {
|
|
784
|
+
defaultConstraint: containsDecimal ? new NumberConstraint({decimals: 2}) : null,
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
if (type === LmrPivotValueType.AllPercentage) {
|
|
788
|
+
return {...valueTypeInfo, sum: getNumericValuesSummary(values, rows, columns)};
|
|
789
|
+
} else if (type === LmrPivotValueType.RowPercentage) {
|
|
790
|
+
return {
|
|
791
|
+
...valueTypeInfo,
|
|
792
|
+
sumsRows: rows.reduce((arr, row) => {
|
|
793
|
+
arr[row] = getNumericValuesSummary(values, [row], columns);
|
|
794
|
+
return arr;
|
|
795
|
+
}, []),
|
|
796
|
+
};
|
|
797
|
+
} else if (type === LmrPivotValueType.ColumnPercentage) {
|
|
798
|
+
return {
|
|
799
|
+
...valueTypeInfo,
|
|
800
|
+
sumsColumns: columns.reduce((arr, column) => {
|
|
801
|
+
arr[column] = getNumericValuesSummary(values, rows, [column]);
|
|
802
|
+
return arr;
|
|
803
|
+
}, []),
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return {...valueTypeInfo};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function containsDecimalValue(values: any[][], rows: number[], columns: number[]): boolean {
|
|
811
|
+
for (const row of rows) {
|
|
812
|
+
for (const column of columns) {
|
|
813
|
+
if (isValueDecimal(values[row][column])) {
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function isValueDecimal(value: string): boolean {
|
|
822
|
+
if (isNullOrUndefined(value)) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (isNumeric(value)) {
|
|
827
|
+
return toNumber(value) % 1 !== 0;
|
|
828
|
+
}
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function createTransformationMap(
|
|
833
|
+
headers: LmrPivotDataHeader[],
|
|
834
|
+
showSums: boolean[],
|
|
835
|
+
additionalNum: number,
|
|
836
|
+
numberOfSums: number
|
|
837
|
+
): number[] {
|
|
838
|
+
const array = [];
|
|
839
|
+
iterateThroughTransformationMap(headers, additionalNum, array, 0, showSums, numberOfSums);
|
|
840
|
+
return array;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function iterateThroughTransformationMap(
|
|
844
|
+
headers: LmrPivotDataHeader[],
|
|
845
|
+
additionalNum: number,
|
|
846
|
+
array: number[],
|
|
847
|
+
level: number,
|
|
848
|
+
showSums: boolean[],
|
|
849
|
+
numberOfSums: number
|
|
850
|
+
) {
|
|
851
|
+
let additional = additionalNum;
|
|
852
|
+
for (let i = 0; i < headers.length; i++) {
|
|
853
|
+
const header = headers[i];
|
|
854
|
+
if (header.children) {
|
|
855
|
+
iterateThroughTransformationMap(header.children, additional, array, level + 1, showSums, numberOfSums);
|
|
856
|
+
additional += getHeaderChildCount(header, level, showSums, numberOfSums);
|
|
857
|
+
} else if (isNotNullOrUndefined(header.targetIndex)) {
|
|
858
|
+
array[header.targetIndex] = i + additional;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function getTargetIndexesForHeaders(headers: LmrPivotDataHeader[]): number[] {
|
|
864
|
+
const allRows = (headers || []).reduce((rows, header) => {
|
|
865
|
+
rows.push(...getTargetIndexesForHeader(header));
|
|
866
|
+
return rows;
|
|
867
|
+
}, []);
|
|
868
|
+
return uniqueValues<number>(allRows);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function getTargetIndexesForHeader(pivotDataHeader: LmrPivotDataHeader): number[] {
|
|
872
|
+
if (pivotDataHeader.children) {
|
|
873
|
+
return pivotDataHeader.children.reduce((rows, header) => {
|
|
874
|
+
rows.push(...getTargetIndexesForHeader(header));
|
|
875
|
+
return rows;
|
|
876
|
+
}, []);
|
|
877
|
+
}
|
|
878
|
+
return [pivotDataHeader.targetIndex];
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function getHeadersChildCount(headers: LmrPivotDataHeader[], showSums: boolean[], numberOfSums = 1): number {
|
|
882
|
+
return (headers || []).reduce(
|
|
883
|
+
(sum, header) => sum + getHeaderChildCount(header, 0, showSums, numberOfSums),
|
|
884
|
+
showSums[0] ? numberOfSums : 0
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function getHeaderChildCount(
|
|
889
|
+
pivotDataHeader: LmrPivotDataHeader,
|
|
890
|
+
level: number,
|
|
891
|
+
showSums: boolean[],
|
|
892
|
+
numberOfSums = 1,
|
|
893
|
+
includeChild = true
|
|
894
|
+
): number {
|
|
895
|
+
if (pivotDataHeader.children) {
|
|
896
|
+
return pivotDataHeader.children.reduce(
|
|
897
|
+
(sum, header) => sum + getHeaderChildCount(header, level + 1, showSums, numberOfSums, includeChild),
|
|
898
|
+
showSums[level + 1] ? numberOfSums : 0
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
return includeChild ? 1 : 0;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function getDirectHeaderChildCount(
|
|
905
|
+
pivotDataHeader: LmrPivotDataHeader,
|
|
906
|
+
level: number,
|
|
907
|
+
showSums: boolean[],
|
|
908
|
+
numberOfSums = 1
|
|
909
|
+
): number {
|
|
910
|
+
if (pivotDataHeader.children) {
|
|
911
|
+
return pivotDataHeader.children.reduce(
|
|
912
|
+
(sum, header) => sum + getHeaderChildCount(header, level + 1, showSums, numberOfSums),
|
|
913
|
+
0
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
return 1;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export function sortPivotData(data: LmrPivotStemData, constraintData: ConstraintData): LmrPivotStemData {
|
|
920
|
+
return {
|
|
921
|
+
...data,
|
|
922
|
+
rowHeaders: sortPivotRowDataHeaders(data.rowHeaders, data.rowSorts, data, constraintData),
|
|
923
|
+
columnHeaders: sortPivotColumnDataHeaders(data.columnHeaders, data.columnSorts, data, constraintData),
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function sortPivotRowDataHeaders(
|
|
928
|
+
rowHeaders: LmrPivotDataHeader[],
|
|
929
|
+
rowSorts: LmrPivotSort[],
|
|
930
|
+
pivotData: LmrPivotStemData,
|
|
931
|
+
constraintData: ConstraintData
|
|
932
|
+
): LmrPivotDataHeader[] {
|
|
933
|
+
return sortPivotDataHeadersRecursive(
|
|
934
|
+
rowHeaders,
|
|
935
|
+
0,
|
|
936
|
+
rowSorts,
|
|
937
|
+
pivotData.columnHeaders,
|
|
938
|
+
pivotData.values,
|
|
939
|
+
pivotData.valueTitles || [],
|
|
940
|
+
true,
|
|
941
|
+
constraintData
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function sortPivotColumnDataHeaders(
|
|
946
|
+
columnHeaders: LmrPivotDataHeader[],
|
|
947
|
+
columnSorts: LmrPivotSort[],
|
|
948
|
+
pivotData: LmrPivotStemData,
|
|
949
|
+
constraintData: ConstraintData
|
|
950
|
+
): LmrPivotDataHeader[] {
|
|
951
|
+
return sortPivotDataHeadersRecursive(
|
|
952
|
+
columnHeaders,
|
|
953
|
+
0,
|
|
954
|
+
columnSorts,
|
|
955
|
+
pivotData.rowHeaders,
|
|
956
|
+
pivotData.values,
|
|
957
|
+
pivotData.valueTitles || [],
|
|
958
|
+
false,
|
|
959
|
+
constraintData
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function sortPivotDataHeadersRecursive(
|
|
964
|
+
headers: LmrPivotDataHeader[],
|
|
965
|
+
index: number,
|
|
966
|
+
sorts: LmrPivotSort[],
|
|
967
|
+
otherSideHeaders: LmrPivotDataHeader[],
|
|
968
|
+
values: any[][],
|
|
969
|
+
valueTitles: string[],
|
|
970
|
+
isRows: boolean,
|
|
971
|
+
constraintData: ConstraintData
|
|
972
|
+
): LmrPivotDataHeader[] {
|
|
973
|
+
// we don't want to sort values headers
|
|
974
|
+
if (!isRows && isValuesHeaders(headers, valueTitles)) {
|
|
975
|
+
return headers;
|
|
976
|
+
}
|
|
977
|
+
const sort = sorts && sorts[index];
|
|
978
|
+
const constraint = getConstraintForSort(sort, headers);
|
|
979
|
+
const valuesMap = createHeadersValuesMap(headers, sort, otherSideHeaders, values, valueTitles, isRows);
|
|
980
|
+
return headers
|
|
981
|
+
.map(header => ({
|
|
982
|
+
...header,
|
|
983
|
+
children:
|
|
984
|
+
header.children &&
|
|
985
|
+
sortPivotDataHeadersRecursive(
|
|
986
|
+
header.children,
|
|
987
|
+
index + 1,
|
|
988
|
+
sorts,
|
|
989
|
+
otherSideHeaders,
|
|
990
|
+
values,
|
|
991
|
+
valueTitles,
|
|
992
|
+
isRows,
|
|
993
|
+
constraintData
|
|
994
|
+
),
|
|
995
|
+
}))
|
|
996
|
+
.sort((r1, r2) => {
|
|
997
|
+
const r1Value = constraint.createDataValue(valuesMap[r1.title], constraintData);
|
|
998
|
+
const r2Value = constraint.createDataValue(valuesMap[r2.title], constraintData);
|
|
999
|
+
const multiplier = !sort || sort.asc ? 1 : -1;
|
|
1000
|
+
return r1Value.compareTo(r2Value) * multiplier;
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function getConstraintForSort(sort: LmrPivotSort, headers: LmrPivotDataHeader[]): Constraint {
|
|
1005
|
+
if ((sort?.list?.values || []).length > 0) {
|
|
1006
|
+
// sort is done by values in columns
|
|
1007
|
+
return new NumberConstraint({});
|
|
1008
|
+
}
|
|
1009
|
+
return ((headers || [])[0] && (headers || [])[0].constraint) || new UnknownConstraint();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function isValuesHeaders(headers: LmrPivotDataHeader[], valueTitles: string[]): boolean {
|
|
1013
|
+
return (
|
|
1014
|
+
valueTitles.length > 1 &&
|
|
1015
|
+
(headers || []).every(
|
|
1016
|
+
(header, index) => isNotNullOrUndefined(header.targetIndex) && header.title === valueTitles[index]
|
|
1017
|
+
)
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function createHeadersValuesMap(
|
|
1022
|
+
headers: LmrPivotDataHeader[],
|
|
1023
|
+
sort: LmrPivotSort,
|
|
1024
|
+
otherSideHeaders: LmrPivotDataHeader[],
|
|
1025
|
+
values: any[][],
|
|
1026
|
+
valueTitles: string[],
|
|
1027
|
+
isRows: boolean
|
|
1028
|
+
): Record<string, any> {
|
|
1029
|
+
const sortTargetIndexes = sortValueTargetIndexes(sort, otherSideHeaders, valueTitles);
|
|
1030
|
+
if (!sortTargetIndexes) {
|
|
1031
|
+
return (headers || []).reduce((valuesMap, header) => {
|
|
1032
|
+
valuesMap[header.title] = header.title;
|
|
1033
|
+
return valuesMap;
|
|
1034
|
+
}, {});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return (headers || []).reduce((valuesMap, header) => {
|
|
1038
|
+
const rows = isRows ? getTargetIndexesForHeader(header) : sortTargetIndexes;
|
|
1039
|
+
const columns = isRows ? sortTargetIndexes : getTargetIndexesForHeader(header);
|
|
1040
|
+
valuesMap[header.title] = getNumericValuesSummary(values, rows, columns);
|
|
1041
|
+
return valuesMap;
|
|
1042
|
+
}, {});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function getNumericValuesSummary(values: any[][], rows: number[], columns: number[]): number {
|
|
1046
|
+
let sum = 0;
|
|
1047
|
+
for (const row of rows) {
|
|
1048
|
+
for (const column of columns) {
|
|
1049
|
+
const value = values[row][column];
|
|
1050
|
+
if (isNotNullOrUndefined(value) && isNumeric(value)) {
|
|
1051
|
+
sum += toNumber(value);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return sum;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function sortValueTargetIndexes(
|
|
1059
|
+
sort: LmrPivotSort,
|
|
1060
|
+
otherSideHeaders: LmrPivotDataHeader[],
|
|
1061
|
+
valueTitles: string[]
|
|
1062
|
+
): number[] | null {
|
|
1063
|
+
if (sort && sort.list) {
|
|
1064
|
+
let valueIndex = valueTitles.findIndex(title => title === sort.list.valueTitle);
|
|
1065
|
+
if (valueIndex === -1) {
|
|
1066
|
+
if (valueTitles.length === 1) {
|
|
1067
|
+
valueIndex = 0;
|
|
1068
|
+
} else {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
let pivotHeader: LmrPivotDataHeader = null;
|
|
1074
|
+
let currentOtherSideHeaders = otherSideHeaders;
|
|
1075
|
+
for (const value of sort.list.values || []) {
|
|
1076
|
+
if (value.isSummary) {
|
|
1077
|
+
const indexes = getTargetIndexesForHeaders(currentOtherSideHeaders || []) || [];
|
|
1078
|
+
return filterIndexesByMod(indexes, valueTitles.length, valueIndex);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
pivotHeader = (currentOtherSideHeaders || []).find(header => header.title === value.title);
|
|
1082
|
+
if (!pivotHeader) {
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
currentOtherSideHeaders = pivotHeader.children || [];
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (pivotHeader) {
|
|
1090
|
+
const targetIndexes = isNotNullOrUndefined(pivotHeader.targetIndex)
|
|
1091
|
+
? [pivotHeader.targetIndex]
|
|
1092
|
+
: getTargetIndexesForHeaders(currentOtherSideHeaders);
|
|
1093
|
+
return filterIndexesByMod(targetIndexes, valueTitles.length, valueIndex);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function filterIndexesByMod(indexes: number[], mod: number, value: number): number[] {
|
|
1101
|
+
return (indexes || []).filter(index => index % mod === value);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function divideValues(value: any, divider: any): number {
|
|
1105
|
+
if (isNullOrUndefined(value)) {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (isNumeric(value) && isNumeric(divider)) {
|
|
1110
|
+
if (divider !== 0) {
|
|
1111
|
+
return value / divider;
|
|
1112
|
+
} else {
|
|
1113
|
+
return 0;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|