@lightdash/common 0.1369.1 → 0.1369.2
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pivotTable/pivotQueryResults.d.ts +19 -0
- package/dist/pivotTable/pivotQueryResults.js +544 -0
- package/dist/pivotTable/pivotQueryResults.mock.d.ts +8 -0
- package/dist/pivotTable/pivotQueryResults.mock.js +73 -0
- package/dist/pivotTable/pivotQueryResults.test.d.ts +1 -0
- package/dist/pivotTable/pivotQueryResults.test.js +857 -0
- package/dist/types/csv.d.ts +2 -0
- package/dist/types/field.d.ts +1 -0
- package/dist/types/field.js +19 -1
- package/dist/types/scheduler.d.ts +2 -0
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
@@ -49,6 +49,7 @@ export * from './compiler/exploreCompiler';
|
|
49
49
|
export * from './compiler/filtersCompiler';
|
50
50
|
export * from './compiler/translator';
|
51
51
|
export * from './dbt/validation';
|
52
|
+
export * from './pivotTable/pivotQueryResults';
|
52
53
|
export { default as lightdashDbtYamlSchema } from './schemas/json/lightdash-dbt-2.0.json';
|
53
54
|
export * from './templating/template';
|
54
55
|
export * from './types/analytics';
|
package/dist/index.js
CHANGED
@@ -19,6 +19,7 @@ tslib_1.__exportStar(require("./compiler/exploreCompiler"), exports);
|
|
19
19
|
tslib_1.__exportStar(require("./compiler/filtersCompiler"), exports);
|
20
20
|
tslib_1.__exportStar(require("./compiler/translator"), exports);
|
21
21
|
tslib_1.__exportStar(require("./dbt/validation"), exports);
|
22
|
+
tslib_1.__exportStar(require("./pivotTable/pivotQueryResults"), exports);
|
22
23
|
var lightdash_dbt_2_0_json_1 = require("./schemas/json/lightdash-dbt-2.0.json");
|
23
24
|
Object.defineProperty(exports, "lightdashDbtYamlSchema", { enumerable: true, get: function () { return tslib_1.__importDefault(lightdash_dbt_2_0_json_1).default; } });
|
24
25
|
tslib_1.__exportStar(require("./templating/template"), exports);
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { type ItemsMap } from '../types/field';
|
2
|
+
import { type MetricQuery } from '../types/metricQuery';
|
3
|
+
import { type PivotConfig, type PivotData } from '../types/pivot';
|
4
|
+
import { type ResultRow } from '../types/results';
|
5
|
+
type FieldFunction = (fieldId: string) => ItemsMap[string] | undefined;
|
6
|
+
type FieldLabelFunction = (fieldId: string) => string | undefined;
|
7
|
+
type PivotQueryResultsArgs = {
|
8
|
+
pivotConfig: PivotConfig;
|
9
|
+
metricQuery: Pick<MetricQuery, 'dimensions' | 'metrics' | 'tableCalculations' | 'additionalMetrics' | 'customDimensions'>;
|
10
|
+
rows: ResultRow[];
|
11
|
+
options: {
|
12
|
+
maxColumns: number;
|
13
|
+
};
|
14
|
+
getField: FieldFunction;
|
15
|
+
getFieldLabel: FieldLabelFunction;
|
16
|
+
};
|
17
|
+
export declare const pivotQueryResults: ({ pivotConfig, metricQuery, rows, options, getField, getFieldLabel, }: PivotQueryResultsArgs) => PivotData;
|
18
|
+
export declare const pivotResultsAsCsv: (pivotConfig: PivotConfig, rows: ResultRow[], itemMap: ItemsMap, metricQuery: MetricQuery, customLabels: Record<string, string> | undefined, onlyRaw: boolean, maxColumnLimit: number) => string[][];
|
19
|
+
export {};
|
@@ -0,0 +1,544 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.pivotResultsAsCsv = exports.pivotQueryResults = void 0;
|
4
|
+
const tslib_1 = require("tslib");
|
5
|
+
const isNumber_1 = tslib_1.__importDefault(require("lodash/isNumber"));
|
6
|
+
const last_1 = tslib_1.__importDefault(require("lodash/last"));
|
7
|
+
const field_1 = require("../types/field");
|
8
|
+
const formatting_1 = require("../utils/formatting");
|
9
|
+
const isRecursiveRecord = (value) => typeof value === 'object' && value !== null;
|
10
|
+
const create2DArray = (rows, columns, value = null) => Array.from({ length: rows }, () => Array.from({ length: columns }, () => value));
|
11
|
+
const parseNumericValue = (value) => {
|
12
|
+
if (value === null)
|
13
|
+
return 0;
|
14
|
+
const parsedVal = Number(value.raw);
|
15
|
+
return Number.isNaN(parsedVal) ? 0 : parsedVal;
|
16
|
+
};
|
17
|
+
const setIndexByKey = (obj, keys, value) => {
|
18
|
+
if (keys.length === 0) {
|
19
|
+
return false;
|
20
|
+
}
|
21
|
+
const [key, ...rest] = keys;
|
22
|
+
if (rest.length === 0) {
|
23
|
+
if (obj[key] === undefined) {
|
24
|
+
// eslint-disable-next-line no-param-reassign
|
25
|
+
obj[key] = value;
|
26
|
+
return true;
|
27
|
+
}
|
28
|
+
return false;
|
29
|
+
}
|
30
|
+
if (obj[key] === undefined) {
|
31
|
+
// eslint-disable-next-line no-param-reassign
|
32
|
+
obj[key] = {};
|
33
|
+
}
|
34
|
+
const nextObject = obj[key];
|
35
|
+
if (!isRecursiveRecord(nextObject)) {
|
36
|
+
throw new Error('Cannot set key on non-object');
|
37
|
+
}
|
38
|
+
return setIndexByKey(nextObject, rest, value);
|
39
|
+
};
|
40
|
+
const getIndexByKey = (obj, keys) => {
|
41
|
+
if (keys.length === 0) {
|
42
|
+
throw new Error('Cannot get key from empty keys array');
|
43
|
+
}
|
44
|
+
const [key, ...rest] = keys;
|
45
|
+
if (rest.length === 0) {
|
46
|
+
const value = obj[key];
|
47
|
+
if (typeof value !== 'number') {
|
48
|
+
throw new Error('Expected a number');
|
49
|
+
}
|
50
|
+
else {
|
51
|
+
return value;
|
52
|
+
}
|
53
|
+
}
|
54
|
+
else {
|
55
|
+
const nextObj = obj[key];
|
56
|
+
if (isRecursiveRecord(nextObj)) {
|
57
|
+
return getIndexByKey(nextObj, rest);
|
58
|
+
}
|
59
|
+
throw new Error('Expected a RecursiveRecord object');
|
60
|
+
}
|
61
|
+
};
|
62
|
+
const getAllIndicesForFieldId = (obj, fieldId) => {
|
63
|
+
const entries = Object.entries(obj);
|
64
|
+
return entries.reduce((acc, [key, value]) => {
|
65
|
+
if (key === fieldId && (0, isNumber_1.default)(value)) {
|
66
|
+
return [...acc, value];
|
67
|
+
}
|
68
|
+
if (isRecursiveRecord(value)) {
|
69
|
+
return [...acc, ...getAllIndicesForFieldId(value, fieldId)];
|
70
|
+
}
|
71
|
+
return acc;
|
72
|
+
}, []);
|
73
|
+
};
|
74
|
+
const getAllIndices = (obj) => Object.values(obj).reduce((acc, value) => {
|
75
|
+
if ((0, isNumber_1.default)(value)) {
|
76
|
+
return [...acc, value];
|
77
|
+
}
|
78
|
+
return [...acc, ...getAllIndices(value)];
|
79
|
+
}, []);
|
80
|
+
const getAllIndicesByKey = (obj, keys) => {
|
81
|
+
const [key, ...rest] = keys;
|
82
|
+
if (rest.length === 0) {
|
83
|
+
const value = obj[key];
|
84
|
+
if ((0, isNumber_1.default)(value)) {
|
85
|
+
return [value];
|
86
|
+
}
|
87
|
+
return getAllIndices(value);
|
88
|
+
}
|
89
|
+
const nextObj = obj[key];
|
90
|
+
if (isRecursiveRecord(nextObj)) {
|
91
|
+
return getAllIndicesByKey(nextObj, rest);
|
92
|
+
}
|
93
|
+
throw new Error('Expected a RecursiveRecord object');
|
94
|
+
};
|
95
|
+
const getColSpanByKey = (currentColumnPosition, obj, keys) => {
|
96
|
+
const allIndices = getAllIndicesByKey(obj, keys).sort((a, b) => a - b);
|
97
|
+
if (allIndices.length === 0) {
|
98
|
+
throw new Error('Cannot get span from empty indices array');
|
99
|
+
}
|
100
|
+
const currentColumnPositionIndex = allIndices.indexOf(currentColumnPosition);
|
101
|
+
const previousColumnPosition = allIndices[currentColumnPositionIndex - 1];
|
102
|
+
if (currentColumnPositionIndex < 0) {
|
103
|
+
throw new Error('Cannot get span for index that does not exist in indices array');
|
104
|
+
}
|
105
|
+
const isFirstColInSpan = !(0, isNumber_1.default)(previousColumnPosition) ||
|
106
|
+
previousColumnPosition !== currentColumnPosition - 1;
|
107
|
+
if (!isFirstColInSpan) {
|
108
|
+
return 0;
|
109
|
+
}
|
110
|
+
return allIndices
|
111
|
+
.slice(currentColumnPositionIndex)
|
112
|
+
.reduce((acc, curr, i) => {
|
113
|
+
if (curr === currentColumnPosition + i) {
|
114
|
+
return acc + 1;
|
115
|
+
}
|
116
|
+
return acc;
|
117
|
+
}, 0);
|
118
|
+
};
|
119
|
+
const combinedRetrofit = (data, getField, getFieldLabel) => {
|
120
|
+
const indexValues = data.indexValues.length ? data.indexValues : [[]];
|
121
|
+
const baseIdInfo = (0, last_1.default)(data.headerValues);
|
122
|
+
const uniqueIdsForDataValueColumns = Array(data.headerValues[0].length);
|
123
|
+
data.headerValues.forEach((headerRow) => {
|
124
|
+
headerRow.forEach((headerColValue, colIndex) => {
|
125
|
+
uniqueIdsForDataValueColumns[colIndex] = `${(uniqueIdsForDataValueColumns[colIndex] ?? '') +
|
126
|
+
headerColValue.fieldId}__`;
|
127
|
+
});
|
128
|
+
});
|
129
|
+
const getMetricAsRowTotalValueFromAxis = (total, rowIndex) => {
|
130
|
+
const value = (0, last_1.default)(data.indexValues[rowIndex]);
|
131
|
+
if (!value || !value.fieldId)
|
132
|
+
throw new Error('Invalid pivot data');
|
133
|
+
const item = getField(value.fieldId);
|
134
|
+
if (!(0, field_1.isSummable)(item)) {
|
135
|
+
return null;
|
136
|
+
}
|
137
|
+
const formattedValue = (0, formatting_1.formatItemValue)(item, total);
|
138
|
+
return {
|
139
|
+
raw: total,
|
140
|
+
formatted: formattedValue,
|
141
|
+
};
|
142
|
+
};
|
143
|
+
const getRowTotalValueFromAxis = (field, total) => {
|
144
|
+
if (!field || !field.fieldId)
|
145
|
+
throw new Error('Invalid pivot data');
|
146
|
+
const item = getField(field.fieldId);
|
147
|
+
const formattedValue = (0, formatting_1.formatItemValue)(item, total);
|
148
|
+
return {
|
149
|
+
raw: total,
|
150
|
+
formatted: formattedValue,
|
151
|
+
};
|
152
|
+
};
|
153
|
+
let pivotColumnInfo = [];
|
154
|
+
const allCombinedData = indexValues.map((row, rowIndex) => {
|
155
|
+
const newRow = row.map((cell, colIndex) => {
|
156
|
+
if (cell.type === 'label') {
|
157
|
+
const cellValue = getFieldLabel(cell.fieldId);
|
158
|
+
return {
|
159
|
+
...cell,
|
160
|
+
fieldId: `label-${colIndex}`,
|
161
|
+
value: {
|
162
|
+
raw: cellValue,
|
163
|
+
formatted: cellValue,
|
164
|
+
},
|
165
|
+
columnType: 'label',
|
166
|
+
};
|
167
|
+
}
|
168
|
+
return {
|
169
|
+
...cell,
|
170
|
+
columnType: 'indexValue',
|
171
|
+
};
|
172
|
+
});
|
173
|
+
const remappedDataValues = data.dataValues[rowIndex].map((dataValue, colIndex) => {
|
174
|
+
const baseIdInfoForCol = baseIdInfo
|
175
|
+
? baseIdInfo[colIndex]
|
176
|
+
: undefined;
|
177
|
+
const baseId = baseIdInfoForCol?.fieldId;
|
178
|
+
const id = uniqueIdsForDataValueColumns[colIndex] + colIndex;
|
179
|
+
return {
|
180
|
+
baseId,
|
181
|
+
fieldId: id,
|
182
|
+
value: dataValue || {},
|
183
|
+
};
|
184
|
+
});
|
185
|
+
const remappedRowTotals = data.rowTotals?.[rowIndex]?.map((total, colIndex) => {
|
186
|
+
const baseId = `row-total-${colIndex}`;
|
187
|
+
const id = baseId;
|
188
|
+
const underlyingData = (0, last_1.default)(data.rowTotalFields)?.[colIndex];
|
189
|
+
const value = data.pivotConfig.metricsAsRows
|
190
|
+
? getMetricAsRowTotalValueFromAxis(total, rowIndex)
|
191
|
+
: getRowTotalValueFromAxis(underlyingData, total);
|
192
|
+
const underlyingId = data.pivotConfig.metricsAsRows
|
193
|
+
? undefined
|
194
|
+
: underlyingData?.fieldId;
|
195
|
+
return {
|
196
|
+
baseId,
|
197
|
+
fieldId: id,
|
198
|
+
underlyingId,
|
199
|
+
value,
|
200
|
+
columnType: 'rowTotal',
|
201
|
+
};
|
202
|
+
});
|
203
|
+
const entireRow = [
|
204
|
+
...newRow,
|
205
|
+
...remappedDataValues,
|
206
|
+
...(remappedRowTotals || []),
|
207
|
+
];
|
208
|
+
if (rowIndex === 0) {
|
209
|
+
pivotColumnInfo = entireRow.map((cell) => ({
|
210
|
+
fieldId: cell.fieldId,
|
211
|
+
baseId: 'baseId' in cell ? cell.baseId : undefined,
|
212
|
+
underlyingId: 'underlyingId' in cell ? cell.underlyingId : undefined,
|
213
|
+
columnType: 'columnType' in cell ? cell.columnType : undefined,
|
214
|
+
}));
|
215
|
+
}
|
216
|
+
const altRow = {};
|
217
|
+
entireRow.forEach((cell) => {
|
218
|
+
const val = cell.value;
|
219
|
+
if (val && 'formatted' in val && val.formatted !== undefined) {
|
220
|
+
altRow[cell.fieldId] = {
|
221
|
+
value: {
|
222
|
+
raw: val.raw,
|
223
|
+
formatted: val.formatted,
|
224
|
+
},
|
225
|
+
};
|
226
|
+
}
|
227
|
+
});
|
228
|
+
return altRow;
|
229
|
+
});
|
230
|
+
// eslint-disable-next-line no-param-reassign
|
231
|
+
data.retrofitData = { allCombinedData, pivotColumnInfo };
|
232
|
+
return data;
|
233
|
+
};
|
234
|
+
const pivotQueryResults = ({ pivotConfig, metricQuery, rows, options, getField, getFieldLabel, }) => {
|
235
|
+
if (rows.length === 0) {
|
236
|
+
throw new Error('Cannot pivot results with no rows');
|
237
|
+
}
|
238
|
+
const hiddenMetricFieldIds = pivotConfig.hiddenMetricFieldIds || [];
|
239
|
+
const summableMetricFieldIds = pivotConfig.summableMetricFieldIds || [];
|
240
|
+
const columnOrder = (pivotConfig.columnOrder || []).filter((id) => !hiddenMetricFieldIds.includes(id));
|
241
|
+
const dimensions = [...metricQuery.dimensions];
|
242
|
+
// Headers (column index)
|
243
|
+
const headerDimensions = pivotConfig.pivotDimensions.filter((pivotDimension) => dimensions.includes(pivotDimension));
|
244
|
+
const headerDimensionValueTypes = headerDimensions.map((d) => ({
|
245
|
+
type: field_1.FieldType.DIMENSION,
|
246
|
+
fieldId: d,
|
247
|
+
}));
|
248
|
+
const headerMetricValueTypes = pivotConfig.metricsAsRows ? [] : [{ type: field_1.FieldType.METRIC }];
|
249
|
+
const headerValueTypes = [
|
250
|
+
...headerDimensionValueTypes,
|
251
|
+
...headerMetricValueTypes,
|
252
|
+
];
|
253
|
+
// Indices (row index)
|
254
|
+
const indexDimensions = dimensions
|
255
|
+
.filter((d) => !pivotConfig.pivotDimensions.includes(d))
|
256
|
+
.slice()
|
257
|
+
.sort((a, b) => columnOrder.indexOf(a) - columnOrder.indexOf(b));
|
258
|
+
const indexDimensionValueTypes = indexDimensions.map((d) => ({
|
259
|
+
type: field_1.FieldType.DIMENSION,
|
260
|
+
fieldId: d,
|
261
|
+
}));
|
262
|
+
const indexMetricValueTypes = pivotConfig.metricsAsRows ? [{ type: field_1.FieldType.METRIC }] : [];
|
263
|
+
const indexValueTypes = [
|
264
|
+
...indexDimensionValueTypes,
|
265
|
+
...indexMetricValueTypes,
|
266
|
+
];
|
267
|
+
// Metrics
|
268
|
+
const metrics = [
|
269
|
+
...metricQuery.metrics,
|
270
|
+
...metricQuery.tableCalculations.map((tc) => tc.name),
|
271
|
+
]
|
272
|
+
.filter((m) => !hiddenMetricFieldIds.includes(m))
|
273
|
+
.sort((a, b) => columnOrder.indexOf(a) - columnOrder.indexOf(b))
|
274
|
+
.map((id) => ({ fieldId: id }));
|
275
|
+
if (metrics.length === 0) {
|
276
|
+
throw new Error('Cannot pivot results with no metrics');
|
277
|
+
}
|
278
|
+
const N_ROWS = rows.length;
|
279
|
+
// For every row in the results, compute the index and header values to determine the shape of the result set
|
280
|
+
const indexValues = [];
|
281
|
+
const headerValuesT = [];
|
282
|
+
const rowIndices = {};
|
283
|
+
const columnIndices = {};
|
284
|
+
let rowCount = 0;
|
285
|
+
let columnCount = 0;
|
286
|
+
for (let nRow = 0; nRow < N_ROWS; nRow += 1) {
|
287
|
+
const row = rows[nRow];
|
288
|
+
for (let nMetric = 0; nMetric < metrics.length; nMetric += 1) {
|
289
|
+
const metric = metrics[nMetric];
|
290
|
+
const indexRowValues = indexDimensions
|
291
|
+
.map((fieldId) => ({
|
292
|
+
type: 'value',
|
293
|
+
fieldId,
|
294
|
+
value: row[fieldId].value,
|
295
|
+
colSpan: 1,
|
296
|
+
}))
|
297
|
+
.concat(pivotConfig.metricsAsRows
|
298
|
+
? [
|
299
|
+
{
|
300
|
+
type: 'label',
|
301
|
+
fieldId: metric.fieldId,
|
302
|
+
},
|
303
|
+
]
|
304
|
+
: []);
|
305
|
+
const headerRowValues = headerDimensions
|
306
|
+
.map((fieldId) => ({
|
307
|
+
type: 'value',
|
308
|
+
fieldId,
|
309
|
+
value: row[fieldId].value,
|
310
|
+
colSpan: 1,
|
311
|
+
}))
|
312
|
+
.concat(pivotConfig.metricsAsRows
|
313
|
+
? []
|
314
|
+
: [
|
315
|
+
{
|
316
|
+
type: 'label',
|
317
|
+
fieldId: metric.fieldId,
|
318
|
+
},
|
319
|
+
]);
|
320
|
+
// Write the index values
|
321
|
+
if (setIndexByKey(rowIndices, indexRowValues.map((l) => l.type === 'value' ? String(l.value?.raw) : l.fieldId), rowCount)) {
|
322
|
+
rowCount += 1;
|
323
|
+
indexValues.push(indexRowValues);
|
324
|
+
}
|
325
|
+
// Write the header values
|
326
|
+
if (setIndexByKey(columnIndices, headerRowValues.map((l) => l.type === 'value' ? String(l.value.raw) : l.fieldId), columnCount)) {
|
327
|
+
columnCount += 1;
|
328
|
+
if (columnCount > options.maxColumns) {
|
329
|
+
throw new Error(`Cannot pivot results with more than ${options.maxColumns} columns. Try adding a filter to limit your results.`);
|
330
|
+
}
|
331
|
+
headerValuesT.push(headerRowValues);
|
332
|
+
}
|
333
|
+
}
|
334
|
+
}
|
335
|
+
const headerValues = headerValuesT[0]?.map((_, colIndex) => headerValuesT.map((row, rowIndex) => {
|
336
|
+
const cell = row[colIndex];
|
337
|
+
if (cell.type === 'label') {
|
338
|
+
return cell;
|
339
|
+
}
|
340
|
+
const keys = row
|
341
|
+
.slice(0, colIndex + 1)
|
342
|
+
.reduce((acc, l) => l.type === 'value'
|
343
|
+
? [...acc, String(l.value.raw)]
|
344
|
+
: acc, []);
|
345
|
+
const cellWithSpan = {
|
346
|
+
...cell,
|
347
|
+
colSpan: getColSpanByKey(rowIndex, columnIndices, keys),
|
348
|
+
};
|
349
|
+
return cellWithSpan;
|
350
|
+
})) ?? [];
|
351
|
+
const hasIndex = indexValueTypes.length > 0;
|
352
|
+
const hasHeader = headerValueTypes.length > 0;
|
353
|
+
// Compute the size of the data values
|
354
|
+
const N_DATA_ROWS = hasIndex ? rowCount : 1;
|
355
|
+
const N_DATA_COLUMNS = hasHeader ? columnCount : 1;
|
356
|
+
// Compute the data values
|
357
|
+
const dataValues = create2DArray(N_DATA_ROWS, N_DATA_COLUMNS);
|
358
|
+
if (N_DATA_ROWS === 0 || N_DATA_COLUMNS === 0) {
|
359
|
+
throw new Error('Cannot pivot results with no data');
|
360
|
+
}
|
361
|
+
// Compute pivoted data
|
362
|
+
for (let nRow = 0; nRow < N_ROWS; nRow += 1) {
|
363
|
+
const row = rows[nRow];
|
364
|
+
for (let nMetric = 0; nMetric < metrics.length; nMetric += 1) {
|
365
|
+
const metric = metrics[nMetric];
|
366
|
+
const { value } = row[metric.fieldId];
|
367
|
+
const rowKeys = [
|
368
|
+
...indexDimensions.map((d) => row[d].value.raw),
|
369
|
+
...(pivotConfig.metricsAsRows ? [metric.fieldId] : []),
|
370
|
+
];
|
371
|
+
const columnKeys = [
|
372
|
+
...headerDimensions.map((d) => row[d].value.raw),
|
373
|
+
...(pivotConfig.metricsAsRows ? [] : [metric.fieldId]),
|
374
|
+
];
|
375
|
+
const rowKeysString = rowKeys.map(String);
|
376
|
+
const columnKeysString = columnKeys.map(String);
|
377
|
+
const rowIndex = hasIndex
|
378
|
+
? getIndexByKey(rowIndices, rowKeysString)
|
379
|
+
: 0;
|
380
|
+
const columnIndex = hasHeader
|
381
|
+
? getIndexByKey(columnIndices, columnKeysString)
|
382
|
+
: 0;
|
383
|
+
dataValues[rowIndex][columnIndex] = value;
|
384
|
+
}
|
385
|
+
}
|
386
|
+
// compute row totals
|
387
|
+
let rowTotalFields;
|
388
|
+
let rowTotals;
|
389
|
+
if (pivotConfig.rowTotals && hasHeader) {
|
390
|
+
if (pivotConfig.metricsAsRows) {
|
391
|
+
const N_TOTAL_COLS = 1;
|
392
|
+
const N_TOTAL_ROWS = headerValues.length;
|
393
|
+
rowTotalFields = create2DArray(N_TOTAL_ROWS, N_TOTAL_COLS);
|
394
|
+
rowTotals = create2DArray(N_DATA_ROWS, N_TOTAL_COLS);
|
395
|
+
// set the last header cell as the "Total"
|
396
|
+
rowTotalFields[N_TOTAL_ROWS - 1][N_TOTAL_COLS - 1] = {
|
397
|
+
fieldId: undefined,
|
398
|
+
};
|
399
|
+
rowTotals = rowTotals.map((row, rowIndex) => row.map(() => dataValues[rowIndex].reduce((acc, value) => acc + parseNumericValue(value), 0)));
|
400
|
+
}
|
401
|
+
else {
|
402
|
+
const N_TOTAL_COLS = summableMetricFieldIds.length;
|
403
|
+
const N_TOTAL_ROWS = headerValues.length;
|
404
|
+
rowTotalFields = create2DArray(N_TOTAL_ROWS, N_TOTAL_COLS);
|
405
|
+
rowTotals = create2DArray(N_DATA_ROWS, N_TOTAL_COLS);
|
406
|
+
summableMetricFieldIds.forEach((fieldId, metricIndex) => {
|
407
|
+
rowTotalFields[N_TOTAL_ROWS - 1][metricIndex] = {
|
408
|
+
fieldId,
|
409
|
+
};
|
410
|
+
});
|
411
|
+
rowTotals = rowTotals.map((row, rowIndex) => row.map((_, totalColIndex) => {
|
412
|
+
const totalColFieldId = rowTotalFields[N_TOTAL_ROWS - 1][totalColIndex]
|
413
|
+
?.fieldId;
|
414
|
+
const valueColIndices = totalColFieldId
|
415
|
+
? getAllIndicesForFieldId(columnIndices, totalColFieldId)
|
416
|
+
: [];
|
417
|
+
return dataValues[rowIndex]
|
418
|
+
.filter((__, dataValueColIndex) => valueColIndices.includes(dataValueColIndex))
|
419
|
+
.reduce((acc, value) => acc + parseNumericValue(value), 0);
|
420
|
+
}));
|
421
|
+
}
|
422
|
+
}
|
423
|
+
let columnTotalFields;
|
424
|
+
let columnTotals;
|
425
|
+
if (pivotConfig.columnTotals && hasIndex) {
|
426
|
+
if (pivotConfig.metricsAsRows) {
|
427
|
+
const N_TOTAL_ROWS = summableMetricFieldIds.length;
|
428
|
+
const N_TOTAL_COLS = indexValueTypes.length;
|
429
|
+
columnTotalFields = create2DArray(N_TOTAL_ROWS, N_TOTAL_COLS);
|
430
|
+
columnTotals = create2DArray(N_TOTAL_ROWS, N_DATA_COLUMNS);
|
431
|
+
summableMetricFieldIds.forEach((fieldId, metricIndex) => {
|
432
|
+
columnTotalFields[metricIndex][N_TOTAL_COLS - 1] = {
|
433
|
+
fieldId,
|
434
|
+
};
|
435
|
+
});
|
436
|
+
columnTotals = columnTotals.map((row, rowIndex) => row.map((_, totalColIndex) => {
|
437
|
+
const totalColFieldId = columnTotalFields[rowIndex][N_TOTAL_COLS - 1]?.fieldId;
|
438
|
+
const valueColIndices = totalColFieldId
|
439
|
+
? getAllIndicesForFieldId(rowIndices, totalColFieldId)
|
440
|
+
: [];
|
441
|
+
return dataValues
|
442
|
+
.filter((__, dataValueColIndex) => valueColIndices.includes(dataValueColIndex))
|
443
|
+
.reduce((acc, value) => acc + parseNumericValue(value[totalColIndex]), 0);
|
444
|
+
}));
|
445
|
+
}
|
446
|
+
else {
|
447
|
+
const N_TOTAL_COLS = indexValues[0].length;
|
448
|
+
const N_TOTAL_ROWS = 1;
|
449
|
+
columnTotalFields = create2DArray(N_TOTAL_ROWS, N_TOTAL_COLS);
|
450
|
+
columnTotals = create2DArray(N_TOTAL_ROWS, N_DATA_COLUMNS);
|
451
|
+
// set the last index cell as the "Total"
|
452
|
+
columnTotalFields[N_TOTAL_ROWS - 1][N_TOTAL_COLS - 1] = {
|
453
|
+
fieldId: undefined,
|
454
|
+
};
|
455
|
+
columnTotals = columnTotals.map((row, _totalRowIndex) => row.map((_col, colIndex) => dataValues
|
456
|
+
.map((dataRow) => dataRow[colIndex])
|
457
|
+
.reduce((acc, value) => acc + parseNumericValue(value), 0)));
|
458
|
+
}
|
459
|
+
}
|
460
|
+
const titleFields = create2DArray(hasHeader ? headerValueTypes.length : 1, hasIndex ? indexValueTypes.length : 1);
|
461
|
+
headerValueTypes.forEach((headerValueType, headerIndex) => {
|
462
|
+
if (headerValueType.type === field_1.FieldType.DIMENSION) {
|
463
|
+
titleFields[headerIndex][hasIndex ? indexValueTypes.length - 1 : 0] = {
|
464
|
+
fieldId: headerValueType.fieldId,
|
465
|
+
direction: 'header',
|
466
|
+
};
|
467
|
+
}
|
468
|
+
});
|
469
|
+
indexValueTypes.forEach((indexValueType, indexIndex) => {
|
470
|
+
if (indexValueType.type === field_1.FieldType.DIMENSION) {
|
471
|
+
titleFields[hasHeader ? headerValueTypes.length - 1 : 0][indexIndex] = {
|
472
|
+
fieldId: indexValueType.fieldId,
|
473
|
+
direction: 'index',
|
474
|
+
};
|
475
|
+
}
|
476
|
+
});
|
477
|
+
const cellsCount = (indexValueTypes.length === 0 ? titleFields[0].length : 0) +
|
478
|
+
indexValueTypes.length +
|
479
|
+
dataValues[0].length +
|
480
|
+
(pivotConfig.rowTotals && rowTotals ? rowTotals[0].length : 0);
|
481
|
+
const rowsCount = dataValues.length || 0;
|
482
|
+
const pivotData = {
|
483
|
+
titleFields,
|
484
|
+
headerValueTypes,
|
485
|
+
headerValues,
|
486
|
+
indexValueTypes,
|
487
|
+
indexValues,
|
488
|
+
dataColumnCount: N_DATA_COLUMNS,
|
489
|
+
dataValues,
|
490
|
+
rowTotalFields,
|
491
|
+
columnTotalFields,
|
492
|
+
rowTotals,
|
493
|
+
columnTotals,
|
494
|
+
cellsCount,
|
495
|
+
rowsCount,
|
496
|
+
pivotConfig,
|
497
|
+
retrofitData: {
|
498
|
+
allCombinedData: [],
|
499
|
+
pivotColumnInfo: [],
|
500
|
+
},
|
501
|
+
};
|
502
|
+
return combinedRetrofit(pivotData, getField, getFieldLabel);
|
503
|
+
};
|
504
|
+
exports.pivotQueryResults = pivotQueryResults;
|
505
|
+
const pivotResultsAsCsv = (pivotConfig, rows, itemMap, metricQuery, customLabels, onlyRaw, maxColumnLimit) => {
|
506
|
+
const getFieldLabel = (fieldId) => {
|
507
|
+
const customLabel = customLabels?.[fieldId];
|
508
|
+
if (customLabel !== undefined)
|
509
|
+
return customLabel;
|
510
|
+
const field = itemMap[fieldId];
|
511
|
+
return (field && (0, field_1.isField)(field) && field?.label) || fieldId;
|
512
|
+
};
|
513
|
+
const pivotedResults = (0, exports.pivotQueryResults)({
|
514
|
+
pivotConfig,
|
515
|
+
metricQuery,
|
516
|
+
rows,
|
517
|
+
options: {
|
518
|
+
maxColumns: maxColumnLimit,
|
519
|
+
},
|
520
|
+
getField: (fieldId) => itemMap && itemMap[fieldId],
|
521
|
+
getFieldLabel,
|
522
|
+
});
|
523
|
+
const formatField = onlyRaw ? 'raw' : 'formatted';
|
524
|
+
const headers = pivotedResults.headerValues.reduce((acc, row, i) => {
|
525
|
+
const values = row.map((header) => 'value' in header
|
526
|
+
? header.value[formatField]
|
527
|
+
: getFieldLabel(header.fieldId));
|
528
|
+
const fields = pivotedResults.titleFields[i];
|
529
|
+
const fieldLabels = fields.map((field) => field ? getFieldLabel(field.fieldId) : '-');
|
530
|
+
acc[i] = [...fieldLabels, ...values];
|
531
|
+
return acc;
|
532
|
+
}, [[]]);
|
533
|
+
const fieldIds = Object.values(pivotedResults.retrofitData.pivotColumnInfo).map((field) => field.fieldId);
|
534
|
+
const hasIndex = pivotedResults.indexValues.length > 0;
|
535
|
+
const pivotedRows = pivotedResults.retrofitData.allCombinedData.map((row) => {
|
536
|
+
// Fields that return `null` don't appear in the pivot table
|
537
|
+
// If there are no index fields, we need to add an empty string to the beginning of the row
|
538
|
+
const noIndexPrefix = hasIndex ? [] : [''];
|
539
|
+
const formattedRows = fieldIds.map((fieldId) => row[fieldId]?.value?.[formatField] || '-');
|
540
|
+
return [...noIndexPrefix, ...formattedRows];
|
541
|
+
});
|
542
|
+
return [...headers, ...pivotedRows];
|
543
|
+
};
|
544
|
+
exports.pivotResultsAsCsv = pivotResultsAsCsv;
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import { type MetricQuery } from '../types/metricQuery';
|
2
|
+
import { type ResultRow } from '../types/results';
|
3
|
+
export declare const METRIC_QUERY_2DIM_2METRIC: Pick<MetricQuery, 'metrics' | 'dimensions' | 'tableCalculations' | 'additionalMetrics'>;
|
4
|
+
export declare const RESULT_ROWS_2DIM_2METRIC: ResultRow[];
|
5
|
+
export declare const METRIC_QUERY_1DIM_2METRIC: Pick<MetricQuery, 'metrics' | 'dimensions' | 'tableCalculations' | 'additionalMetrics'>;
|
6
|
+
export declare const RESULT_ROWS_1DIM_2METRIC: ResultRow[];
|
7
|
+
export declare const METRIC_QUERY_0DIM_2METRIC: Pick<MetricQuery, 'metrics' | 'dimensions' | 'tableCalculations' | 'additionalMetrics'>;
|
8
|
+
export declare const RESULT_ROWS_0DIM_2METRIC: ResultRow[];
|
@@ -0,0 +1,73 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.RESULT_ROWS_0DIM_2METRIC = exports.METRIC_QUERY_0DIM_2METRIC = exports.RESULT_ROWS_1DIM_2METRIC = exports.METRIC_QUERY_1DIM_2METRIC = exports.RESULT_ROWS_2DIM_2METRIC = exports.METRIC_QUERY_2DIM_2METRIC = void 0;
|
4
|
+
exports.METRIC_QUERY_2DIM_2METRIC = {
|
5
|
+
metrics: ['views', 'devices'],
|
6
|
+
dimensions: ['page', 'site'],
|
7
|
+
tableCalculations: [],
|
8
|
+
};
|
9
|
+
exports.RESULT_ROWS_2DIM_2METRIC = [
|
10
|
+
{
|
11
|
+
page: { value: { raw: '/home', formatted: '/home' } },
|
12
|
+
site: { value: { raw: 'blog', formatted: 'Blog' } },
|
13
|
+
views: { value: { raw: 6, formatted: '6.0' } },
|
14
|
+
devices: { value: { raw: 7, formatted: '7.0' } },
|
15
|
+
},
|
16
|
+
{
|
17
|
+
page: { value: { raw: '/about', formatted: '/about' } },
|
18
|
+
site: { value: { raw: 'blog', formatted: 'Blog' } },
|
19
|
+
views: { value: { raw: 12, formatted: '12.0' } },
|
20
|
+
devices: { value: { raw: 0, formatted: '0.0' } },
|
21
|
+
},
|
22
|
+
{
|
23
|
+
page: { value: { raw: '/first-post', formatted: '/first-post' } },
|
24
|
+
site: { value: { raw: 'blog', formatted: 'Blog' } },
|
25
|
+
views: { value: { raw: 11, formatted: '11.0' } },
|
26
|
+
devices: { value: { raw: 1, formatted: '1.0' } },
|
27
|
+
},
|
28
|
+
{
|
29
|
+
page: { value: { raw: '/home', formatted: '/home' } },
|
30
|
+
site: { value: { raw: 'docs', formatted: 'Docs' } },
|
31
|
+
views: { value: { raw: 2, formatted: '2.0' } },
|
32
|
+
devices: { value: { raw: 10, formatted: '10.0' } },
|
33
|
+
},
|
34
|
+
{
|
35
|
+
page: { value: { raw: '/about', formatted: '/about' } },
|
36
|
+
site: { value: { raw: 'docs', formatted: 'Docs' } },
|
37
|
+
views: { value: { raw: 2, formatted: '2.0' } },
|
38
|
+
devices: { value: { raw: 13, formatted: '13.0' } },
|
39
|
+
},
|
40
|
+
];
|
41
|
+
exports.METRIC_QUERY_1DIM_2METRIC = {
|
42
|
+
metrics: ['views', 'devices'],
|
43
|
+
dimensions: ['page'],
|
44
|
+
tableCalculations: [],
|
45
|
+
};
|
46
|
+
exports.RESULT_ROWS_1DIM_2METRIC = [
|
47
|
+
{
|
48
|
+
page: { value: { raw: '/home', formatted: '/home' } },
|
49
|
+
views: { value: { raw: 6, formatted: '6.0' } },
|
50
|
+
devices: { value: { raw: 7, formatted: '7.0' } },
|
51
|
+
},
|
52
|
+
{
|
53
|
+
page: { value: { raw: '/about', formatted: '/about' } },
|
54
|
+
views: { value: { raw: 12, formatted: '12.0' } },
|
55
|
+
devices: { value: { raw: 0, formatted: '0.0' } },
|
56
|
+
},
|
57
|
+
{
|
58
|
+
page: { value: { raw: '/first-post', formatted: '/first-post' } },
|
59
|
+
views: { value: { raw: 11, formatted: '11.0' } },
|
60
|
+
devices: { value: { raw: 1, formatted: '1.0' } },
|
61
|
+
},
|
62
|
+
];
|
63
|
+
exports.METRIC_QUERY_0DIM_2METRIC = {
|
64
|
+
metrics: ['views', 'devices'],
|
65
|
+
dimensions: [],
|
66
|
+
tableCalculations: [],
|
67
|
+
};
|
68
|
+
exports.RESULT_ROWS_0DIM_2METRIC = [
|
69
|
+
{
|
70
|
+
views: { value: { raw: 6, formatted: '6.0' } },
|
71
|
+
devices: { value: { raw: 7, formatted: '7.0' } },
|
72
|
+
},
|
73
|
+
];
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|