From 647a3367a4ffd1417f642e3ca0eb97486ba3c9f0 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 3 Nov 2020 14:30:21 +0100 Subject: [PATCH] complete time scale function --- .../time_scale.test.ts | 279 +++++++++++++++++- .../indexpattern_datasource/time_scale.ts | 2 +- 2 files changed, 276 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts index ef97d420456d5..c29e2cd9567dc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts @@ -53,10 +53,12 @@ describe('time_scale', () => { }, interval: '1d', }); - (dataMock.query.timefilter.timefilter.calculateBounds as jest.Mock).mockReturnValue({ - min: moment('2020-10-05T00:00:00.000Z'), - max: moment('2020-10-10T00:00:00.000Z'), - }); + (dataMock.query.timefilter.timefilter.calculateBounds as jest.Mock).mockImplementation( + ({ from, to }) => ({ + min: moment(from), + max: moment(to), + }) + ); timeScale = functionWrapper(getTimeScaleFunction(dataMock)); }); @@ -94,4 +96,273 @@ describe('time_scale', () => { expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1, 1]); }); + + it('should skip gaps in the data', async () => { + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + date: moment('2020-10-05T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-06T00:00:00.000Z').valueOf(), + }, + { + date: moment('2020-10-07T00:00:00.000Z').valueOf(), + }, + { + date: moment('2020-10-08T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-09T00:00:00.000Z').valueOf(), + metric: 24, + }, + ], + }, + { + ...defaultArgs, + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([ + 1, + undefined, + undefined, + 1, + 1, + ]); + }); + + it('should return input unchanged if input column does not exist', async () => { + const mismatchedTable = { + ...emptyTable, + rows: [ + { + date: moment('2020-10-05T00:00:00.000Z').valueOf(), + metric: 24, + }, + ], + }; + const result = await timeScale(mismatchedTable, { + ...defaultArgs, + inputColumnId: 'nonexistent', + }); + + expect(result).toBe(mismatchedTable); + }); + + it('should be able to scale up as well', async () => { + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'UTC', + timeRange: { + from: '2020-10-05T12:00:00.000Z', + to: '2020-10-05T16:00:00.000Z', + }, + interval: '1h', + }); + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + date: moment('2020-10-05T12:00:00.000Z').valueOf(), + metric: 1, + }, + { + date: moment('2020-10-05T13:00:00.000Z').valueOf(), + metric: 1, + }, + { + date: moment('2020-10-05T14:00:00.000Z').valueOf(), + metric: 1, + }, + { + date: moment('2020-10-05T15:00:00.000Z').valueOf(), + metric: 1, + }, + ], + }, + { + ...defaultArgs, + targetUnit: 'd', + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([24, 24, 24, 24]); + }); + + it('can scale starting from unit multiple target intervals', async () => { + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'UTC', + timeRange: { + from: '2020-10-05T13:00:00.000Z', + to: '2020-10-05T23:00:00.000Z', + }, + interval: '3h', + }); + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + // bucket is cut off by one hour because of the time range + date: moment('2020-10-05T12:00:00.000Z').valueOf(), + metric: 2, + }, + { + date: moment('2020-10-05T15:00:00.000Z').valueOf(), + metric: 3, + }, + { + date: moment('2020-10-05T18:00:00.000Z').valueOf(), + metric: 3, + }, + { + // bucket is cut off by one hour because of the time range + date: moment('2020-10-05T21:00:00.000Z').valueOf(), + metric: 2, + }, + ], + }, + { + ...defaultArgs, + targetUnit: 'h', + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1]); + }); + + it('take start and end of timerange into account', async () => { + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'UTC', + timeRange: { + from: '2020-10-05T12:00:00.000Z', + to: '2020-10-09T12:00:00.000Z', + }, + interval: '1d', + }); + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + // this is a partial bucket because it starts before the start of the time range + date: moment('2020-10-05T00:00:00.000Z').valueOf(), + metric: 12, + }, + { + date: moment('2020-10-06T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-07T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-08T00:00:00.000Z').valueOf(), + metric: 24, + }, + { + // this is a partial bucket because it ends earlier than the regular interval of 1d + date: moment('2020-10-09T00:00:00.000Z').valueOf(), + metric: 12, + }, + ], + }, + { + ...defaultArgs, + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1, 1]); + }); + + it('should respect DST switches', async () => { + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'Europe/Berlin', + timeRange: { + from: '2020-10-23T00:00:00.000+02:00', + to: '2020-10-27T00:00:00.000+01:00', + }, + interval: '1d', + }); + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + date: moment('2020-10-23T00:00:00.000+02:00').valueOf(), + metric: 24, + }, + { + date: moment('2020-10-24T00:00:00.000+02:00').valueOf(), + metric: 24, + }, + { + // this day has one hour more in Europe/Berlin due to DST switch + date: moment('2020-10-25T00:00:00.000+02:00').valueOf(), + metric: 25, + }, + { + date: moment('2020-10-26T00:00:00.000+01:00').valueOf(), + metric: 24, + }, + ], + }, + { + ...defaultArgs, + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1]); + }); + + it('take leap years into account', async () => { + (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + timeZone: 'UTC', + timeRange: { + from: '2010-01-01T00:00:00.000Z', + to: '2015-01-01T00:00:00.000Z', + }, + interval: '1y', + }); + const result = await timeScale( + { + ...emptyTable, + rows: [ + { + date: moment('2010-01-01T00:00:00.000Z').valueOf(), + metric: 365, + }, + { + date: moment('2011-01-01T00:00:00.000Z').valueOf(), + metric: 365, + }, + { + // 2012 is a leap year and has an additional day + date: moment('2012-01-01T00:00:00.000Z').valueOf(), + metric: 366, + }, + { + date: moment('2013-01-01T00:00:00.000Z').valueOf(), + metric: 365, + }, + { + date: moment('2014-01-01T00:00:00.000Z').valueOf(), + metric: 365, + }, + ], + }, + { + ...defaultArgs, + targetUnit: 'd', + } + ); + + expect(result.rows.map(({ scaledMetric }) => scaledMetric)).toEqual([1, 1, 1, 1, 1]); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts index 7f914f250da64..06a6c6393478e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts @@ -139,10 +139,10 @@ export function getTimeScaleFunction(data: DataPublicPluginStart) { const newRow = { ...row }; let startOfBucket = moment(row[dateColumnId]); + let endOfBucket = startOfBucket.clone().add(intervalDuration); if (timeBounds && timeBounds.min) { startOfBucket = moment.max(startOfBucket, timeBounds.min); } - let endOfBucket = startOfBucket.clone().add(intervalDuration); if (timeBounds && timeBounds.max) { endOfBucket = moment.min(endOfBucket, timeBounds.max); }