Skip to content

Commit 3496874

Browse files
committed
feat(client-core): support custom intervals time series generation
1 parent c58e97a commit 3496874

File tree

6 files changed

+342
-76
lines changed

6 files changed

+342
-76
lines changed

packages/cubejs-client-core/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,4 +1303,10 @@ declare module '@cubejs-client/core' {
13031303
stage: string;
13041304
timeElapsed: number;
13051305
};
1306+
1307+
export function granularityFor(dateStr: string): string;
1308+
1309+
export function minGranularityForIntervals(i1: string, i2: string): string;
1310+
1311+
export function isPredefinedGranularity(granularity: TimeDimensionGranularity): boolean;
13061312
}

packages/cubejs-client-core/src/ResultSet.js

Lines changed: 28 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,19 @@
11
import dayjs from 'dayjs';
2-
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
3-
4-
import en from 'dayjs/locale/en';
52
import {
63
groupBy, pipe, fromPairs, uniq, filter, map, dropLast, equals, reduce, minBy, maxBy, clone, mergeDeepLeft,
74
pluck, mergeAll, flatten,
85
} from 'ramda';
96

107
import { aliasSeries } from './utils';
11-
12-
dayjs.extend(quarterOfYear);
13-
14-
// When granularity is week, weekStart Value must be 1. However, since the client can change it globally (https://day.js.org/docs/en/i18n/changing-locale)
15-
// So the function below has been added.
16-
const internalDayjs = (...args) => dayjs(...args).locale({ ...en, weekStart: 1 });
17-
18-
export const TIME_SERIES = {
19-
day: (range) => range.by('d').map(d => d.format('YYYY-MM-DDT00:00:00.000')),
20-
month: (range) => range.snapTo('month').by('M').map(d => d.format('YYYY-MM-01T00:00:00.000')),
21-
year: (range) => range.snapTo('year').by('y').map(d => d.format('YYYY-01-01T00:00:00.000')),
22-
hour: (range) => range.by('h').map(d => d.format('YYYY-MM-DDTHH:00:00.000')),
23-
minute: (range) => range.by('m').map(d => d.format('YYYY-MM-DDTHH:mm:00.000')),
24-
second: (range) => range.by('s').map(d => d.format('YYYY-MM-DDTHH:mm:ss.000')),
25-
week: (range) => range.snapTo('week').by('w').map(d => d.startOf('week').format('YYYY-MM-DDT00:00:00.000')),
26-
quarter: (range) => range.snapTo('quarter').by('quarter').map(d => d.startOf('quarter').format('YYYY-MM-DDT00:00:00.000')),
27-
};
28-
29-
const DateRegex = /^\d\d\d\d-\d\d-\d\d$/;
30-
const LocalDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z?$/;
8+
import {
9+
DateRegex,
10+
dayRange,
11+
internalDayjs,
12+
isPredefinedGranularity,
13+
LocalDateRegex,
14+
TIME_SERIES,
15+
timeSeriesFromCustomInterval
16+
} from './time';
3117

3218
const groupByToPairs = (keyFn) => {
3319
const acc = new Map();
@@ -56,25 +42,6 @@ const unnest = (arr) => {
5642
return res;
5743
};
5844

59-
export const dayRange = (from, to) => ({
60-
by: (value) => {
61-
const results = [];
62-
63-
let start = internalDayjs(from);
64-
const end = internalDayjs(to);
65-
66-
while (start.isBefore(end) || start.isSame(end)) {
67-
results.push(start);
68-
start = start.add(1, value);
69-
}
70-
71-
return results;
72-
},
73-
snapTo: (value) => dayRange(internalDayjs(from).startOf(value), internalDayjs(to).endOf(value)),
74-
start: internalDayjs(from),
75-
end: internalDayjs(to),
76-
});
77-
7845
export const QUERY_TYPE = {
7946
REGULAR_QUERY: 'regularQuery',
8047
COMPARE_DATE_RANGE_QUERY: 'compareDateRangeQuery',
@@ -163,15 +130,15 @@ class ResultSet {
163130
if (granularity !== undefined) {
164131
const range = dayRange(value, value).snapTo(granularity);
165132
const originalTimeDimension = query.timeDimensions.find((td) => td.dimension);
166-
133+
167134
let dateRange = [
168135
range.start,
169136
range.end
170137
];
171-
138+
172139
if (originalTimeDimension?.dateRange) {
173140
const [originalStart, originalEnd] = originalTimeDimension.dateRange;
174-
141+
175142
dateRange = [
176143
dayjs(originalStart) > range.start ? dayjs(originalStart) : range.start,
177144
dayjs(originalEnd) < range.end ? dayjs(originalEnd) : range.end,
@@ -195,7 +162,7 @@ class ResultSet {
195162
});
196163
}
197164
});
198-
165+
199166
if (
200167
timeDimensions.length === 0 &&
201168
query.timeDimensions.length > 0 &&
@@ -321,7 +288,7 @@ class ResultSet {
321288
return ResultSet.getNormalizedPivotConfig(this.loadResponse.pivotQuery, pivotConfig);
322289
}
323290

324-
timeSeries(timeDimension, resultIndex) {
291+
timeSeries(timeDimension, resultIndex, annotations) {
325292
if (!timeDimension.granularity) {
326293
return null;
327294
}
@@ -352,12 +319,18 @@ class ResultSet {
352319
const [start, end] = dateRange;
353320
const range = dayRange(start, end);
354321

355-
if (!TIME_SERIES[timeDimension.granularity]) {
356-
throw new Error(`Unsupported time granularity: ${timeDimension.granularity}`);
322+
if (isPredefinedGranularity(timeDimension.granularity)) {
323+
return TIME_SERIES[timeDimension.granularity](
324+
padToDay ? range.snapTo('d') : range
325+
);
326+
}
327+
328+
if (!annotations[`${timeDimension.dimension}.${timeDimension.granularity}`]) {
329+
throw new Error(`Granularity "${timeDimension.granularity}" not found in time dimension "${timeDimension.dimension}"`);
357330
}
358331

359-
return TIME_SERIES[timeDimension.granularity](
360-
padToDay ? range.snapTo('d') : range
332+
return timeSeriesFromCustomInterval(
333+
start, end, annotations[`${timeDimension.dimension}.${timeDimension.granularity}`].granularity
361334
);
362335
}
363336

@@ -381,7 +354,10 @@ class ResultSet {
381354
))
382355
) {
383356
const series = this.loadResponses.map(
384-
(loadResponse) => this.timeSeries(loadResponse.query.timeDimensions[0], resultIndex)
357+
(loadResponse) => this.timeSeries(
358+
loadResponse.query.timeDimensions[0],
359+
resultIndex, loadResponse.annotation.timeDimensions
360+
)
385361
);
386362

387363
if (series[0]) {

packages/cubejs-client-core/src/index.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,22 +271,22 @@ class CubeApi {
271271
if (v.type === 'number') {
272272
return k;
273273
}
274-
274+
275275
return undefined;
276276
}).filter(Boolean);
277-
277+
278278
result.data = result.data.map((row) => {
279279
numericMembers.forEach((key) => {
280280
if (row[key] != null) {
281281
row[key] = Number(row[key]);
282282
}
283283
});
284-
284+
285285
return row;
286286
});
287287
});
288288
}
289-
289+
290290
if (response.results[0].query.responseFormat &&
291291
response.results[0].query.responseFormat === ResultType.COMPACT) {
292292
response.results.forEach((result, j) => {
@@ -302,7 +302,7 @@ class CubeApi {
302302
});
303303
}
304304
}
305-
305+
306306
return new ResultSet(response, {
307307
parseDateMeasures: this.parseDateMeasures
308308
});
@@ -388,3 +388,4 @@ export default (apiToken, options) => new CubeApi(apiToken, options);
388388

389389
export { CubeApi, HttpTransport, ResultSet, RequestError, Meta };
390390
export * from './utils';
391+
export * from './time';

packages/cubejs-client-core/src/tests/utils.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'jest';
22

3-
import { TIME_SERIES, dayRange } from '../ResultSet';
43
import { defaultOrder } from '../utils';
4+
import { dayRange, TIME_SERIES } from '../time';
55

66
describe('utils', () => {
77
test('default order', () => {

0 commit comments

Comments
 (0)