diff --git a/frontend/src/components/Graph/__tests__/xAxisConfig.test.ts b/frontend/src/components/Graph/__tests__/xAxisConfig.test.ts new file mode 100644 index 0000000000..e0320e29b0 --- /dev/null +++ b/frontend/src/components/Graph/__tests__/xAxisConfig.test.ts @@ -0,0 +1,75 @@ +import { expect } from '@jest/globals'; +import dayjs from 'dayjs'; + +import { convertTimeRange, TIME_UNITS } from '../xAxisConfig'; + +describe('xAxisConfig for Chart', () => { + describe('convertTimeRange', () => { + it('should return relevant time units for given range', () => { + { + const start = dayjs(); + const end = start.add(10, 'millisecond'); + + expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual( + TIME_UNITS.millisecond, + ); + } + { + const start = dayjs(); + const end = start.add(10, 'second'); + + expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual( + TIME_UNITS.second, + ); + } + { + const start = dayjs(); + const end = start.add(10, 'minute'); + + expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual( + TIME_UNITS.minute, + ); + } + { + const start = dayjs(); + const end = start.add(10, 'hour'); + + expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual( + TIME_UNITS.hour, + ); + } + { + const start = dayjs(); + const end = start.add(10, 'day'); + + expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual( + TIME_UNITS.day, + ); + } + { + const start = dayjs(); + const end = start.add(10, 'week'); + + expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual( + TIME_UNITS.week, + ); + } + { + const start = dayjs(); + const end = start.add(10, 'month'); + + expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual( + TIME_UNITS.month, + ); + } + { + const start = dayjs(); + const end = start.add(10, 'year'); + + expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual( + TIME_UNITS.year, + ); + } + }); + }); +}); diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index b6905495d4..4cf6c04a0f 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -27,6 +27,7 @@ import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; +import { useXAxisTimeUnit } from './xAxisConfig'; Chart.register( LineElement, PointElement, @@ -59,7 +60,8 @@ const Graph = ({ const chartRef = useRef(null); const currentTheme = isDarkMode ? 'dark' : 'light'; - // const [tooltipVisible, setTooltipVisible] = useState(false); + const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data + const lineChartRef = useRef(); const getGridColor = useCallback(() => { @@ -109,7 +111,18 @@ const Graph = ({ date: chartjsAdapter, }, time: { - unit: 'minute', + unit: xAxisTimeUnit?.unitName || 'minute', + stepSize: xAxisTimeUnit?.stepSize || 1, + displayFormats: { + millisecond: 'hh:mm:ss', + second: 'hh:mm:ss', + minute: 'HH:mm', + hour: 'MM/dd HH:mm', + day: 'MM/dd', + week: 'MM/dd', + month: 'yy-MM', + year: 'yy', + }, }, type: 'time', }, diff --git a/frontend/src/components/Graph/xAxisConfig.ts b/frontend/src/components/Graph/xAxisConfig.ts new file mode 100644 index 0000000000..ae99a49a23 --- /dev/null +++ b/frontend/src/components/Graph/xAxisConfig.ts @@ -0,0 +1,147 @@ +import { Chart, TimeUnit } from 'chart.js'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +interface ITimeUnit { + [key: string]: TimeUnit; +} +interface IAxisTimeUintConfig { + unitName: TimeUnit; + multiplier: number; +} + +interface IAxisTimeConfig { + unitName: TimeUnit; + stepSize: number; +} + +export interface ITimeRange { + minTime: number | null; + maxTime: number | null; +} + +export const TIME_UNITS: ITimeUnit = { + millisecond: 'millisecond', + second: 'second', + minute: 'minute', + hour: 'hour', + day: 'day', + week: 'week', + month: 'month', + year: 'year', +}; + +const TIME_UNITS_CONFIG: IAxisTimeUintConfig[] = [ + { + unitName: TIME_UNITS.millisecond, + multiplier: 1, + }, + { + unitName: TIME_UNITS.second, + multiplier: 1 / 1e3, + }, + { + unitName: TIME_UNITS.minute, + multiplier: 1 / (1e3 * 60), + }, + { + unitName: TIME_UNITS.hour, + multiplier: 1 / (1e3 * 60 * 60), + }, + { + unitName: TIME_UNITS.day, + multiplier: 1 / (1e3 * 60 * 60 * 24), + }, + { + unitName: TIME_UNITS.week, + multiplier: 1 / (1e3 * 60 * 60 * 24 * 7), + }, + { + unitName: TIME_UNITS.month, + multiplier: 1 / (1e3 * 60 * 60 * 24 * 30), + }, + { + unitName: TIME_UNITS.year, + multiplier: 1 / (1e3 * 60 * 60 * 24 * 365), + }, +]; + +/** + * Accepts Chart.js data's data-structure and returns the relevant time unit for the axis based on the range of the data. + */ +export const useXAxisTimeUnit = (data: Chart['data']): IAxisTimeConfig => { + // Local time is the time range inferred from the input chart data. + let localTime: ITimeRange | null; + try { + let minTime = Number.POSITIVE_INFINITY; + let maxTime = Number.NEGATIVE_INFINITY; + data?.labels?.forEach((timeStamp: string | number): void => { + if (typeof timeStamp === 'string') timeStamp = Date.parse(timeStamp); + minTime = Math.min(timeStamp, minTime); + maxTime = Math.max(timeStamp, maxTime); + }); + + localTime = { + minTime: minTime === Number.POSITIVE_INFINITY ? null : minTime, + maxTime: maxTime === Number.NEGATIVE_INFINITY ? null : maxTime, + }; + } catch (error) { + localTime = null; + console.error(error); + } + + // Global time is the time selected from the global time selector menu. + const globalTime = useSelector( + (state) => state.globalTime, + ); + + // Use local time if valid else use the global time range + const { maxTime, minTime } = useMemo(() => { + if (localTime && localTime.maxTime && localTime.minTime) { + return { + minTime: localTime.minTime, + maxTime: localTime.maxTime, + }; + } else { + return { + minTime: globalTime.minTime / 1e6, + maxTime: globalTime.maxTime / 1e6, + }; + } + }, [globalTime, localTime]); + + return convertTimeRange(minTime, maxTime); +}; + +/** + * Finds the relevant time unit based on the input time stamps (in ms) + */ +export const convertTimeRange = ( + start: number, + end: number, +): IAxisTimeConfig => { + const MIN_INTERVALS = 6; + const range = end - start; + let relevantTimeUnit = TIME_UNITS_CONFIG[1]; + let stepSize = 1; + try { + for (let idx = TIME_UNITS_CONFIG.length - 1; idx >= 0; idx--) { + const timeUnit = TIME_UNITS_CONFIG[idx]; + const units = range * timeUnit.multiplier; + const steps = units / MIN_INTERVALS; + if (steps >= 1) { + relevantTimeUnit = timeUnit; + stepSize = steps; + break; + } + } + } catch (error) { + console.error(error); + } + return { + unitName: relevantTimeUnit.unitName, + stepSize: Math.floor(stepSize) || 1, + }; +}; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx index 5da66f3394..06665218fc 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx @@ -82,7 +82,6 @@ const FullView = ({ }; const queryMinMax = getMinMax(selectedTime.enum); - const response = await Promise.all( widget.query .filter((e) => e.query.length !== 0) diff --git a/frontend/src/container/Trace/Graph/config.ts b/frontend/src/container/Trace/Graph/config.ts index 978ada0062..b0b500c06d 100644 --- a/frontend/src/container/Trace/Graph/config.ts +++ b/frontend/src/container/Trace/Graph/config.ts @@ -27,7 +27,6 @@ export const getChartData = ( data: [], type: 'line', }; - const chartLabels: ChartData<'line'>['labels'] = []; Object.keys(allDataPoints).forEach((timestamp) => { diff --git a/frontend/src/container/Trace/Graph/index.tsx b/frontend/src/container/Trace/Graph/index.tsx index 7ee311cc89..ce667aec67 100644 --- a/frontend/src/container/Trace/Graph/index.tsx +++ b/frontend/src/container/Trace/Graph/index.tsx @@ -9,7 +9,7 @@ import { TraceReducer } from 'types/reducer/trace'; import { getChartData, getChartDataforGroupBy } from './config'; import { Container } from './styles'; -const TraceGraph = () => { +const TraceGraph = (): JSX.Element => { const { spansGraph, selectedGroupBy } = useSelector( (state) => state.traces, );