Skip to content

Commit

Permalink
feat(client-core): support custom intervals time series generation
Browse files Browse the repository at this point in the history
  • Loading branch information
KSDaemon committed Sep 24, 2024
1 parent 3e2202e commit e6669b1
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 76 deletions.
6 changes: 6 additions & 0 deletions packages/cubejs-client-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1303,4 +1303,10 @@ declare module '@cubejs-client/core' {
stage: string;
timeElapsed: number;
};

export function granularityFor(dateStr: string): string;

export function minGranularityForIntervals(i1: string, i2: string): string;

export function isPredefinedGranularity(granularity: TimeDimensionGranularity): boolean;
}
80 changes: 28 additions & 52 deletions packages/cubejs-client-core/src/ResultSet.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,19 @@
import dayjs from 'dayjs';
import quarterOfYear from 'dayjs/plugin/quarterOfYear';

import en from 'dayjs/locale/en';
import {
groupBy, pipe, fromPairs, uniq, filter, map, dropLast, equals, reduce, minBy, maxBy, clone, mergeDeepLeft,
pluck, mergeAll, flatten,
} from 'ramda';

import { aliasSeries } from './utils';

dayjs.extend(quarterOfYear);

// 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)
// So the function below has been added.
const internalDayjs = (...args) => dayjs(...args).locale({ ...en, weekStart: 1 });

export const TIME_SERIES = {
day: (range) => range.by('d').map(d => d.format('YYYY-MM-DDT00:00:00.000')),
month: (range) => range.snapTo('month').by('M').map(d => d.format('YYYY-MM-01T00:00:00.000')),
year: (range) => range.snapTo('year').by('y').map(d => d.format('YYYY-01-01T00:00:00.000')),
hour: (range) => range.by('h').map(d => d.format('YYYY-MM-DDTHH:00:00.000')),
minute: (range) => range.by('m').map(d => d.format('YYYY-MM-DDTHH:mm:00.000')),
second: (range) => range.by('s').map(d => d.format('YYYY-MM-DDTHH:mm:ss.000')),
week: (range) => range.snapTo('week').by('w').map(d => d.startOf('week').format('YYYY-MM-DDT00:00:00.000')),
quarter: (range) => range.snapTo('quarter').by('quarter').map(d => d.startOf('quarter').format('YYYY-MM-DDT00:00:00.000')),
};

const DateRegex = /^\d\d\d\d-\d\d-\d\d$/;
const LocalDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z?$/;
import {
DateRegex,
dayRange,
internalDayjs,
isPredefinedGranularity,
LocalDateRegex,
TIME_SERIES,
timeSeriesFromCustomInterval
} from './time';

const groupByToPairs = (keyFn) => {
const acc = new Map();
Expand Down Expand Up @@ -56,25 +42,6 @@ const unnest = (arr) => {
return res;
};

export const dayRange = (from, to) => ({
by: (value) => {
const results = [];

let start = internalDayjs(from);
const end = internalDayjs(to);

while (start.isBefore(end) || start.isSame(end)) {
results.push(start);
start = start.add(1, value);
}

return results;
},
snapTo: (value) => dayRange(internalDayjs(from).startOf(value), internalDayjs(to).endOf(value)),
start: internalDayjs(from),
end: internalDayjs(to),
});

export const QUERY_TYPE = {
REGULAR_QUERY: 'regularQuery',
COMPARE_DATE_RANGE_QUERY: 'compareDateRangeQuery',
Expand Down Expand Up @@ -163,15 +130,15 @@ class ResultSet {
if (granularity !== undefined) {
const range = dayRange(value, value).snapTo(granularity);
const originalTimeDimension = query.timeDimensions.find((td) => td.dimension);

let dateRange = [
range.start,
range.end
];

if (originalTimeDimension?.dateRange) {
const [originalStart, originalEnd] = originalTimeDimension.dateRange;

dateRange = [
dayjs(originalStart) > range.start ? dayjs(originalStart) : range.start,
dayjs(originalEnd) < range.end ? dayjs(originalEnd) : range.end,
Expand All @@ -195,7 +162,7 @@ class ResultSet {
});
}
});

if (
timeDimensions.length === 0 &&
query.timeDimensions.length > 0 &&
Expand Down Expand Up @@ -321,7 +288,7 @@ class ResultSet {
return ResultSet.getNormalizedPivotConfig(this.loadResponse.pivotQuery, pivotConfig);
}

timeSeries(timeDimension, resultIndex) {
timeSeries(timeDimension, resultIndex, annotations) {
if (!timeDimension.granularity) {
return null;
}
Expand Down Expand Up @@ -352,12 +319,18 @@ class ResultSet {
const [start, end] = dateRange;
const range = dayRange(start, end);

if (!TIME_SERIES[timeDimension.granularity]) {
throw new Error(`Unsupported time granularity: ${timeDimension.granularity}`);
if (isPredefinedGranularity(timeDimension.granularity)) {
return TIME_SERIES[timeDimension.granularity](
padToDay ? range.snapTo('d') : range
);
}

if (!annotations[`${timeDimension.dimension}.${timeDimension.granularity}`]) {
throw new Error(`Granularity "${timeDimension.granularity}" not found in time dimension "${timeDimension.dimension}"`);
}

return TIME_SERIES[timeDimension.granularity](
padToDay ? range.snapTo('d') : range
return timeSeriesFromCustomInterval(
start, end, annotations[`${timeDimension.dimension}.${timeDimension.granularity}`].granularity
);
}

Expand All @@ -381,7 +354,10 @@ class ResultSet {
))
) {
const series = this.loadResponses.map(
(loadResponse) => this.timeSeries(loadResponse.query.timeDimensions[0], resultIndex)
(loadResponse) => this.timeSeries(
loadResponse.query.timeDimensions[0],
resultIndex, loadResponse.annotation.timeDimensions
)
);

if (series[0]) {
Expand Down
11 changes: 6 additions & 5 deletions packages/cubejs-client-core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,22 +271,22 @@ class CubeApi {
if (v.type === 'number') {
return k;
}

return undefined;
}).filter(Boolean);

result.data = result.data.map((row) => {
numericMembers.forEach((key) => {
if (row[key] != null) {
row[key] = Number(row[key]);
}
});

return row;
});
});
}

if (response.results[0].query.responseFormat &&
response.results[0].query.responseFormat === ResultType.COMPACT) {
response.results.forEach((result, j) => {
Expand All @@ -302,7 +302,7 @@ class CubeApi {
});
}
}

return new ResultSet(response, {
parseDateMeasures: this.parseDateMeasures
});
Expand Down Expand Up @@ -388,3 +388,4 @@ export default (apiToken, options) => new CubeApi(apiToken, options);

export { CubeApi, HttpTransport, ResultSet, RequestError, Meta };
export * from './utils';
export * from './time';
2 changes: 1 addition & 1 deletion packages/cubejs-client-core/src/tests/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'jest';

import { TIME_SERIES, dayRange } from '../ResultSet';
import { defaultOrder } from '../utils';
import { dayRange, TIME_SERIES } from '../time';

describe('utils', () => {
test('default order', () => {
Expand Down
Loading

0 comments on commit e6669b1

Please sign in to comment.