@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.
@@ -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
+ }