From 188cee4221c97fe3a66c7657650787c6d0ee9656 Mon Sep 17 00:00:00 2001 From: Paul Sebastian Date: Wed, 13 Sep 2023 10:14:26 -0700 Subject: [PATCH] Explorer empty bucket (#990) * Empty bucket addition proof of concept Signed-off-by: Paul Sebastian * Use the page's interval period for empty buckets Signed-off-by: Paul Sebastian * redid empty bucket loop design to be more intuitive Signed-off-by: Paul Sebastian * Added dynamic start and end datetime info to new count distribution Signed-off-by: Paul Sebastian * use interval period to align count distribution rebucketing Signed-off-by: Paul Sebastian * count distribution snapshot Signed-off-by: Paul Sebastian * moved empty bucket adding functionality out to a util file and generalized Signed-off-by: Paul Sebastian * util func tests Signed-off-by: Paul Sebastian * added stricter types for input and return in util func Signed-off-by: Paul Sebastian * needed to update util test Signed-off-by: Paul Sebastian --------- Signed-off-by: Paul Sebastian --- common/types/explorer.ts | 20 +++ .../event_analytics/explorer/explorer.tsx | 7 +- .../count_distribution.test.tsx.snap | 129 +----------------- .../count_distribution/count_distribution.tsx | 35 ++++- .../utils/__tests__/utils.test.tsx | 66 +++++++++ .../event_analytics/utils/utils.tsx | 58 +++++++- 6 files changed, 182 insertions(+), 133 deletions(-) diff --git a/common/types/explorer.ts b/common/types/explorer.ts index 6dbab6dae..ceb95bb9e 100644 --- a/common/types/explorer.ts +++ b/common/types/explorer.ts @@ -381,3 +381,23 @@ export interface VisSpecificMetaData { x_coordinate: string; y_coordinate: string; } + +export type MOMENT_UNIT_OF_TIME = + | 'years' + | 'y' + | 'quarters' + | 'Q' + | 'months' + | 'M' + | 'weeks' + | 'w' + | 'days' + | 'd' + | 'hours' + | 'h' + | 'minutes' + | 'm' + | 'seconds' + | 's' + | 'milliseconds' + | 'ms'; diff --git a/public/components/event_analytics/explorer/explorer.tsx b/public/components/event_analytics/explorer/explorer.tsx index f24508a6b..52c9abaa9 100644 --- a/public/components/event_analytics/explorer/explorer.tsx +++ b/public/components/event_analytics/explorer/explorer.tsx @@ -530,7 +530,12 @@ export const Explorer = ({ startTime={appLogEvents ? startTime : dateRange[0]} endTime={appLogEvents ? endTime : dateRange[1]} /> - + - - -
- - - +/> `; exports[`Count distribution component Renders empty count distribution component 1`] = ``; diff --git a/public/components/event_analytics/explorer/visualizations/count_distribution/count_distribution.tsx b/public/components/event_analytics/explorer/visualizations/count_distribution/count_distribution.tsx index e7ea547df..8e5f10f31 100644 --- a/public/components/event_analytics/explorer/visualizations/count_distribution/count_distribution.tsx +++ b/public/components/event_analytics/explorer/visualizations/count_distribution/count_distribution.tsx @@ -6,13 +6,20 @@ import React from 'react'; import { BarOrientation, LONG_CHART_COLOR } from '../../../../../../common/constants/shared'; import { Plt } from '../../../../visualizations/plotly/plot'; +import { fillTimeDataWithEmpty } from '../../../utils/utils'; -export const CountDistribution = ({ countDistribution }: any) => { +export const CountDistribution = ({ + countDistribution, + selectedInterval, + startTime, + endTime, +}: any) => { if ( !countDistribution || !countDistribution.data || !countDistribution.metadata || - !countDistribution.metadata.fields + !countDistribution.metadata.fields || + !selectedInterval ) return null; @@ -31,9 +38,31 @@ export const CountDistribution = ({ countDistribution }: any) => { }, ]; + // fill the final data with the exact right amount of empty buckets + function fillWithEmpty(processedData: any) { + // original x and y fields + const xVals = processedData[0].x; + const yVals = processedData[0].y; + + const { buckets, values } = fillTimeDataWithEmpty( + xVals, + yVals, + selectedInterval.replace(/^auto_/, ''), + startTime, + endTime + ); + + // replace old x and y values with new + processedData[0].x = buckets; + processedData[0].y = values; + + // // at the end, return the new object + return processedData; + } + return ( { @@ -80,4 +81,69 @@ describe('Utils event analytics helper functions', () => { ).toBeTruthy(); expect(getHeaders([], ['', 'Time', '_source'], undefined)).toBeTruthy(); }); + + it('validates fillTimeDataWithEmpty function', () => { + expect( + fillTimeDataWithEmpty( + ['2023-07-01 00:00:00', '2023-08-01 00:00:00', '2023-09-01 00:00:00'], + [54, 802, 292], + 'M', + '2023-01-01T08:00:00.000Z', + '2023-09-12T21:36:31.389Z' + ) + ).toEqual({ + buckets: [ + '2023-01-01 00:00:00', + '2023-02-01 00:00:00', + '2023-03-01 00:00:00', + '2023-04-01 00:00:00', + '2023-05-01 00:00:00', + '2023-06-01 00:00:00', + '2023-07-01 00:00:00', + '2023-08-01 00:00:00', + '2023-09-01 00:00:00', + ], + values: [0, 0, 0, 0, 0, 0, 54, 802, 292], + }); + expect( + fillTimeDataWithEmpty( + [ + '2023-09-11 07:00:00', + '2023-09-11 09:00:00', + '2023-09-11 10:00:00', + '2023-09-11 11:00:00', + '2023-09-11 12:00:00', + '2023-09-11 13:00:00', + '2023-09-11 14:00:00', + '2023-09-11 15:00:00', + ], + [1, 1, 5, 4, 2, 3, 3, 1], + 'h', + '2023-09-11T00:00:00.000', + '2023-09-11T17:00:00.000' + ) + ).toEqual({ + buckets: [ + '2023-09-11 00:00:00', + '2023-09-11 01:00:00', + '2023-09-11 02:00:00', + '2023-09-11 03:00:00', + '2023-09-11 04:00:00', + '2023-09-11 05:00:00', + '2023-09-11 06:00:00', + '2023-09-11 07:00:00', + '2023-09-11 08:00:00', + '2023-09-11 09:00:00', + '2023-09-11 10:00:00', + '2023-09-11 11:00:00', + '2023-09-11 12:00:00', + '2023-09-11 13:00:00', + '2023-09-11 14:00:00', + '2023-09-11 15:00:00', + '2023-09-11 16:00:00', + '2023-09-11 17:00:00', + ], + values: [0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 5, 4, 2, 3, 3, 1, 0, 0], + }); + }); }); diff --git a/public/components/event_analytics/utils/utils.tsx b/public/components/event_analytics/utils/utils.tsx index cc786f0d8..fe7d47aa4 100644 --- a/public/components/event_analytics/utils/utils.tsx +++ b/public/components/event_analytics/utils/utils.tsx @@ -1,13 +1,15 @@ -/* eslint-disable no-bitwise */ /* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ +/* eslint-disable no-bitwise */ + import { uniqueId, isEmpty } from 'lodash'; import moment from 'moment'; import React from 'react'; import { EuiText } from '@elastic/eui'; +import datemath from '@elastic/datemath'; import { HttpStart } from '../../../../../../src/core/public'; import { CUSTOM_LABEL, @@ -15,6 +17,7 @@ import { GROUPBY, AGGREGATIONS, BREAKDOWNS, + DATE_PICKER_FORMAT, } from '../../../../common/constants/explorer'; import { PPL_DATE_FORMAT, PPL_INDEX_REGEX } from '../../../../common/constants/shared'; import { @@ -23,6 +26,7 @@ import { IExplorerFields, IField, IQuery, + MOMENT_UNIT_OF_TIME, } from '../../../../common/types/explorer'; import PPLService from '../../../services/requests/ppl'; import { DocViewRow, IDocType } from '../explorer/events_views'; @@ -459,3 +463,55 @@ export const getContentTabTitle = (tabID: string, tabTitle: string) => { ); }; + +/** + * Used to fill in missing empty data where x is an array of time values and there are only x + * values when y is non-zero. + * @param xVals all x values being used + * @param yVals all y values being used + * @param intervalPeriod Moment unitOfTime used to dictate how long each interval is + * @param startTime starting time of x values + * @param endTime ending time of x values + * @returns an object with buckets and values where the buckets are all of the new x values and + * values are the corresponding values which include y values that are 0 for empty data + */ +export const fillTimeDataWithEmpty = ( + xVals: string[], + yVals: number[], + intervalPeriod: MOMENT_UNIT_OF_TIME, + startTime: string, + endTime: string +): { buckets: string[]; values: number[] } => { + // parses out datetime for start and end, then reformats + const startDate = datemath + .parse(startTime) + ?.startOf(intervalPeriod === 'w' ? 'isoWeek' : intervalPeriod); + const endDate = datemath + .parse(endTime) + ?.startOf(intervalPeriod === 'w' ? 'isoWeek' : intervalPeriod); + + // find the number of buckets + // below essentially does ((end - start) / interval_period) + 1 + const numBuckets = endDate.diff(startDate, intervalPeriod) + 1; + + // populate buckets as x values in the graph + const buckets = [startDate.format(DATE_PICKER_FORMAT)]; + const currentDate = startDate; + for (let i = 1; i < numBuckets; i++) { + const nextBucket = currentDate.add(1, intervalPeriod); + buckets.push(nextBucket.format(DATE_PICKER_FORMAT)); + } + + // create y values, use old y values if they exist + const values: number[] = []; + buckets.forEach((bucket) => { + const bucketIndex = xVals.findIndex((x: string) => x === bucket); + if (bucketIndex !== -1) { + values.push(yVals[bucketIndex]); + } else { + values.push(0); + } + }); + + return { buckets, values }; +};