@lightdash/common 0.1369.1 → 0.1369.2
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/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 {};
|