@cubejs-client/core 1.3.14 → 1.3.16
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/{cubejs-client-core.js → cubejs-client-core.cjs.js} +1016 -411
- package/dist/cubejs-client-core.cjs.js.map +1 -0
- package/dist/cubejs-client-core.umd.js +2901 -12088
- package/dist/cubejs-client-core.umd.js.map +1 -1
- package/dist/src/HttpTransport.d.ts +54 -0
- package/dist/src/HttpTransport.d.ts.map +1 -0
- package/dist/src/HttpTransport.js +55 -0
- package/dist/src/Meta.d.ts +62 -0
- package/dist/src/Meta.d.ts.map +1 -0
- package/dist/src/Meta.js +150 -0
- package/dist/src/ProgressResult.d.ts +8 -0
- package/dist/src/ProgressResult.d.ts.map +1 -0
- package/dist/src/ProgressResult.js +11 -0
- package/dist/src/RequestError.d.ts +6 -0
- package/dist/src/RequestError.d.ts.map +1 -0
- package/dist/src/RequestError.js +7 -0
- package/dist/src/ResultSet.d.ts +430 -0
- package/dist/src/ResultSet.d.ts.map +1 -0
- package/dist/src/ResultSet.js +952 -0
- package/dist/src/SqlQuery.d.ts +17 -0
- package/dist/src/SqlQuery.d.ts.map +1 -0
- package/dist/src/SqlQuery.js +11 -0
- package/dist/src/index.d.ts +194 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +411 -0
- package/dist/src/index.umd.d.ts +3 -0
- package/dist/src/index.umd.d.ts.map +1 -0
- package/dist/src/index.umd.js +6 -0
- package/dist/src/time.d.ts +70 -0
- package/dist/src/time.d.ts.map +1 -0
- package/dist/src/time.js +249 -0
- package/dist/src/types.d.ts +424 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +19 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +294 -0
- package/dist/test/CubeApi.test.d.ts +7 -0
- package/dist/test/CubeApi.test.d.ts.map +1 -0
- package/dist/test/CubeApi.test.js +279 -0
- package/dist/test/HttpTransport.test.d.ts +2 -0
- package/dist/test/HttpTransport.test.d.ts.map +1 -0
- package/dist/test/HttpTransport.test.js +244 -0
- package/dist/test/ResultSet.test.d.ts +7 -0
- package/dist/test/ResultSet.test.d.ts.map +1 -0
- package/dist/test/ResultSet.test.js +1725 -0
- package/dist/test/compare-date-range.test.d.ts +2 -0
- package/dist/test/compare-date-range.test.d.ts.map +1 -0
- package/dist/test/compare-date-range.test.js +742 -0
- package/dist/test/data-blending.test.d.ts +2 -0
- package/dist/test/data-blending.test.d.ts.map +1 -0
- package/dist/test/data-blending.test.js +423 -0
- package/dist/test/default-heuristics.test.d.ts +2 -0
- package/dist/test/default-heuristics.test.d.ts.map +1 -0
- package/dist/test/default-heuristics.test.js +108 -0
- package/dist/test/drill-down.test.d.ts +2 -0
- package/dist/test/drill-down.test.d.ts.map +1 -0
- package/dist/test/drill-down.test.js +373 -0
- package/dist/test/fixtures/datablending/load-responses.json +261 -0
- package/dist/test/granularity.test.d.ts +2 -0
- package/dist/test/granularity.test.d.ts.map +1 -0
- package/dist/test/granularity.test.js +218 -0
- package/dist/test/helpers.d.ts +283 -0
- package/dist/test/helpers.d.ts.map +1 -0
- package/dist/test/helpers.js +974 -0
- package/dist/test/index.test.d.ts +7 -0
- package/dist/test/index.test.d.ts.map +1 -0
- package/dist/test/index.test.js +370 -0
- package/dist/test/table.test.d.ts +2 -0
- package/dist/test/table.test.d.ts.map +1 -0
- package/dist/test/table.test.js +757 -0
- package/dist/test/utils.test.d.ts +2 -0
- package/dist/test/utils.test.d.ts.map +1 -0
- package/dist/test/utils.test.js +32 -0
- package/package.json +26 -21
- package/dist/cubejs-client-core.esm.js +0 -1639
- package/dist/cubejs-client-core.esm.js.map +0 -1
- package/dist/cubejs-client-core.js.map +0 -1
- package/index.d.ts +0 -1338
- package/src/HttpTransport.js +0 -60
- package/src/HttpTransport.test.js +0 -117
- package/src/Meta.js +0 -142
- package/src/ProgressResult.js +0 -13
- package/src/RequestError.js +0 -7
- package/src/ResultSet.js +0 -746
- package/src/SqlQuery.js +0 -13
- package/src/index.js +0 -398
- package/src/index.test.js +0 -454
- package/src/index.umd.js +0 -8
- package/src/tests/ResultSet.test.js +0 -1655
- package/src/tests/compare-date-range.test.js +0 -753
- package/src/tests/data-blending.test.js +0 -432
- package/src/tests/default-heuristics.test.js +0 -118
- package/src/tests/drill-down.test.js +0 -402
- package/src/tests/fixtures/datablending/load-responses.json +0 -261
- package/src/tests/granularity.test.js +0 -225
- package/src/tests/table.test.js +0 -791
- package/src/tests/utils.test.js +0 -35
- package/src/time.js +0 -296
- package/src/utils.js +0 -368
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import { groupBy, pipe, fromPairs, uniq, map, dropLast, equals, reduce, minBy, maxBy, clone, mergeDeepLeft, flatten, } from 'ramda';
|
|
3
|
+
import { aliasSeries } from './utils';
|
|
4
|
+
import { DateRegex, dayRange, internalDayjs, isPredefinedGranularity, LocalDateRegex, TIME_SERIES, timeSeriesFromCustomInterval } from './time';
|
|
5
|
+
const groupByToPairs = function groupByToPairsImpl(keyFn) {
|
|
6
|
+
const acc = new Map();
|
|
7
|
+
return (data) => {
|
|
8
|
+
data.forEach((row) => {
|
|
9
|
+
const key = keyFn(row);
|
|
10
|
+
if (!acc.has(key)) {
|
|
11
|
+
acc.set(key, []);
|
|
12
|
+
}
|
|
13
|
+
acc.get(key).push(row);
|
|
14
|
+
});
|
|
15
|
+
return Array.from(acc.entries());
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
const unnest = (arr) => {
|
|
19
|
+
const res = [];
|
|
20
|
+
arr.forEach((subArr) => {
|
|
21
|
+
subArr.forEach(element => res.push(element));
|
|
22
|
+
});
|
|
23
|
+
return res;
|
|
24
|
+
};
|
|
25
|
+
export const QUERY_TYPE = {
|
|
26
|
+
REGULAR_QUERY: 'regularQuery',
|
|
27
|
+
COMPARE_DATE_RANGE_QUERY: 'compareDateRangeQuery',
|
|
28
|
+
BLENDING_QUERY: 'blendingQuery',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Provides a convenient interface for data manipulation.
|
|
32
|
+
*/
|
|
33
|
+
export default class ResultSet {
|
|
34
|
+
static measureFromAxis(axisValues) {
|
|
35
|
+
return axisValues[axisValues.length - 1];
|
|
36
|
+
}
|
|
37
|
+
static timeDimensionMember(td) {
|
|
38
|
+
return `${td.dimension}.${td.granularity}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* ```js
|
|
42
|
+
* import { ResultSet } from '@cubejs-client/core';
|
|
43
|
+
*
|
|
44
|
+
* const resultSet = await cubeApi.load(query);
|
|
45
|
+
* // You can store the result somewhere
|
|
46
|
+
* const tmp = resultSet.serialize();
|
|
47
|
+
*
|
|
48
|
+
* // and restore it later
|
|
49
|
+
* const resultSet = ResultSet.deserialize(tmp);
|
|
50
|
+
* ```
|
|
51
|
+
* @param data the result of [serialize](#result-set-serialize)
|
|
52
|
+
* @param options
|
|
53
|
+
*/
|
|
54
|
+
static deserialize(data, options) {
|
|
55
|
+
return new ResultSet(data.loadResponse, options);
|
|
56
|
+
}
|
|
57
|
+
constructor(loadResponse, options = {}) {
|
|
58
|
+
if ('queryType' in loadResponse && loadResponse.queryType != null) {
|
|
59
|
+
this.loadResponse = loadResponse;
|
|
60
|
+
this.queryType = loadResponse.queryType;
|
|
61
|
+
this.loadResponses = loadResponse.results;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.queryType = QUERY_TYPE.REGULAR_QUERY;
|
|
65
|
+
this.loadResponse = {
|
|
66
|
+
...loadResponse,
|
|
67
|
+
pivotQuery: {
|
|
68
|
+
...loadResponse.query,
|
|
69
|
+
queryType: this.queryType
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
this.loadResponses = [loadResponse];
|
|
73
|
+
}
|
|
74
|
+
if (!Object.values(QUERY_TYPE).includes(this.queryType)) {
|
|
75
|
+
throw new Error('Unknown query type');
|
|
76
|
+
}
|
|
77
|
+
this.parseDateMeasures = options.parseDateMeasures;
|
|
78
|
+
this.options = options;
|
|
79
|
+
this.backwardCompatibleData = [];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns a measure drill down query.
|
|
83
|
+
*
|
|
84
|
+
* Provided you have a measure with the defined `drillMembers` on the `Orders` cube
|
|
85
|
+
* ```js
|
|
86
|
+
* measures: {
|
|
87
|
+
* count: {
|
|
88
|
+
* type: `count`,
|
|
89
|
+
* drillMembers: [Orders.status, Users.city, count],
|
|
90
|
+
* },
|
|
91
|
+
* // ...
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* Then you can use the `drillDown` method to see the rows that contribute to that metric
|
|
96
|
+
* ```js
|
|
97
|
+
* resultSet.drillDown(
|
|
98
|
+
* {
|
|
99
|
+
* xValues,
|
|
100
|
+
* yValues,
|
|
101
|
+
* },
|
|
102
|
+
* // you should pass the `pivotConfig` if you have used it for axes manipulation
|
|
103
|
+
* pivotConfig
|
|
104
|
+
* )
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* the result will be a query with the required filters applied and the dimensions/measures filled out
|
|
108
|
+
* ```js
|
|
109
|
+
* {
|
|
110
|
+
* measures: ['Orders.count'],
|
|
111
|
+
* dimensions: ['Orders.status', 'Users.city'],
|
|
112
|
+
* filters: [
|
|
113
|
+
* // dimension and measure filters
|
|
114
|
+
* ],
|
|
115
|
+
* timeDimensions: [
|
|
116
|
+
* //...
|
|
117
|
+
* ]
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* In case when you want to add `order` or `limit` to the query, you can simply spread it
|
|
122
|
+
*
|
|
123
|
+
* ```js
|
|
124
|
+
* // An example for React
|
|
125
|
+
* const drillDownResponse = useCubeQuery(
|
|
126
|
+
* {
|
|
127
|
+
* ...drillDownQuery,
|
|
128
|
+
* limit: 30,
|
|
129
|
+
* order: {
|
|
130
|
+
* 'Orders.ts': 'desc'
|
|
131
|
+
* }
|
|
132
|
+
* },
|
|
133
|
+
* {
|
|
134
|
+
* skip: !drillDownQuery
|
|
135
|
+
* }
|
|
136
|
+
* );
|
|
137
|
+
* ```
|
|
138
|
+
* @returns Drill down query
|
|
139
|
+
*/
|
|
140
|
+
drillDown(drillDownLocator, pivotConfig) {
|
|
141
|
+
if (this.queryType === QUERY_TYPE.COMPARE_DATE_RANGE_QUERY) {
|
|
142
|
+
throw new Error('compareDateRange drillDown query is not currently supported');
|
|
143
|
+
}
|
|
144
|
+
if (this.queryType === QUERY_TYPE.BLENDING_QUERY) {
|
|
145
|
+
throw new Error('Data blending drillDown query is not currently supported');
|
|
146
|
+
}
|
|
147
|
+
const { query } = this.loadResponses[0];
|
|
148
|
+
const xValues = drillDownLocator?.xValues ?? [];
|
|
149
|
+
const yValues = drillDownLocator?.yValues ?? [];
|
|
150
|
+
const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig);
|
|
151
|
+
const values = [];
|
|
152
|
+
normalizedPivotConfig?.x.forEach((member, currentIndex) => values.push([member, xValues[currentIndex]]));
|
|
153
|
+
normalizedPivotConfig?.y.forEach((member, currentIndex) => values.push([member, yValues[currentIndex]]));
|
|
154
|
+
const { filters: parentFilters = [], segments = [] } = this.query();
|
|
155
|
+
const { measures } = this.loadResponses[0].annotation;
|
|
156
|
+
let [, measureName] = values.find(([member]) => member === 'measures') || [];
|
|
157
|
+
if (measureName === undefined) {
|
|
158
|
+
[measureName] = Object.keys(measures);
|
|
159
|
+
}
|
|
160
|
+
if (!(measures[measureName]?.drillMembers?.length ?? 0)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const filters = [
|
|
164
|
+
{
|
|
165
|
+
member: measureName,
|
|
166
|
+
operator: 'measureFilter',
|
|
167
|
+
},
|
|
168
|
+
...parentFilters
|
|
169
|
+
];
|
|
170
|
+
const timeDimensions = [];
|
|
171
|
+
values.filter(([member]) => member !== 'measures')
|
|
172
|
+
.forEach(([member, value]) => {
|
|
173
|
+
const [cubeName, dimension, granularity] = member.split('.');
|
|
174
|
+
if (granularity !== undefined) {
|
|
175
|
+
const range = dayRange(value, value).snapTo(granularity);
|
|
176
|
+
const originalTimeDimension = query.timeDimensions?.find((td) => td.dimension);
|
|
177
|
+
let dateRange = [
|
|
178
|
+
range.start,
|
|
179
|
+
range.end
|
|
180
|
+
];
|
|
181
|
+
if (originalTimeDimension?.dateRange) {
|
|
182
|
+
const [originalStart, originalEnd] = originalTimeDimension.dateRange;
|
|
183
|
+
dateRange = [
|
|
184
|
+
dayjs(originalStart) > range.start ? dayjs(originalStart) : range.start,
|
|
185
|
+
dayjs(originalEnd) < range.end ? dayjs(originalEnd) : range.end,
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
timeDimensions.push({
|
|
189
|
+
dimension: [cubeName, dimension].join('.'),
|
|
190
|
+
dateRange: dateRange.map((dt) => dt.format('YYYY-MM-DDTHH:mm:ss.SSS')),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
else if (value == null) {
|
|
194
|
+
filters.push({
|
|
195
|
+
member,
|
|
196
|
+
operator: 'notSet',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
filters.push({
|
|
201
|
+
member,
|
|
202
|
+
operator: 'equals',
|
|
203
|
+
values: [value.toString()],
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
if (timeDimensions.length === 0 &&
|
|
208
|
+
Array.isArray(query.timeDimensions) &&
|
|
209
|
+
query.timeDimensions.length > 0 &&
|
|
210
|
+
query.timeDimensions[0].granularity == null) {
|
|
211
|
+
timeDimensions.push(query.timeDimensions[0]);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
...measures[measureName].drillMembersGrouped,
|
|
215
|
+
filters,
|
|
216
|
+
...(segments.length > 0 ? { segments } : {}),
|
|
217
|
+
timeDimensions,
|
|
218
|
+
segments,
|
|
219
|
+
timezone: query.timezone
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Returns an array of series with key, title and series data.
|
|
224
|
+
* ```js
|
|
225
|
+
* // For the query
|
|
226
|
+
* {
|
|
227
|
+
* measures: ['Stories.count'],
|
|
228
|
+
* timeDimensions: [{
|
|
229
|
+
* dimension: 'Stories.time',
|
|
230
|
+
* dateRange: ['2015-01-01', '2015-12-31'],
|
|
231
|
+
* granularity: 'month'
|
|
232
|
+
* }]
|
|
233
|
+
* }
|
|
234
|
+
*
|
|
235
|
+
* // ResultSet.series() will return
|
|
236
|
+
* [
|
|
237
|
+
* {
|
|
238
|
+
* key: 'Stories.count',
|
|
239
|
+
* title: 'Stories Count',
|
|
240
|
+
* shortTitle: 'Count',
|
|
241
|
+
* series: [
|
|
242
|
+
* { x: '2015-01-01T00:00:00', value: 27120 },
|
|
243
|
+
* { x: '2015-02-01T00:00:00', value: 25861 },
|
|
244
|
+
* { x: '2015-03-01T00:00:00', value: 29661 },
|
|
245
|
+
* //...
|
|
246
|
+
* ],
|
|
247
|
+
* },
|
|
248
|
+
* ]
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
series(pivotConfig) {
|
|
252
|
+
return this.seriesNames(pivotConfig).map(({ title, shortTitle, key }) => ({
|
|
253
|
+
title,
|
|
254
|
+
shortTitle,
|
|
255
|
+
key,
|
|
256
|
+
series: this.chartPivot(pivotConfig).map(({ x, ...obj }) => ({ value: obj[key], x }))
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
axisValues(axis, resultIndex = 0) {
|
|
260
|
+
const { query } = this.loadResponses[resultIndex];
|
|
261
|
+
return (row) => {
|
|
262
|
+
const value = (measure) => axis
|
|
263
|
+
.filter(d => d !== 'measures')
|
|
264
|
+
.map((d) => {
|
|
265
|
+
const val = row[d];
|
|
266
|
+
return val != null ? val : null;
|
|
267
|
+
})
|
|
268
|
+
.concat(measure ? [measure] : []);
|
|
269
|
+
if (axis.find(d => d === 'measures') && (query.measures || []).length) {
|
|
270
|
+
return (query.measures || []).map(value);
|
|
271
|
+
}
|
|
272
|
+
return [value()];
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
axisValuesString(axisValues, delimiter = ', ') {
|
|
276
|
+
const formatValue = (v) => {
|
|
277
|
+
if (v == null) {
|
|
278
|
+
return '∅';
|
|
279
|
+
}
|
|
280
|
+
else if (v === '') {
|
|
281
|
+
return '[Empty string]';
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
return v;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
return axisValues.map(formatValue).join(delimiter);
|
|
288
|
+
}
|
|
289
|
+
static getNormalizedPivotConfig(query, pivotConfig) {
|
|
290
|
+
const defaultPivotConfig = {
|
|
291
|
+
x: [],
|
|
292
|
+
y: [],
|
|
293
|
+
fillMissingDates: true,
|
|
294
|
+
joinDateRange: false
|
|
295
|
+
};
|
|
296
|
+
const { measures = [], dimensions = [] } = query || {};
|
|
297
|
+
const timeDimensions = (query?.timeDimensions || []).filter(td => !!td.granularity);
|
|
298
|
+
pivotConfig = pivotConfig || (timeDimensions.length ? {
|
|
299
|
+
x: timeDimensions.map(td => ResultSet.timeDimensionMember(td)),
|
|
300
|
+
y: dimensions
|
|
301
|
+
} : {
|
|
302
|
+
x: dimensions,
|
|
303
|
+
y: []
|
|
304
|
+
});
|
|
305
|
+
const normalizedPivotConfig = mergeDeepLeft(pivotConfig, defaultPivotConfig);
|
|
306
|
+
const substituteTimeDimensionMembers = (axis) => axis.map(subDim => ((timeDimensions.find(td => td.dimension === subDim) &&
|
|
307
|
+
!dimensions.find(d => d === subDim)) ?
|
|
308
|
+
ResultSet.timeDimensionMember((query?.timeDimensions || []).find(td => td.dimension === subDim)) :
|
|
309
|
+
subDim));
|
|
310
|
+
normalizedPivotConfig.x = substituteTimeDimensionMembers(normalizedPivotConfig.x);
|
|
311
|
+
normalizedPivotConfig.y = substituteTimeDimensionMembers(normalizedPivotConfig.y);
|
|
312
|
+
const allIncludedDimensions = normalizedPivotConfig.x.concat(normalizedPivotConfig.y);
|
|
313
|
+
const allDimensions = timeDimensions.map(td => ResultSet.timeDimensionMember(td)).concat(dimensions);
|
|
314
|
+
const dimensionFilter = (key) => allDimensions.includes(key) || key === 'measures';
|
|
315
|
+
normalizedPivotConfig.x = normalizedPivotConfig.x.concat(allDimensions.filter(d => !allIncludedDimensions.includes(d) && d !== 'compareDateRange'))
|
|
316
|
+
.filter(dimensionFilter);
|
|
317
|
+
normalizedPivotConfig.y = normalizedPivotConfig.y.filter(dimensionFilter);
|
|
318
|
+
if (!normalizedPivotConfig.x.concat(normalizedPivotConfig.y).find(d => d === 'measures')) {
|
|
319
|
+
normalizedPivotConfig.y.push('measures');
|
|
320
|
+
}
|
|
321
|
+
if (dimensions.includes('compareDateRange') && !normalizedPivotConfig.y.concat(normalizedPivotConfig.x).includes('compareDateRange')) {
|
|
322
|
+
normalizedPivotConfig.y.unshift('compareDateRange');
|
|
323
|
+
}
|
|
324
|
+
if (!measures.length) {
|
|
325
|
+
normalizedPivotConfig.x = normalizedPivotConfig.x.filter(d => d !== 'measures');
|
|
326
|
+
normalizedPivotConfig.y = normalizedPivotConfig.y.filter(d => d !== 'measures');
|
|
327
|
+
}
|
|
328
|
+
return normalizedPivotConfig;
|
|
329
|
+
}
|
|
330
|
+
normalizePivotConfig(pivotConfig) {
|
|
331
|
+
return ResultSet.getNormalizedPivotConfig(this.loadResponse.pivotQuery, pivotConfig);
|
|
332
|
+
}
|
|
333
|
+
timeSeries(timeDimension, resultIndex, annotations) {
|
|
334
|
+
if (!timeDimension.granularity) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
let dateRange;
|
|
338
|
+
dateRange = timeDimension.dateRange;
|
|
339
|
+
if (!dateRange) {
|
|
340
|
+
const member = ResultSet.timeDimensionMember(timeDimension);
|
|
341
|
+
const rawRows = this.timeDimensionBackwardCompatibleData(resultIndex || 0);
|
|
342
|
+
const dates = rawRows
|
|
343
|
+
.map(row => {
|
|
344
|
+
const value = row[member];
|
|
345
|
+
return value ? internalDayjs(value) : null;
|
|
346
|
+
})
|
|
347
|
+
.filter((d) => Boolean(d));
|
|
348
|
+
dateRange = dates.length && [
|
|
349
|
+
(reduce(minBy((d) => d.toDate()), dates[0], dates)).toString(),
|
|
350
|
+
(reduce(maxBy((d) => d.toDate()), dates[0], dates)).toString(),
|
|
351
|
+
] || null;
|
|
352
|
+
}
|
|
353
|
+
if (!dateRange) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const padToDay = timeDimension.dateRange ?
|
|
357
|
+
timeDimension.dateRange.find(d => d.match(DateRegex)) :
|
|
358
|
+
!['hour', 'minute', 'second'].includes(timeDimension.granularity);
|
|
359
|
+
const [start, end] = dateRange;
|
|
360
|
+
const range = dayRange(start, end);
|
|
361
|
+
if (isPredefinedGranularity(timeDimension.granularity)) {
|
|
362
|
+
return TIME_SERIES[timeDimension.granularity](padToDay ? range.snapTo('d') : range);
|
|
363
|
+
}
|
|
364
|
+
if (!annotations?.[`${timeDimension.dimension}.${timeDimension.granularity}`]) {
|
|
365
|
+
throw new Error(`Granularity "${timeDimension.granularity}" not found in time dimension "${timeDimension.dimension}"`);
|
|
366
|
+
}
|
|
367
|
+
return timeSeriesFromCustomInterval(start, end, annotations[`${timeDimension.dimension}.${timeDimension.granularity}`].granularity);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Base method for pivoting [ResultSet](#result-set) data.
|
|
371
|
+
* Most of the time shouldn't be used directly and [chartPivot](#result-set-chart-pivot)
|
|
372
|
+
* or [tablePivot](#table-pivot) should be used instead.
|
|
373
|
+
*
|
|
374
|
+
* You can find the examples of using the `pivotConfig` [here](#types-pivot-config)
|
|
375
|
+
* ```js
|
|
376
|
+
* // For query
|
|
377
|
+
* {
|
|
378
|
+
* measures: ['Stories.count'],
|
|
379
|
+
* timeDimensions: [{
|
|
380
|
+
* dimension: 'Stories.time',
|
|
381
|
+
* dateRange: ['2015-01-01', '2015-03-31'],
|
|
382
|
+
* granularity: 'month'
|
|
383
|
+
* }]
|
|
384
|
+
* }
|
|
385
|
+
*
|
|
386
|
+
* // ResultSet.pivot({ x: ['Stories.time'], y: ['measures'] }) will return
|
|
387
|
+
* [
|
|
388
|
+
* {
|
|
389
|
+
* xValues: ["2015-01-01T00:00:00"],
|
|
390
|
+
* yValuesArray: [
|
|
391
|
+
* [['Stories.count'], 27120]
|
|
392
|
+
* ]
|
|
393
|
+
* },
|
|
394
|
+
* {
|
|
395
|
+
* xValues: ["2015-02-01T00:00:00"],
|
|
396
|
+
* yValuesArray: [
|
|
397
|
+
* [['Stories.count'], 25861]
|
|
398
|
+
* ]
|
|
399
|
+
* },
|
|
400
|
+
* {
|
|
401
|
+
* xValues: ["2015-03-01T00:00:00"],
|
|
402
|
+
* yValuesArray: [
|
|
403
|
+
* [['Stories.count'], 29661]
|
|
404
|
+
* ]
|
|
405
|
+
* }
|
|
406
|
+
* ]
|
|
407
|
+
* ```
|
|
408
|
+
* @returns An array of pivoted rows.
|
|
409
|
+
*/
|
|
410
|
+
pivot(pivotConfig) {
|
|
411
|
+
const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig);
|
|
412
|
+
const { pivotQuery: query } = this.loadResponse;
|
|
413
|
+
const pivotImpl = (resultIndex = 0) => {
|
|
414
|
+
let groupByXAxis = groupByToPairs(({ xValues }) => this.axisValuesString(xValues));
|
|
415
|
+
const measureValue = (row, measure) => row[measure] || normalizedPivotConfig.fillWithValue || 0;
|
|
416
|
+
if (normalizedPivotConfig.fillMissingDates &&
|
|
417
|
+
normalizedPivotConfig.x.length === 1 &&
|
|
418
|
+
(equals(normalizedPivotConfig.x, (query.timeDimensions || [])
|
|
419
|
+
.filter(td => Boolean(td.granularity))
|
|
420
|
+
.map(td => ResultSet.timeDimensionMember(td))))) {
|
|
421
|
+
const series = this.loadResponses.map((loadResponse) => this.timeSeries(loadResponse.query.timeDimensions[0], resultIndex, loadResponse.annotation.timeDimensions));
|
|
422
|
+
if (series[0]) {
|
|
423
|
+
groupByXAxis = (rows) => {
|
|
424
|
+
const byXValues = groupBy(({ xValues }) => xValues[0], rows);
|
|
425
|
+
return series[resultIndex]?.map(d => [d, byXValues[d] || [{ xValues: [d], row: {} }]]) ?? [];
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const xGrouped = pipe(map((row) => this.axisValues(normalizedPivotConfig.x, resultIndex)(row).map(xValues => ({ xValues, row }))), unnest, groupByXAxis)(this.timeDimensionBackwardCompatibleData(resultIndex));
|
|
430
|
+
const yValuesMap = {};
|
|
431
|
+
xGrouped.forEach(([, rows]) => {
|
|
432
|
+
rows.forEach(({ row }) => {
|
|
433
|
+
this.axisValues(normalizedPivotConfig.y, resultIndex)(row).forEach((values) => {
|
|
434
|
+
if (Object.keys(row).length > 0) {
|
|
435
|
+
yValuesMap[values.join()] = values;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
const allYValues = Object.values(yValuesMap);
|
|
441
|
+
const measureOnX = Boolean((normalizedPivotConfig.x).find(d => d === 'measures'));
|
|
442
|
+
return xGrouped.map(([, rows]) => {
|
|
443
|
+
const { xValues } = rows[0];
|
|
444
|
+
const yGrouped = {};
|
|
445
|
+
rows.forEach(({ row }) => {
|
|
446
|
+
const arr = this.axisValues(normalizedPivotConfig.y, resultIndex)(row).map(yValues => ({ yValues, row }));
|
|
447
|
+
arr.forEach((res) => {
|
|
448
|
+
yGrouped[this.axisValuesString(res.yValues)] = res;
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
return {
|
|
452
|
+
xValues,
|
|
453
|
+
yValuesArray: unnest(allYValues.map(yValues => {
|
|
454
|
+
const measure = measureOnX ?
|
|
455
|
+
ResultSet.measureFromAxis(xValues) :
|
|
456
|
+
ResultSet.measureFromAxis(yValues);
|
|
457
|
+
return [[yValues, measureValue((yGrouped[this.axisValuesString(yValues)] ||
|
|
458
|
+
({ row: {} })).row, measure)]];
|
|
459
|
+
}))
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
};
|
|
463
|
+
const pivots = this.loadResponses.length > 1
|
|
464
|
+
? this.loadResponses.map((_, index) => pivotImpl(index))
|
|
465
|
+
: [];
|
|
466
|
+
return pivots.length
|
|
467
|
+
? this.mergePivots(pivots, normalizedPivotConfig.joinDateRange || false)
|
|
468
|
+
: pivotImpl();
|
|
469
|
+
}
|
|
470
|
+
mergePivots(pivots, joinDateRange) {
|
|
471
|
+
const minLengthPivot = pivots.reduce((memo, current) => (memo != null && current.length >= memo.length ? memo : current), null) || [];
|
|
472
|
+
return minLengthPivot.map((_, index) => {
|
|
473
|
+
const xValues = joinDateRange
|
|
474
|
+
? [pivots.map((pivot) => pivot[index]?.xValues || []).join(', ')]
|
|
475
|
+
: minLengthPivot[index].xValues;
|
|
476
|
+
return {
|
|
477
|
+
xValues,
|
|
478
|
+
yValuesArray: unnest(pivots.map((pivot) => pivot[index].yValuesArray))
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Returns normalized query result data in the following format.
|
|
484
|
+
*
|
|
485
|
+
* You can find the examples of using the `pivotConfig` [here](#types-pivot-config)
|
|
486
|
+
* ```js
|
|
487
|
+
* // For the query
|
|
488
|
+
* {
|
|
489
|
+
* measures: ['Stories.count'],
|
|
490
|
+
* timeDimensions: [{
|
|
491
|
+
* dimension: 'Stories.time',
|
|
492
|
+
* dateRange: ['2015-01-01', '2015-12-31'],
|
|
493
|
+
* granularity: 'month'
|
|
494
|
+
* }]
|
|
495
|
+
* }
|
|
496
|
+
*
|
|
497
|
+
* // ResultSet.chartPivot() will return
|
|
498
|
+
* [
|
|
499
|
+
* { "x":"2015-01-01T00:00:00", "Stories.count": 27120, "xValues": ["2015-01-01T00:00:00"] },
|
|
500
|
+
* { "x":"2015-02-01T00:00:00", "Stories.count": 25861, "xValues": ["2015-02-01T00:00:00"] },
|
|
501
|
+
* { "x":"2015-03-01T00:00:00", "Stories.count": 29661, "xValues": ["2015-03-01T00:00:00"] },
|
|
502
|
+
* //...
|
|
503
|
+
* ]
|
|
504
|
+
*
|
|
505
|
+
* ```
|
|
506
|
+
* When using `chartPivot()` or `seriesNames()`, you can pass `aliasSeries` in the [pivotConfig](#types-pivot-config)
|
|
507
|
+
* to give each series a unique prefix. This is useful for `blending queries` which use the same measure multiple times.
|
|
508
|
+
*
|
|
509
|
+
* ```js
|
|
510
|
+
* // For the queries
|
|
511
|
+
* {
|
|
512
|
+
* measures: ['Stories.count'],
|
|
513
|
+
* timeDimensions: [
|
|
514
|
+
* {
|
|
515
|
+
* dimension: 'Stories.time',
|
|
516
|
+
* dateRange: ['2015-01-01', '2015-12-31'],
|
|
517
|
+
* granularity: 'month',
|
|
518
|
+
* },
|
|
519
|
+
* ],
|
|
520
|
+
* },
|
|
521
|
+
* {
|
|
522
|
+
* measures: ['Stories.count'],
|
|
523
|
+
* timeDimensions: [
|
|
524
|
+
* {
|
|
525
|
+
* dimension: 'Stories.time',
|
|
526
|
+
* dateRange: ['2015-01-01', '2015-12-31'],
|
|
527
|
+
* granularity: 'month',
|
|
528
|
+
* },
|
|
529
|
+
* ],
|
|
530
|
+
* filters: [
|
|
531
|
+
* {
|
|
532
|
+
* member: 'Stores.read',
|
|
533
|
+
* operator: 'equals',
|
|
534
|
+
* value: ['true'],
|
|
535
|
+
* },
|
|
536
|
+
* ],
|
|
537
|
+
* },
|
|
538
|
+
*
|
|
539
|
+
* // ResultSet.chartPivot({ aliasSeries: ['one', 'two'] }) will return
|
|
540
|
+
* [
|
|
541
|
+
* {
|
|
542
|
+
* x: '2015-01-01T00:00:00',
|
|
543
|
+
* 'one,Stories.count': 27120,
|
|
544
|
+
* 'two,Stories.count': 8933,
|
|
545
|
+
* xValues: ['2015-01-01T00:00:00'],
|
|
546
|
+
* },
|
|
547
|
+
* {
|
|
548
|
+
* x: '2015-02-01T00:00:00',
|
|
549
|
+
* 'one,Stories.count': 25861,
|
|
550
|
+
* 'two,Stories.count': 8344,
|
|
551
|
+
* xValues: ['2015-02-01T00:00:00'],
|
|
552
|
+
* },
|
|
553
|
+
* {
|
|
554
|
+
* x: '2015-03-01T00:00:00',
|
|
555
|
+
* 'one,Stories.count': 29661,
|
|
556
|
+
* 'two,Stories.count': 9023,
|
|
557
|
+
* xValues: ['2015-03-01T00:00:00'],
|
|
558
|
+
* },
|
|
559
|
+
* //...
|
|
560
|
+
* ]
|
|
561
|
+
* ```
|
|
562
|
+
*/
|
|
563
|
+
chartPivot(pivotConfig) {
|
|
564
|
+
const validate = (value) => {
|
|
565
|
+
if (this.parseDateMeasures && LocalDateRegex.test(value)) {
|
|
566
|
+
return new Date(value);
|
|
567
|
+
}
|
|
568
|
+
else if (!Number.isNaN(Number.parseFloat(value))) {
|
|
569
|
+
return Number.parseFloat(value);
|
|
570
|
+
}
|
|
571
|
+
return value;
|
|
572
|
+
};
|
|
573
|
+
const duplicateMeasures = new Set();
|
|
574
|
+
if (this.queryType === QUERY_TYPE.BLENDING_QUERY) {
|
|
575
|
+
const allMeasures = flatten(this.loadResponses.map(({ query }) => query.measures ?? []));
|
|
576
|
+
allMeasures.filter((e, i, a) => a.indexOf(e) !== i).forEach(m => duplicateMeasures.add(m));
|
|
577
|
+
}
|
|
578
|
+
return this.pivot(pivotConfig).map(({ xValues, yValuesArray }) => {
|
|
579
|
+
const yValuesMap = {};
|
|
580
|
+
yValuesArray
|
|
581
|
+
.forEach(([yValues, m], i) => {
|
|
582
|
+
yValuesMap[this.axisValuesString(aliasSeries(yValues, i, pivotConfig, duplicateMeasures), ',')] = m && validate(m);
|
|
583
|
+
});
|
|
584
|
+
return {
|
|
585
|
+
x: this.axisValuesString(xValues, ','),
|
|
586
|
+
xValues,
|
|
587
|
+
...yValuesMap
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Returns normalized query result data prepared for visualization in the table format.
|
|
593
|
+
*
|
|
594
|
+
* You can find the examples of using the `pivotConfig` [here](#types-pivot-config)
|
|
595
|
+
*
|
|
596
|
+
* For example:
|
|
597
|
+
* ```js
|
|
598
|
+
* // For the query
|
|
599
|
+
* {
|
|
600
|
+
* measures: ['Stories.count'],
|
|
601
|
+
* timeDimensions: [{
|
|
602
|
+
* dimension: 'Stories.time',
|
|
603
|
+
* dateRange: ['2015-01-01', '2015-12-31'],
|
|
604
|
+
* granularity: 'month'
|
|
605
|
+
* }]
|
|
606
|
+
* }
|
|
607
|
+
*
|
|
608
|
+
* // ResultSet.tablePivot() will return
|
|
609
|
+
* [
|
|
610
|
+
* { "Stories.time": "2015-01-01T00:00:00", "Stories.count": 27120 },
|
|
611
|
+
* { "Stories.time": "2015-02-01T00:00:00", "Stories.count": 25861 },
|
|
612
|
+
* { "Stories.time": "2015-03-01T00:00:00", "Stories.count": 29661 },
|
|
613
|
+
* //...
|
|
614
|
+
* ]
|
|
615
|
+
* ```
|
|
616
|
+
* @returns An array of pivoted rows
|
|
617
|
+
*/
|
|
618
|
+
tablePivot(pivotConfig) {
|
|
619
|
+
const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig || {});
|
|
620
|
+
const isMeasuresPresent = normalizedPivotConfig.x.concat(normalizedPivotConfig.y).includes('measures');
|
|
621
|
+
return this.pivot(normalizedPivotConfig).map(({ xValues, yValuesArray }) => fromPairs([
|
|
622
|
+
...(normalizedPivotConfig.x).map((key, index) => [
|
|
623
|
+
key,
|
|
624
|
+
xValues[index] ?? ''
|
|
625
|
+
]),
|
|
626
|
+
...(isMeasuresPresent
|
|
627
|
+
? yValuesArray.map(([yValues, measure]) => [
|
|
628
|
+
yValues.length ? yValues.join() : 'value',
|
|
629
|
+
measure
|
|
630
|
+
])
|
|
631
|
+
: [])
|
|
632
|
+
]));
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Returns an array of column definitions for `tablePivot`.
|
|
636
|
+
*
|
|
637
|
+
* For example:
|
|
638
|
+
* ```js
|
|
639
|
+
* // For the query
|
|
640
|
+
* {
|
|
641
|
+
* measures: ['Stories.count'],
|
|
642
|
+
* timeDimensions: [{
|
|
643
|
+
* dimension: 'Stories.time',
|
|
644
|
+
* dateRange: ['2015-01-01', '2015-12-31'],
|
|
645
|
+
* granularity: 'month'
|
|
646
|
+
* }]
|
|
647
|
+
* }
|
|
648
|
+
*
|
|
649
|
+
* // ResultSet.tableColumns() will return
|
|
650
|
+
* [
|
|
651
|
+
* {
|
|
652
|
+
* key: 'Stories.time',
|
|
653
|
+
* dataIndex: 'Stories.time',
|
|
654
|
+
* title: 'Stories Time',
|
|
655
|
+
* shortTitle: 'Time',
|
|
656
|
+
* type: 'time',
|
|
657
|
+
* format: undefined,
|
|
658
|
+
* },
|
|
659
|
+
* {
|
|
660
|
+
* key: 'Stories.count',
|
|
661
|
+
* dataIndex: 'Stories.count',
|
|
662
|
+
* title: 'Stories Count',
|
|
663
|
+
* shortTitle: 'Count',
|
|
664
|
+
* type: 'count',
|
|
665
|
+
* format: undefined,
|
|
666
|
+
* },
|
|
667
|
+
* //...
|
|
668
|
+
* ]
|
|
669
|
+
* ```
|
|
670
|
+
*
|
|
671
|
+
* In case we want to pivot the table axes
|
|
672
|
+
* ```js
|
|
673
|
+
* // Let's take this query as an example
|
|
674
|
+
* {
|
|
675
|
+
* measures: ['Orders.count'],
|
|
676
|
+
* dimensions: ['Users.country', 'Users.gender']
|
|
677
|
+
* }
|
|
678
|
+
*
|
|
679
|
+
* // and put the dimensions on `y` axis
|
|
680
|
+
* resultSet.tableColumns({
|
|
681
|
+
* x: [],
|
|
682
|
+
* y: ['Users.country', 'Users.gender', 'measures']
|
|
683
|
+
* })
|
|
684
|
+
* ```
|
|
685
|
+
*
|
|
686
|
+
* then `tableColumns` will group the table head and return
|
|
687
|
+
* ```js
|
|
688
|
+
* {
|
|
689
|
+
* key: 'Germany',
|
|
690
|
+
* type: 'string',
|
|
691
|
+
* title: 'Users Country Germany',
|
|
692
|
+
* shortTitle: 'Germany',
|
|
693
|
+
* meta: undefined,
|
|
694
|
+
* format: undefined,
|
|
695
|
+
* children: [
|
|
696
|
+
* {
|
|
697
|
+
* key: 'male',
|
|
698
|
+
* type: 'string',
|
|
699
|
+
* title: 'Users Gender male',
|
|
700
|
+
* shortTitle: 'male',
|
|
701
|
+
* meta: undefined,
|
|
702
|
+
* format: undefined,
|
|
703
|
+
* children: [
|
|
704
|
+
* {
|
|
705
|
+
* // ...
|
|
706
|
+
* dataIndex: 'Germany.male.Orders.count',
|
|
707
|
+
* shortTitle: 'Count',
|
|
708
|
+
* },
|
|
709
|
+
* ],
|
|
710
|
+
* },
|
|
711
|
+
* {
|
|
712
|
+
* // ...
|
|
713
|
+
* shortTitle: 'female',
|
|
714
|
+
* children: [
|
|
715
|
+
* {
|
|
716
|
+
* // ...
|
|
717
|
+
* dataIndex: 'Germany.female.Orders.count',
|
|
718
|
+
* shortTitle: 'Count',
|
|
719
|
+
* },
|
|
720
|
+
* ],
|
|
721
|
+
* },
|
|
722
|
+
* ],
|
|
723
|
+
* },
|
|
724
|
+
* // ...
|
|
725
|
+
* ```
|
|
726
|
+
* @returns An array of columns
|
|
727
|
+
*/
|
|
728
|
+
tableColumns(pivotConfig) {
|
|
729
|
+
const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig || {});
|
|
730
|
+
const annotations = this.loadResponses
|
|
731
|
+
.map((r) => r.annotation)
|
|
732
|
+
.reduce((acc, annotation) => mergeDeepLeft(acc, annotation), {
|
|
733
|
+
dimensions: {},
|
|
734
|
+
measures: {},
|
|
735
|
+
timeDimensions: {},
|
|
736
|
+
segments: {},
|
|
737
|
+
});
|
|
738
|
+
const flatMeta = Object.values(annotations).reduce((a, b) => ({ ...a, ...b }), {});
|
|
739
|
+
const schema = {};
|
|
740
|
+
const extractFields = (key) => {
|
|
741
|
+
const { title, shortTitle, type, format, meta } = flatMeta[key] || {};
|
|
742
|
+
return {
|
|
743
|
+
key,
|
|
744
|
+
title,
|
|
745
|
+
shortTitle,
|
|
746
|
+
type,
|
|
747
|
+
format,
|
|
748
|
+
meta
|
|
749
|
+
};
|
|
750
|
+
};
|
|
751
|
+
const pivot = this.pivot(normalizedPivotConfig);
|
|
752
|
+
(pivot[0]?.yValuesArray || []).forEach(([yValues]) => {
|
|
753
|
+
if (yValues.length > 0) {
|
|
754
|
+
let currentItem = schema;
|
|
755
|
+
yValues.forEach((value, index) => {
|
|
756
|
+
currentItem[`_${value}`] = {
|
|
757
|
+
key: value,
|
|
758
|
+
memberId: normalizedPivotConfig.y[index] === 'measures'
|
|
759
|
+
? value
|
|
760
|
+
: normalizedPivotConfig.y[index],
|
|
761
|
+
children: currentItem[`_${value}`]?.children || {}
|
|
762
|
+
};
|
|
763
|
+
currentItem = currentItem[`_${value}`].children;
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
const toColumns = (item = {}, path = []) => {
|
|
768
|
+
if (Object.keys(item).length === 0) {
|
|
769
|
+
return [];
|
|
770
|
+
}
|
|
771
|
+
return Object.values(item).map(({ key, ...currentItem }) => {
|
|
772
|
+
const children = toColumns(currentItem.children, [
|
|
773
|
+
...path,
|
|
774
|
+
key
|
|
775
|
+
]);
|
|
776
|
+
const { title, shortTitle, ...fields } = extractFields(currentItem.memberId);
|
|
777
|
+
const dimensionValue = key !== currentItem.memberId || title == null ? key : '';
|
|
778
|
+
if (!children.length) {
|
|
779
|
+
return {
|
|
780
|
+
...fields,
|
|
781
|
+
key,
|
|
782
|
+
dataIndex: [...path, key].join(),
|
|
783
|
+
title: [title, dimensionValue].join(' ').trim(),
|
|
784
|
+
shortTitle: dimensionValue || shortTitle,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
...fields,
|
|
789
|
+
key,
|
|
790
|
+
title: [title, dimensionValue].join(' ').trim(),
|
|
791
|
+
shortTitle: dimensionValue || shortTitle,
|
|
792
|
+
children,
|
|
793
|
+
};
|
|
794
|
+
});
|
|
795
|
+
};
|
|
796
|
+
let otherColumns = [];
|
|
797
|
+
if (!pivot.length && normalizedPivotConfig.y.includes('measures')) {
|
|
798
|
+
otherColumns = (this.loadResponses[0].query.measures || []).map((key) => ({ ...extractFields(key), dataIndex: key }));
|
|
799
|
+
}
|
|
800
|
+
// Synthetic column to display the measure value
|
|
801
|
+
if (!normalizedPivotConfig.y.length && normalizedPivotConfig.x.includes('measures')) {
|
|
802
|
+
otherColumns.push({
|
|
803
|
+
key: 'value',
|
|
804
|
+
dataIndex: 'value',
|
|
805
|
+
title: 'Value',
|
|
806
|
+
shortTitle: 'Value',
|
|
807
|
+
type: 'string',
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
return (normalizedPivotConfig.x).map((key) => {
|
|
811
|
+
if (key === 'measures') {
|
|
812
|
+
return {
|
|
813
|
+
key: 'measures',
|
|
814
|
+
dataIndex: 'measures',
|
|
815
|
+
title: 'Measures',
|
|
816
|
+
shortTitle: 'Measures',
|
|
817
|
+
type: 'string',
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return ({ ...extractFields(key), dataIndex: key });
|
|
821
|
+
})
|
|
822
|
+
.concat(toColumns(schema))
|
|
823
|
+
.concat(otherColumns);
|
|
824
|
+
}
|
|
825
|
+
totalRow(pivotConfig) {
|
|
826
|
+
return this.chartPivot(pivotConfig)[0];
|
|
827
|
+
}
|
|
828
|
+
categories(pivotConfig) {
|
|
829
|
+
return this.chartPivot(pivotConfig);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Returns an array of series objects, containing `key` and `title` parameters.
|
|
833
|
+
* ```js
|
|
834
|
+
* // For query
|
|
835
|
+
* {
|
|
836
|
+
* measures: ['Stories.count'],
|
|
837
|
+
* timeDimensions: [{
|
|
838
|
+
* dimension: 'Stories.time',
|
|
839
|
+
* dateRange: ['2015-01-01', '2015-12-31'],
|
|
840
|
+
* granularity: 'month'
|
|
841
|
+
* }]
|
|
842
|
+
* }
|
|
843
|
+
*
|
|
844
|
+
* // ResultSet.seriesNames() will return
|
|
845
|
+
* [
|
|
846
|
+
* {
|
|
847
|
+
* key: 'Stories.count',
|
|
848
|
+
* title: 'Stories Count',
|
|
849
|
+
* shortTitle: 'Count',
|
|
850
|
+
* yValues: ['Stories.count'],
|
|
851
|
+
* },
|
|
852
|
+
* ]
|
|
853
|
+
* ```
|
|
854
|
+
* @returns An array of series names
|
|
855
|
+
*/
|
|
856
|
+
seriesNames(pivotConfig) {
|
|
857
|
+
const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig);
|
|
858
|
+
const measures = this.loadResponses
|
|
859
|
+
.map(r => r.annotation.measures)
|
|
860
|
+
.reduce((acc, m) => ({ ...acc, ...m }), {});
|
|
861
|
+
const seriesNames = unnest(this.loadResponses.map((_, index) => pipe(map(this.axisValues(normalizedPivotConfig.y, index)), unnest, uniq)(this.timeDimensionBackwardCompatibleData(index))));
|
|
862
|
+
const duplicateMeasures = new Set();
|
|
863
|
+
if (this.queryType === QUERY_TYPE.BLENDING_QUERY) {
|
|
864
|
+
const allMeasures = flatten(this.loadResponses.map(({ query }) => query.measures ?? []));
|
|
865
|
+
allMeasures.filter((e, i, a) => a.indexOf(e) !== i).forEach(m => duplicateMeasures.add(m));
|
|
866
|
+
}
|
|
867
|
+
return seriesNames.map((axisValues, i) => {
|
|
868
|
+
const aliasedAxis = aliasSeries(axisValues, i, normalizedPivotConfig, duplicateMeasures);
|
|
869
|
+
return {
|
|
870
|
+
title: this.axisValuesString(normalizedPivotConfig.y.find(d => d === 'measures') ?
|
|
871
|
+
dropLast(1, aliasedAxis).concat(measures[ResultSet.measureFromAxis(axisValues)].title) :
|
|
872
|
+
aliasedAxis, ', '),
|
|
873
|
+
shortTitle: this.axisValuesString(normalizedPivotConfig.y.find(d => d === 'measures') ?
|
|
874
|
+
dropLast(1, aliasedAxis).concat(measures[ResultSet.measureFromAxis(axisValues)].shortTitle) :
|
|
875
|
+
aliasedAxis, ', '),
|
|
876
|
+
key: this.axisValuesString(aliasedAxis, ','),
|
|
877
|
+
yValues: axisValues
|
|
878
|
+
};
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
query() {
|
|
882
|
+
if (this.queryType !== QUERY_TYPE.REGULAR_QUERY) {
|
|
883
|
+
throw new Error(`Method is not supported for a '${this.queryType}' query type. Please use decompose`);
|
|
884
|
+
}
|
|
885
|
+
return this.loadResponses[0].query;
|
|
886
|
+
}
|
|
887
|
+
pivotQuery() {
|
|
888
|
+
return this.loadResponse.pivotQuery || null;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* @return the total number of rows if the `total` option was set, when sending the query
|
|
892
|
+
*/
|
|
893
|
+
totalRows() {
|
|
894
|
+
return this.loadResponses[0].total;
|
|
895
|
+
}
|
|
896
|
+
rawData() {
|
|
897
|
+
if (this.queryType !== QUERY_TYPE.REGULAR_QUERY) {
|
|
898
|
+
throw new Error(`Method is not supported for a '${this.queryType}' query type. Please use decompose`);
|
|
899
|
+
}
|
|
900
|
+
return this.loadResponses[0].data;
|
|
901
|
+
}
|
|
902
|
+
annotation() {
|
|
903
|
+
if (this.queryType !== QUERY_TYPE.REGULAR_QUERY) {
|
|
904
|
+
throw new Error(`Method is not supported for a '${this.queryType}' query type. Please use decompose`);
|
|
905
|
+
}
|
|
906
|
+
return this.loadResponses[0].annotation;
|
|
907
|
+
}
|
|
908
|
+
timeDimensionBackwardCompatibleData(resultIndex) {
|
|
909
|
+
if (resultIndex === undefined) {
|
|
910
|
+
throw new Error('resultIndex is required');
|
|
911
|
+
}
|
|
912
|
+
if (!this.backwardCompatibleData[resultIndex]) {
|
|
913
|
+
const { data, query } = this.loadResponses[resultIndex];
|
|
914
|
+
const timeDimensions = (query.timeDimensions || []).filter(td => Boolean(td.granularity));
|
|
915
|
+
this.backwardCompatibleData[resultIndex] = data.map(row => ({
|
|
916
|
+
...row,
|
|
917
|
+
...(fromPairs(Object.keys(row)
|
|
918
|
+
.filter(field => {
|
|
919
|
+
const foundTd = timeDimensions.find(d => d.dimension === field);
|
|
920
|
+
return foundTd && !row[ResultSet.timeDimensionMember(foundTd)];
|
|
921
|
+
}).map(field => ([ResultSet.timeDimensionMember(timeDimensions.find(d => d.dimension === field)), row[field]]))))
|
|
922
|
+
}));
|
|
923
|
+
}
|
|
924
|
+
return this.backwardCompatibleData[resultIndex];
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Can be used when you need access to the methods that can't be used with some query types (eg `compareDateRangeQuery` or `blendingQuery`)
|
|
928
|
+
* ```js
|
|
929
|
+
* resultSet.decompose().forEach((currentResultSet) => {
|
|
930
|
+
* console.log(currentResultSet.rawData());
|
|
931
|
+
* });
|
|
932
|
+
* ```
|
|
933
|
+
*/
|
|
934
|
+
decompose() {
|
|
935
|
+
return this.loadResponses.map((result) => new ResultSet({
|
|
936
|
+
queryType: QUERY_TYPE.REGULAR_QUERY,
|
|
937
|
+
pivotQuery: {
|
|
938
|
+
...result.query,
|
|
939
|
+
queryType: QUERY_TYPE.REGULAR_QUERY,
|
|
940
|
+
},
|
|
941
|
+
results: [result]
|
|
942
|
+
}, this.options));
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Can be used to stash the `ResultSet` in a storage and restored later with [deserialize](#result-set-deserialize)
|
|
946
|
+
*/
|
|
947
|
+
serialize() {
|
|
948
|
+
return {
|
|
949
|
+
loadResponse: clone(this.loadResponse)
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
}
|