@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,803 @@
|
|
|
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
|
+
Attribute,
|
|
21
|
+
AggregatedDataMap,
|
|
22
|
+
AggregatedDataValues,
|
|
23
|
+
AggregatedMapData,
|
|
24
|
+
Constraint,
|
|
25
|
+
ConstraintData,
|
|
26
|
+
ConstraintType,
|
|
27
|
+
DataAggregationType,
|
|
28
|
+
DataAggregator,
|
|
29
|
+
DataAggregatorAttribute,
|
|
30
|
+
DataValue,
|
|
31
|
+
DocumentsAndLinksData,
|
|
32
|
+
UnknownConstraint,
|
|
33
|
+
aggregateDataResources,
|
|
34
|
+
dataAggregationConstraint, QueryStem, Collection, LinkType, DataResource, Query, AttributesResourceType, attributesResourcesAttributesMap,
|
|
35
|
+
} from '@lumeer/data-filters';
|
|
36
|
+
import {deepObjectsEquals, flattenMatrix, flattenValues, isArray, isNotNullOrUndefined, uniqueValues} from '@lumeer/utils';
|
|
37
|
+
import {LmrPivotAttribute, LmrPivotConfig, LmrPivotRowColumnAttribute, LmrPivotSort, LmrPivotStemConfig, LmrPivotTransform, LmrPivotValueAttribute} from './lmr-pivot-config';
|
|
38
|
+
import {LmrPivotData, LmrPivotDataHeader, LmrPivotStemData} from './lmr-pivot-data';
|
|
39
|
+
import {pivotStemConfigIsEmpty} from './pivot-util';
|
|
40
|
+
|
|
41
|
+
interface PivotMergeData {
|
|
42
|
+
configs: LmrPivotStemConfig[];
|
|
43
|
+
stems: QueryStem[];
|
|
44
|
+
stemsIndexes: number[];
|
|
45
|
+
type: PivotConfigType;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
enum PivotConfigType {
|
|
49
|
+
Values,
|
|
50
|
+
Rows,
|
|
51
|
+
Columns,
|
|
52
|
+
RowsAndColumns,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface PivotColors {
|
|
56
|
+
rows: string[];
|
|
57
|
+
columns: string[];
|
|
58
|
+
values: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface PivotConfigData {
|
|
62
|
+
rowShowSums: boolean[];
|
|
63
|
+
rowSticky: boolean[];
|
|
64
|
+
rowSorts: LmrPivotSort[];
|
|
65
|
+
columnShowSums: boolean[];
|
|
66
|
+
columnSticky: boolean[];
|
|
67
|
+
columnSorts: LmrPivotSort[];
|
|
68
|
+
rowAttributes: Attribute[];
|
|
69
|
+
columnAttributes: Attribute[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class PivotDataConverter {
|
|
73
|
+
private collections: Collection[];
|
|
74
|
+
private linkTypes: LinkType[];
|
|
75
|
+
private collectionsAttributesMap: Record<string, Record<string, Attribute>>;
|
|
76
|
+
private linkTypesAttributesMap: Record<string, Record<string, Attribute>>;
|
|
77
|
+
private data: DocumentsAndLinksData;
|
|
78
|
+
private config: LmrPivotConfig;
|
|
79
|
+
private transform: LmrPivotTransform;
|
|
80
|
+
private constraintData?: ConstraintData;
|
|
81
|
+
|
|
82
|
+
private dataAggregator: DataAggregator;
|
|
83
|
+
|
|
84
|
+
constructor() {
|
|
85
|
+
this.dataAggregator = new DataAggregator((value, constraint, data, aggregatorAttribute) =>
|
|
86
|
+
this.formatPivotValue(value, constraint, data, aggregatorAttribute)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private formatPivotValue(
|
|
91
|
+
value: any,
|
|
92
|
+
constraint: Constraint,
|
|
93
|
+
constraintData: ConstraintData,
|
|
94
|
+
aggregatorAttribute: DataAggregatorAttribute
|
|
95
|
+
): any {
|
|
96
|
+
const pivotConstraint = aggregatorAttribute.data && (aggregatorAttribute.data as Constraint);
|
|
97
|
+
const overrideConstraint =
|
|
98
|
+
pivotConstraint && this.transform?.checkValidConstraintOverride?.(constraint, pivotConstraint);
|
|
99
|
+
const finalConstraint = overrideConstraint || constraint || new UnknownConstraint();
|
|
100
|
+
return this.formatDataValue(finalConstraint.createDataValue(value, constraintData), finalConstraint);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private formatDataValue(dataValue: DataValue, constraint: Constraint): any {
|
|
104
|
+
switch (constraint.type) {
|
|
105
|
+
case ConstraintType.DateTime:
|
|
106
|
+
return dataValue.format();
|
|
107
|
+
default:
|
|
108
|
+
return dataValue.serialize();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private updateData(
|
|
113
|
+
config: LmrPivotConfig,
|
|
114
|
+
transform: LmrPivotTransform,
|
|
115
|
+
collections: Collection[],
|
|
116
|
+
linkTypes: LinkType[],
|
|
117
|
+
data: DocumentsAndLinksData,
|
|
118
|
+
constraintData: ConstraintData
|
|
119
|
+
) {
|
|
120
|
+
this.config = config;
|
|
121
|
+
this.transform = transform;
|
|
122
|
+
this.collections = collections;
|
|
123
|
+
this.linkTypes = linkTypes;
|
|
124
|
+
this.collectionsAttributesMap = attributesResourcesAttributesMap(collections);
|
|
125
|
+
this.linkTypesAttributesMap = attributesResourcesAttributesMap(linkTypes);
|
|
126
|
+
this.data = data;
|
|
127
|
+
this.constraintData = constraintData;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public createData(
|
|
131
|
+
config: LmrPivotConfig,
|
|
132
|
+
transform: LmrPivotTransform,
|
|
133
|
+
collections: Collection[],
|
|
134
|
+
linkTypes: LinkType[],
|
|
135
|
+
data: DocumentsAndLinksData,
|
|
136
|
+
query: Query,
|
|
137
|
+
constraintData?: ConstraintData
|
|
138
|
+
): LmrPivotData {
|
|
139
|
+
this.updateData(config, transform, collections, linkTypes, data, constraintData);
|
|
140
|
+
|
|
141
|
+
const {stemsConfigs, stems} = this.filterEmptyConfigs(config, query);
|
|
142
|
+
|
|
143
|
+
const mergeData = this.createPivotMergeData(config.mergeTables, stemsConfigs, stems);
|
|
144
|
+
const ableToMerge = mergeData.length <= 1;
|
|
145
|
+
const pivotData = this.mergePivotData(mergeData);
|
|
146
|
+
return {data: pivotData, constraintData, ableToMerge, mergeTables: config.mergeTables};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private filterEmptyConfigs(config: LmrPivotConfig, query: Query): {stemsConfigs: LmrPivotStemConfig[]; stems: QueryStem[]} {
|
|
150
|
+
return (config.stemsConfigs || []).reduce(
|
|
151
|
+
({stemsConfigs, stems}, stemConfig, index) => {
|
|
152
|
+
if (!pivotStemConfigIsEmpty(stemConfig)) {
|
|
153
|
+
const stem = (query.stems || [])[index];
|
|
154
|
+
stemsConfigs.push(stemConfig);
|
|
155
|
+
stems.push(stem);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {stemsConfigs, stems};
|
|
159
|
+
},
|
|
160
|
+
{stemsConfigs: [], stems: []}
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private createPivotMergeData(
|
|
165
|
+
mergeTables: boolean,
|
|
166
|
+
stemsConfigs: LmrPivotStemConfig[],
|
|
167
|
+
stems: QueryStem[]
|
|
168
|
+
): PivotMergeData[] {
|
|
169
|
+
return stemsConfigs.reduce((mergeData: PivotMergeData[], stemConfig, index) => {
|
|
170
|
+
const configType = getPivotStemConfigType(stemConfig);
|
|
171
|
+
const mergeDataIndex = mergeData.findIndex(
|
|
172
|
+
data => data.type === configType && canMergeConfigsByType(data.type, data.configs[0], stemConfig)
|
|
173
|
+
);
|
|
174
|
+
if (mergeTables && mergeDataIndex >= 0) {
|
|
175
|
+
mergeData[mergeDataIndex].configs.push(stemConfig);
|
|
176
|
+
mergeData[mergeDataIndex].stems.push(stems[index]);
|
|
177
|
+
mergeData[mergeDataIndex].stemsIndexes.push(index);
|
|
178
|
+
} else {
|
|
179
|
+
mergeData.push({configs: [stemConfig], stems: [stems[index]], stemsIndexes: [index], type: configType});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return mergeData;
|
|
183
|
+
}, []);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private mergePivotData(mergeData: PivotMergeData[]): LmrPivotStemData[] {
|
|
187
|
+
return mergeData.reduce((stemData, data) => {
|
|
188
|
+
if (data.type === PivotConfigType.Values) {
|
|
189
|
+
stemData.push(this.convertValueAttributes(data.configs, data.stems, data.stemsIndexes));
|
|
190
|
+
} else {
|
|
191
|
+
stemData.push(this.transformStems(data.configs, data.stems, data.stemsIndexes));
|
|
192
|
+
}
|
|
193
|
+
return stemData;
|
|
194
|
+
}, []);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private transformStems(configs: LmrPivotStemConfig[], queryStems: QueryStem[], stemsIndexes: number[]): LmrPivotStemData {
|
|
198
|
+
const pivotColors: PivotColors = {rows: [], columns: [], values: []};
|
|
199
|
+
const mergedValueAttributes: LmrPivotValueAttribute[] = [];
|
|
200
|
+
let mergedAggregatedData: AggregatedMapData = null;
|
|
201
|
+
let additionalData: PivotConfigData;
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < configs.length; i++) {
|
|
204
|
+
const config = configs[i];
|
|
205
|
+
const queryStem = queryStems[i];
|
|
206
|
+
const stemIndex = stemsIndexes[i];
|
|
207
|
+
const stemData = this.data?.dataByStems?.[stemIndex];
|
|
208
|
+
|
|
209
|
+
this.dataAggregator.updateData(
|
|
210
|
+
this.collections,
|
|
211
|
+
stemData?.documents || [],
|
|
212
|
+
this.linkTypes,
|
|
213
|
+
stemData?.linkInstances || [],
|
|
214
|
+
queryStem,
|
|
215
|
+
this.constraintData
|
|
216
|
+
);
|
|
217
|
+
const rowAttributes = (config.rowAttributes || []).map(attribute =>
|
|
218
|
+
this.convertPivotRowColumnAttribute(attribute)
|
|
219
|
+
);
|
|
220
|
+
const columnAttributes = (config.columnAttributes || []).map(attribute =>
|
|
221
|
+
this.convertPivotRowColumnAttribute(attribute)
|
|
222
|
+
);
|
|
223
|
+
const valueAttributes = (config.valueAttributes || []).map(attribute => this.convertPivotAttribute(attribute));
|
|
224
|
+
|
|
225
|
+
pivotColors.rows.push(...this.getAttributesColors(config.rowAttributes));
|
|
226
|
+
pivotColors.columns.push(...this.getAttributesColors(config.columnAttributes));
|
|
227
|
+
pivotColors.values.push(...this.getAttributesColors(config.valueAttributes));
|
|
228
|
+
|
|
229
|
+
const aggregatedData = this.dataAggregator.aggregate(rowAttributes, columnAttributes, valueAttributes);
|
|
230
|
+
mergedAggregatedData = this.mergeAggregatedData(mergedAggregatedData, aggregatedData);
|
|
231
|
+
|
|
232
|
+
const filteredValueAttributes = (config.valueAttributes || []).filter(
|
|
233
|
+
valueAttr => !mergedValueAttributes.some(merAttr => deepObjectsEquals(valueAttr, merAttr))
|
|
234
|
+
);
|
|
235
|
+
mergedValueAttributes.push(...filteredValueAttributes);
|
|
236
|
+
|
|
237
|
+
if (!additionalData) {
|
|
238
|
+
additionalData = {
|
|
239
|
+
rowShowSums: (config.rowAttributes || []).map(attr => attr.showSums),
|
|
240
|
+
rowSticky: this.mapStickyValues((config.rowAttributes || []).map(attr => !!attr.sticky)),
|
|
241
|
+
rowSorts: (config.rowAttributes || []).map(attr => attr.sort),
|
|
242
|
+
rowAttributes: (config.rowAttributes || []).map(attr => this.pivotAttributeAttribute(attr)),
|
|
243
|
+
columnShowSums: (config.columnAttributes || []).map(attr => attr.showSums),
|
|
244
|
+
columnSticky: this.mapStickyValues((config.columnAttributes || []).map(attr => !!attr.sticky)),
|
|
245
|
+
columnSorts: (config.columnAttributes || []).map(attr => attr.sort),
|
|
246
|
+
columnAttributes: (config.columnAttributes || []).map(attr => this.pivotAttributeAttribute(attr)),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return this.convertAggregatedData(mergedAggregatedData, mergedValueAttributes, pivotColors, additionalData);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private mapStickyValues(values: boolean[]): boolean[] {
|
|
255
|
+
// we support only sticky rows/columns in a row
|
|
256
|
+
return values.reduce((stickyValues, sticky, index) => {
|
|
257
|
+
stickyValues.push(sticky && (index === 0 || stickyValues[index - 1]));
|
|
258
|
+
return stickyValues;
|
|
259
|
+
}, []);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private pivotAttributeConstraint(pivotAttribute: LmrPivotAttribute): Constraint | undefined {
|
|
263
|
+
const attribute = this.findAttributeByPivotAttribute(pivotAttribute);
|
|
264
|
+
const constraint = attribute && attribute.constraint;
|
|
265
|
+
const overrideConstraint =
|
|
266
|
+
pivotAttribute.constraint &&
|
|
267
|
+
this.transform?.checkValidConstraintOverride?.(constraint, pivotAttribute.constraint);
|
|
268
|
+
return overrideConstraint || constraint;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private pivotAttributeAttribute(pivotAttribute: LmrPivotAttribute): Attribute | undefined {
|
|
272
|
+
const attribute = this.findAttributeByPivotAttribute(pivotAttribute);
|
|
273
|
+
if (attribute) {
|
|
274
|
+
const constraint = attribute?.constraint;
|
|
275
|
+
const overrideConstraint =
|
|
276
|
+
pivotAttribute.constraint &&
|
|
277
|
+
this.transform?.checkValidConstraintOverride?.(constraint, pivotAttribute.constraint);
|
|
278
|
+
return {...attribute, constraint: overrideConstraint || constraint || new UnknownConstraint()};
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private mergeAggregatedData(a1: AggregatedMapData, a2: AggregatedMapData): AggregatedMapData {
|
|
284
|
+
if (!a1 || !a2) {
|
|
285
|
+
return a1 || a2;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.mergeMaps(a1.map, a2.map);
|
|
289
|
+
this.mergeMaps(a1.columnsMap, a2.columnsMap);
|
|
290
|
+
return {
|
|
291
|
+
map: a1.map,
|
|
292
|
+
columnsMap: a1.columnsMap,
|
|
293
|
+
rowLevels: Math.max(a1.rowLevels, a2.rowLevels),
|
|
294
|
+
columnLevels: Math.max(a1.columnLevels, a2.columnLevels),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private mergeMaps(m1: Record<string, any>, m2: Record<string, any>) {
|
|
299
|
+
Object.keys(m2).forEach(key => {
|
|
300
|
+
if (m1[key]) {
|
|
301
|
+
if (isArray(m1[key]) && isArray(m2[key])) {
|
|
302
|
+
m1[key].push(...m2[key]);
|
|
303
|
+
} else if (!isArray(m1[key]) && !isArray(m2[key])) {
|
|
304
|
+
this.mergeMaps(m1[key], m2[key]);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
m1[key] = m2[key];
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private getAttributesColors(attributes: LmrPivotAttribute[]): string[] {
|
|
313
|
+
return (attributes || []).map(attribute => {
|
|
314
|
+
const resource = this.dataAggregator.getNextCollectionResource(attribute.resourceIndex);
|
|
315
|
+
return resource && (<Collection>resource).color;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private convertPivotRowColumnAttribute(pivotAttribute: LmrPivotRowColumnAttribute): DataAggregatorAttribute {
|
|
320
|
+
return {...this.convertPivotAttribute(pivotAttribute), data: pivotAttribute.constraint};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private convertPivotAttribute(pivotAttribute: LmrPivotAttribute): DataAggregatorAttribute {
|
|
324
|
+
return {resourceIndex: pivotAttribute.resourceIndex, attributeId: pivotAttribute.attributeId};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private convertValueAttributes(
|
|
328
|
+
configs: LmrPivotStemConfig[],
|
|
329
|
+
stems: QueryStem[],
|
|
330
|
+
stemsIndexes: number[]
|
|
331
|
+
): LmrPivotStemData {
|
|
332
|
+
const data = configs.reduce(
|
|
333
|
+
(allData, config, index) => {
|
|
334
|
+
const stem = stems[index];
|
|
335
|
+
const stemIndex = stemsIndexes[index];
|
|
336
|
+
|
|
337
|
+
const stemData = this.data?.dataByStems?.[stemIndex];
|
|
338
|
+
this.dataAggregator.updateData(
|
|
339
|
+
this.collections,
|
|
340
|
+
stemData?.documents || [],
|
|
341
|
+
this.linkTypes,
|
|
342
|
+
stemData?.linkInstances || [],
|
|
343
|
+
stem,
|
|
344
|
+
this.constraintData
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const valueAttributes = config.valueAttributes || [];
|
|
348
|
+
allData.valueTypes.push(...valueAttributes.map(attr => attr.valueType));
|
|
349
|
+
const valueColors = this.getAttributesColors(valueAttributes);
|
|
350
|
+
|
|
351
|
+
const {titles, constraints} = this.createValueTitles(valueAttributes);
|
|
352
|
+
allData.titles.push(...titles);
|
|
353
|
+
allData.constraints.push(...constraints);
|
|
354
|
+
|
|
355
|
+
const {headers} = this.convertMapToPivotDataHeader({}, 0, [], valueColors, [], titles, allData.headers.length);
|
|
356
|
+
allData.headers.push(...headers);
|
|
357
|
+
|
|
358
|
+
allData.aggregations = [...(valueAttributes || []).map(valueAttribute => valueAttribute.aggregation)];
|
|
359
|
+
|
|
360
|
+
const {values, dataResources} = (valueAttributes || []).reduce<{values: any[]; dataResources: DataResource[][]}>(
|
|
361
|
+
(aggregator, valueAttribute, index) => {
|
|
362
|
+
const dataResources = this.findDataResourcesByPivotAttribute(valueAttribute);
|
|
363
|
+
const attribute = this.findAttributeByPivotAttribute(valueAttribute);
|
|
364
|
+
const value = aggregateDataResources(valueAttribute.aggregation, dataResources, attribute, true);
|
|
365
|
+
aggregator.values.push(value);
|
|
366
|
+
aggregator.dataResources.push(dataResources);
|
|
367
|
+
return aggregator;
|
|
368
|
+
},
|
|
369
|
+
{values: [], dataResources: []}
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
allData.values.push(...values);
|
|
373
|
+
allData.dataResources.push(...dataResources);
|
|
374
|
+
return allData;
|
|
375
|
+
},
|
|
376
|
+
{titles: [], constraints: [], headers: [], values: [], dataResources: [], valueTypes: [], aggregations: []}
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
columnHeaders: data.headers,
|
|
381
|
+
rowHeaders: [],
|
|
382
|
+
valueTitles: data.titles,
|
|
383
|
+
values: [data.values],
|
|
384
|
+
dataResources: [data.dataResources],
|
|
385
|
+
valuesConstraints: data.constraints,
|
|
386
|
+
valueTypes: data.valueTypes,
|
|
387
|
+
valueAggregations: data.aggregations,
|
|
388
|
+
|
|
389
|
+
rowShowSums: [],
|
|
390
|
+
rowSticky: [],
|
|
391
|
+
rowSorts: [],
|
|
392
|
+
columnShowSums: [],
|
|
393
|
+
columnSticky: [],
|
|
394
|
+
columnSorts: [],
|
|
395
|
+
|
|
396
|
+
hasAdditionalColumnLevel: true,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private findDataResourcesByPivotAttribute(pivotAttribute: LmrPivotAttribute): DataResource[] {
|
|
401
|
+
if (pivotAttribute.resourceType === AttributesResourceType.Collection) {
|
|
402
|
+
return (this.data?.uniqueDocuments || []).filter(document => document.collectionId === pivotAttribute.resourceId);
|
|
403
|
+
} else if (pivotAttribute.resourceType === AttributesResourceType.LinkType) {
|
|
404
|
+
return (this.data?.uniqueLinkInstances || []).filter(link => link.linkTypeId === pivotAttribute.resourceId);
|
|
405
|
+
}
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private convertAggregatedData(
|
|
410
|
+
aggregatedData: AggregatedMapData,
|
|
411
|
+
valueAttributes: LmrPivotValueAttribute[],
|
|
412
|
+
pivotColors: PivotColors,
|
|
413
|
+
additionalData: PivotConfigData
|
|
414
|
+
): LmrPivotStemData {
|
|
415
|
+
const rowData = this.convertMapToPivotDataHeader(
|
|
416
|
+
aggregatedData.map,
|
|
417
|
+
aggregatedData.rowLevels,
|
|
418
|
+
pivotColors.rows,
|
|
419
|
+
pivotColors.values,
|
|
420
|
+
additionalData.rowAttributes
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
const {titles: valueTitles, constraints: valuesConstraints} = this.createValueTitles(valueAttributes);
|
|
424
|
+
const columnData = this.convertMapToPivotDataHeader(
|
|
425
|
+
aggregatedData.rowLevels > 0 ? aggregatedData.columnsMap : aggregatedData.map,
|
|
426
|
+
aggregatedData.columnLevels,
|
|
427
|
+
pivotColors.columns,
|
|
428
|
+
pivotColors.values,
|
|
429
|
+
additionalData.columnAttributes,
|
|
430
|
+
valueTitles
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const values = this.initMatrix<number>(rowData.maxIndex + 1, columnData.maxIndex + 1);
|
|
434
|
+
const dataResources = this.initMatrix<DataResource[]>(rowData.maxIndex + 1, columnData.maxIndex + 1);
|
|
435
|
+
if ((valueAttributes || []).length > 0) {
|
|
436
|
+
this.fillValues(values, dataResources, rowData.headers, columnData.headers, valueAttributes, aggregatedData);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const valueAggregations = (valueAttributes || []).map(valueAttribute => valueAttribute.aggregation);
|
|
440
|
+
|
|
441
|
+
const hasAdditionalColumnLevel =
|
|
442
|
+
(aggregatedData.columnLevels === 0 && valueTitles.length > 0) ||
|
|
443
|
+
(aggregatedData.columnLevels > 0 && valueTitles.length > 1);
|
|
444
|
+
return {
|
|
445
|
+
rowHeaders: rowData.headers,
|
|
446
|
+
columnHeaders: columnData.headers,
|
|
447
|
+
valueTitles,
|
|
448
|
+
values,
|
|
449
|
+
dataResources,
|
|
450
|
+
valuesConstraints,
|
|
451
|
+
valueAggregations,
|
|
452
|
+
|
|
453
|
+
...additionalData,
|
|
454
|
+
|
|
455
|
+
valueTypes: valueAttributes.map(attr => attr.valueType!),
|
|
456
|
+
hasAdditionalColumnLevel,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private convertMapToPivotDataHeader(
|
|
461
|
+
map: Record<string, any>,
|
|
462
|
+
levels: number,
|
|
463
|
+
colors: string[],
|
|
464
|
+
valueColors: string[],
|
|
465
|
+
attributes: Attribute[],
|
|
466
|
+
valueTitles?: string[],
|
|
467
|
+
additionalNum: number = 0
|
|
468
|
+
): {headers: LmrPivotDataHeader[]; maxIndex: number} {
|
|
469
|
+
const headers: LmrPivotDataHeader[] = [];
|
|
470
|
+
const data = {maxIndex: 0};
|
|
471
|
+
if (levels === 0) {
|
|
472
|
+
if ((valueTitles || []).length > 0) {
|
|
473
|
+
headers.push(
|
|
474
|
+
...valueTitles!.map((title, index) => ({
|
|
475
|
+
title,
|
|
476
|
+
targetIndex: index + additionalNum,
|
|
477
|
+
color: valueColors[index],
|
|
478
|
+
isValueHeader: true,
|
|
479
|
+
}))
|
|
480
|
+
);
|
|
481
|
+
data.maxIndex = valueTitles!.length - 1 + additionalNum;
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
let currentIndex = additionalNum;
|
|
485
|
+
Object.keys(map).forEach((title, index) => {
|
|
486
|
+
const attribute = attributes && attributes[0];
|
|
487
|
+
if (levels === 1 && (valueTitles || []).length <= 1) {
|
|
488
|
+
headers.push({
|
|
489
|
+
title,
|
|
490
|
+
targetIndex: currentIndex,
|
|
491
|
+
color: colors[0],
|
|
492
|
+
constraint: attribute?.constraint || new UnknownConstraint(),
|
|
493
|
+
isValueHeader: false,
|
|
494
|
+
attributeName: attribute?.name,
|
|
495
|
+
});
|
|
496
|
+
data.maxIndex = Math.max(data.maxIndex, currentIndex);
|
|
497
|
+
} else {
|
|
498
|
+
headers.push({
|
|
499
|
+
title,
|
|
500
|
+
color: colors[0],
|
|
501
|
+
constraint: attribute?.constraint || new UnknownConstraint(),
|
|
502
|
+
isValueHeader: false,
|
|
503
|
+
attributeName: attribute?.name,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
this.iterateThroughPivotDataHeader(
|
|
508
|
+
map[title],
|
|
509
|
+
headers[index],
|
|
510
|
+
currentIndex,
|
|
511
|
+
1,
|
|
512
|
+
levels,
|
|
513
|
+
colors,
|
|
514
|
+
valueColors,
|
|
515
|
+
valueTitles || [],
|
|
516
|
+
attributes,
|
|
517
|
+
data
|
|
518
|
+
);
|
|
519
|
+
currentIndex += this.numChildren(map[title], levels - 1, (valueTitles && valueTitles.length) || 1);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {headers, maxIndex: data.maxIndex};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private iterateThroughPivotDataHeader(
|
|
527
|
+
currentMap: Record<string, any>,
|
|
528
|
+
header: LmrPivotDataHeader,
|
|
529
|
+
headerIndex: number,
|
|
530
|
+
level: number,
|
|
531
|
+
maxLevels: number,
|
|
532
|
+
colors: string[],
|
|
533
|
+
valueColors: string[],
|
|
534
|
+
valueTitles: string[],
|
|
535
|
+
attributes: Attribute[],
|
|
536
|
+
additionalData: {maxIndex: number}
|
|
537
|
+
) {
|
|
538
|
+
if (level === maxLevels) {
|
|
539
|
+
if ((valueTitles || []).length > 1) {
|
|
540
|
+
header.children = valueTitles.map((title, index) => ({
|
|
541
|
+
title,
|
|
542
|
+
targetIndex: headerIndex + index,
|
|
543
|
+
color: valueColors[index],
|
|
544
|
+
isValueHeader: true,
|
|
545
|
+
}));
|
|
546
|
+
additionalData.maxIndex = Math.max(additionalData.maxIndex, headerIndex + valueTitles.length - 1);
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
header.children = [];
|
|
552
|
+
let currentIndex = headerIndex;
|
|
553
|
+
Object.keys(currentMap).forEach((title, index) => {
|
|
554
|
+
const attribute = attributes && attributes[level];
|
|
555
|
+
if (level + 1 === maxLevels && (valueTitles || []).length <= 1) {
|
|
556
|
+
header.children!.push({
|
|
557
|
+
title,
|
|
558
|
+
targetIndex: currentIndex,
|
|
559
|
+
color: colors[level],
|
|
560
|
+
constraint: attribute?.constraint || new UnknownConstraint(),
|
|
561
|
+
isValueHeader: false,
|
|
562
|
+
attributeName: attribute?.name,
|
|
563
|
+
});
|
|
564
|
+
additionalData.maxIndex = Math.max(additionalData.maxIndex, currentIndex);
|
|
565
|
+
} else {
|
|
566
|
+
header.children!.push({
|
|
567
|
+
title,
|
|
568
|
+
color: colors[level],
|
|
569
|
+
constraint: attribute?.constraint || new UnknownConstraint(),
|
|
570
|
+
isValueHeader: false,
|
|
571
|
+
attributeName: attribute?.name,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
this.iterateThroughPivotDataHeader(
|
|
576
|
+
currentMap[title],
|
|
577
|
+
header.children?.[index],
|
|
578
|
+
currentIndex,
|
|
579
|
+
level + 1,
|
|
580
|
+
maxLevels,
|
|
581
|
+
colors,
|
|
582
|
+
valueColors,
|
|
583
|
+
valueTitles,
|
|
584
|
+
attributes,
|
|
585
|
+
additionalData
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
currentIndex += this.numChildren(
|
|
589
|
+
currentMap[title],
|
|
590
|
+
maxLevels - (level + 1),
|
|
591
|
+
(valueTitles && valueTitles.length) || 1
|
|
592
|
+
);
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private numChildren(map: Record<string, any>, maxLevels: number, numTitles: number): number {
|
|
597
|
+
if (maxLevels === 0) {
|
|
598
|
+
return numTitles;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const keys = Object.keys(map || {});
|
|
602
|
+
if (maxLevels === 1) {
|
|
603
|
+
return keys.length * numTitles;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const count = keys.reduce((sum, key) => sum + this.numChildrenRecursive(map[key], 1, maxLevels), 0);
|
|
607
|
+
return count * numTitles;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private numChildrenRecursive(map: Record<string, any>, level: number, maxLevels: number): number {
|
|
611
|
+
if (level >= maxLevels) {
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const keys = Object.keys(map || {});
|
|
616
|
+
if (level + 1 === maxLevels) {
|
|
617
|
+
return keys.length;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return keys.reduce((sum, key) => sum + this.numChildrenRecursive(map[key], level + 1, maxLevels), 0);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private createValueTitles(valueAttributes: LmrPivotValueAttribute[]): {titles: string[]; constraints: Constraint[]} {
|
|
624
|
+
return (valueAttributes || []).reduce<{titles: string[]; constraints: Constraint[]}>(
|
|
625
|
+
({titles, constraints}, pivotAttribute) => {
|
|
626
|
+
const attribute = this.findAttributeByPivotAttribute(pivotAttribute);
|
|
627
|
+
constraints.push(
|
|
628
|
+
dataAggregationConstraint(pivotAttribute.aggregation) || this.pivotAttributeConstraint(pivotAttribute)
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const title = this.createValueTitle(pivotAttribute.aggregation, attribute?.name || '');
|
|
632
|
+
titles.push(title);
|
|
633
|
+
|
|
634
|
+
return {titles, constraints};
|
|
635
|
+
},
|
|
636
|
+
{titles: [], constraints: []}
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
public createValueTitle(aggregation: DataAggregationType, attributeName: string): string {
|
|
641
|
+
const valueAggregationTitle = this.transform?.translateAggregation?.(aggregation) || aggregation.toString();
|
|
642
|
+
return `${valueAggregationTitle} ${attributeName || ''}`.trim();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private initMatrix<T>(rows: number, columns: number): T[][] {
|
|
646
|
+
const matrix: T[][] = [];
|
|
647
|
+
for (let i = 0; i < rows; i++) {
|
|
648
|
+
matrix[i] = [];
|
|
649
|
+
for (let j = 0; j < columns; j++) {
|
|
650
|
+
matrix[i][j] = undefined;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return matrix;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private fillValues(
|
|
658
|
+
values: number[][],
|
|
659
|
+
dataResources: DataResource[][][],
|
|
660
|
+
rowHeaders: LmrPivotDataHeader[],
|
|
661
|
+
columnHeaders: LmrPivotDataHeader[],
|
|
662
|
+
valueAttributes: LmrPivotValueAttribute[],
|
|
663
|
+
aggregatedData: AggregatedMapData
|
|
664
|
+
) {
|
|
665
|
+
if (rowHeaders.length > 0) {
|
|
666
|
+
this.iterateThroughRowHeaders(
|
|
667
|
+
values,
|
|
668
|
+
dataResources,
|
|
669
|
+
rowHeaders,
|
|
670
|
+
columnHeaders,
|
|
671
|
+
valueAttributes,
|
|
672
|
+
aggregatedData.map
|
|
673
|
+
);
|
|
674
|
+
} else {
|
|
675
|
+
this.iterateThroughColumnHeaders(values, dataResources, columnHeaders, 0, valueAttributes, aggregatedData.map);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private iterateThroughRowHeaders(
|
|
680
|
+
values: number[][],
|
|
681
|
+
dataResources: DataResource[][][],
|
|
682
|
+
rowHeaders: LmrPivotDataHeader[],
|
|
683
|
+
columnHeaders: LmrPivotDataHeader[],
|
|
684
|
+
valueAttributes: LmrPivotValueAttribute[],
|
|
685
|
+
currentMap: AggregatedDataMap
|
|
686
|
+
) {
|
|
687
|
+
for (const rowHeader of rowHeaders) {
|
|
688
|
+
const rowHeaderMap = currentMap[rowHeader.title] || {};
|
|
689
|
+
|
|
690
|
+
if (rowHeader.children) {
|
|
691
|
+
this.iterateThroughRowHeaders(
|
|
692
|
+
values,
|
|
693
|
+
dataResources,
|
|
694
|
+
rowHeader.children,
|
|
695
|
+
columnHeaders,
|
|
696
|
+
valueAttributes,
|
|
697
|
+
rowHeaderMap
|
|
698
|
+
);
|
|
699
|
+
} else if (isNotNullOrUndefined(rowHeader.targetIndex) && columnHeaders.length > 0) {
|
|
700
|
+
this.iterateThroughColumnHeaders(
|
|
701
|
+
values,
|
|
702
|
+
dataResources,
|
|
703
|
+
columnHeaders,
|
|
704
|
+
rowHeader.targetIndex!,
|
|
705
|
+
valueAttributes,
|
|
706
|
+
rowHeaderMap
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private iterateThroughColumnHeaders(
|
|
713
|
+
values: number[][],
|
|
714
|
+
dataResources: DataResource[][][],
|
|
715
|
+
columnHeaders: LmrPivotDataHeader[],
|
|
716
|
+
rowIndex: number,
|
|
717
|
+
valueAttributes: LmrPivotValueAttribute[],
|
|
718
|
+
currentMap: AggregatedDataMap | AggregatedDataValues[]
|
|
719
|
+
) {
|
|
720
|
+
for (const columnHeader of columnHeaders) {
|
|
721
|
+
if (columnHeader.children) {
|
|
722
|
+
this.iterateThroughColumnHeaders(
|
|
723
|
+
values,
|
|
724
|
+
dataResources,
|
|
725
|
+
columnHeader.children,
|
|
726
|
+
rowIndex,
|
|
727
|
+
valueAttributes,
|
|
728
|
+
currentMap[columnHeader.title] || {}
|
|
729
|
+
);
|
|
730
|
+
} else if (isNotNullOrUndefined(columnHeader.targetIndex)) {
|
|
731
|
+
const aggregatedDataValues = isArray(currentMap) ? currentMap : currentMap[columnHeader.title];
|
|
732
|
+
|
|
733
|
+
if (valueAttributes.length) {
|
|
734
|
+
const valueIndex = columnHeader.targetIndex! % valueAttributes.length;
|
|
735
|
+
const {value, dataResources: aggregatedDataResources} = this.aggregateValue(
|
|
736
|
+
valueAttributes[valueIndex],
|
|
737
|
+
aggregatedDataValues
|
|
738
|
+
);
|
|
739
|
+
values[rowIndex][columnHeader.targetIndex!] = value;
|
|
740
|
+
dataResources[rowIndex][columnHeader.targetIndex!] = aggregatedDataResources || [];
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private aggregateValue(
|
|
747
|
+
valueAttribute: LmrPivotValueAttribute,
|
|
748
|
+
aggregatedDataValues: AggregatedDataValues[]
|
|
749
|
+
): {value?: any; dataResources?: DataResource[]} {
|
|
750
|
+
const resourceAggregatedDataValues = (aggregatedDataValues || []).filter(
|
|
751
|
+
agg => agg.resourceId === valueAttribute.resourceId && agg.type === valueAttribute.resourceType
|
|
752
|
+
);
|
|
753
|
+
if (resourceAggregatedDataValues.length) {
|
|
754
|
+
const dataResources = flattenMatrix(resourceAggregatedDataValues.map(val => val.objects));
|
|
755
|
+
const attribute = this.pivotAttributeAttribute(valueAttribute);
|
|
756
|
+
if (valueAttribute.aggregation === DataAggregationType.Join) {
|
|
757
|
+
// values will be joined in pivot-table-converter
|
|
758
|
+
const values = (dataResources || []).map((resource: DataResource) => resource.data?.[attribute?.id || '']);
|
|
759
|
+
return {value: uniqueValues(flattenValues(values)), dataResources};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const value = attribute && aggregateDataResources(valueAttribute.aggregation, dataResources, attribute, true);
|
|
763
|
+
return {value, dataResources};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return {};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private findAttributeByPivotAttribute(valueAttribute: LmrPivotAttribute): Attribute | undefined {
|
|
770
|
+
if (valueAttribute.resourceType === AttributesResourceType.Collection) {
|
|
771
|
+
return this.collectionsAttributesMap?.[valueAttribute.resourceId]?.[valueAttribute.attributeId];
|
|
772
|
+
} else if (valueAttribute.resourceType === AttributesResourceType.LinkType) {
|
|
773
|
+
return this.linkTypesAttributesMap?.[valueAttribute.resourceId]?.[valueAttribute.attributeId];
|
|
774
|
+
}
|
|
775
|
+
return undefined
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function getPivotStemConfigType(stemConfig: LmrPivotStemConfig): PivotConfigType {
|
|
780
|
+
const rowLength = (stemConfig.rowAttributes || []).length;
|
|
781
|
+
const columnLength = (stemConfig.columnAttributes || []).length;
|
|
782
|
+
|
|
783
|
+
if (rowLength > 0 && columnLength > 0) {
|
|
784
|
+
return PivotConfigType.RowsAndColumns;
|
|
785
|
+
} else if (rowLength > 0) {
|
|
786
|
+
return PivotConfigType.Rows;
|
|
787
|
+
} else if (columnLength > 0) {
|
|
788
|
+
return PivotConfigType.Columns;
|
|
789
|
+
}
|
|
790
|
+
return PivotConfigType.Values;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function canMergeConfigsByType(type: PivotConfigType, c1: LmrPivotStemConfig, c2: LmrPivotStemConfig): boolean {
|
|
794
|
+
if (type === PivotConfigType.Rows) {
|
|
795
|
+
return (c1.rowAttributes || []).length === (c2.rowAttributes || []).length;
|
|
796
|
+
} else if (type === PivotConfigType.Columns) {
|
|
797
|
+
return (c1.columnAttributes || []).length === (c2.columnAttributes || []).length;
|
|
798
|
+
}
|
|
799
|
+
return (
|
|
800
|
+
(c1.rowAttributes || []).length === (c2.rowAttributes || []).length &&
|
|
801
|
+
(c1.columnAttributes || []).length === (c2.columnAttributes || []).length
|
|
802
|
+
);
|
|
803
|
+
}
|