From ecd50f72327db889b7665ec0701bf5ed727d7a5f Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Tue, 7 Jan 2025 14:09:06 +0530 Subject: [PATCH] feat(timeline): add new timeline v2 component (#6760) * feat(timeline): base commit for timeline v2 * feat(timeline): svg rendering for timeline v2 * feat(timeline): dynamic scale based on screen size * feat(timeline): cleanup code * feat(timeline): make position functioning of timeline height --- .../TimelineV2/TimelineV2.styles.scss | 4 + .../src/components/TimelineV2/TimelineV2.tsx | 90 +++++++++++++ frontend/src/components/TimelineV2/utils.ts | 118 ++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 frontend/src/components/TimelineV2/TimelineV2.styles.scss create mode 100644 frontend/src/components/TimelineV2/TimelineV2.tsx create mode 100644 frontend/src/components/TimelineV2/utils.ts diff --git a/frontend/src/components/TimelineV2/TimelineV2.styles.scss b/frontend/src/components/TimelineV2/TimelineV2.styles.scss new file mode 100644 index 0000000000..e0df793e73 --- /dev/null +++ b/frontend/src/components/TimelineV2/TimelineV2.styles.scss @@ -0,0 +1,4 @@ +.timeline-v2-container { + flex: 1; + overflow: visible; +} diff --git a/frontend/src/components/TimelineV2/TimelineV2.tsx b/frontend/src/components/TimelineV2/TimelineV2.tsx new file mode 100644 index 0000000000..f7c0488bdf --- /dev/null +++ b/frontend/src/components/TimelineV2/TimelineV2.tsx @@ -0,0 +1,90 @@ +import './TimelineV2.styles.scss'; + +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { useEffect, useState } from 'react'; +import { useMeasure } from 'react-use'; + +import { + getIntervals, + getMinimumIntervalsBasedOnWidth, + Interval, +} from './utils'; + +interface ITimelineV2Props { + startTimestamp: number; + endTimestamp: number; + timelineHeight: number; +} + +function TimelineV2(props: ITimelineV2Props): JSX.Element { + const { startTimestamp, endTimestamp, timelineHeight } = props; + const [intervals, setIntervals] = useState([]); + const [ref, { width }] = useMeasure(); + const isDarkMode = useIsDarkMode(); + + useEffect(() => { + const spread = endTimestamp - startTimestamp; + if (spread < 0) { + return; + } + + const minIntervals = getMinimumIntervalsBasedOnWidth(width); + const intervalisedSpread = (spread / minIntervals) * 1.0; + setIntervals(getIntervals(intervalisedSpread, spread)); + }, [startTimestamp, endTimestamp, width]); + + if (endTimestamp < startTimestamp) { + console.error( + 'endTimestamp cannot be less than startTimestamp', + startTimestamp, + endTimestamp, + ); + return
; + } + + return ( +
+ + + {intervals && + intervals.length > 0 && + intervals.map((interval, index) => ( + + + {interval.label} + + + + ))} + +
+ ); +} + +export default TimelineV2; diff --git a/frontend/src/components/TimelineV2/utils.ts b/frontend/src/components/TimelineV2/utils.ts new file mode 100644 index 0000000000..f97bc96d03 --- /dev/null +++ b/frontend/src/components/TimelineV2/utils.ts @@ -0,0 +1,118 @@ +import { toFixed } from 'utils/toFixed'; + +type TTimeUnitName = 'ms' | 's' | 'm' | 'hr' | 'day' | 'week'; + +export interface IIntervalUnit { + name: TTimeUnitName; + multiplier: number; +} + +export interface Interval { + label: string; + percentage: number; +} + +export const INTERVAL_UNITS: IIntervalUnit[] = [ + { + name: 'ms', + multiplier: 1, + }, + { + name: 's', + multiplier: 1 / 1e3, + }, + { + name: 'm', + multiplier: 1 / (1e3 * 60), + }, + { + name: 'hr', + multiplier: 1 / (1e3 * 60 * 60), + }, + { + name: 'day', + multiplier: 1 / (1e3 * 60 * 60 * 24), + }, + { + name: 'week', + multiplier: 1 / (1e3 * 60 * 60 * 24 * 7), + }, +]; + +export const getMinimumIntervalsBasedOnWidth = (width: number): number => { + // S + if (width < 640) { + return 5; + } + // M + if (width < 768) { + return 6; + } + // L + if (width < 1024) { + return 8; + } + + return 10; +}; + +export const resolveTimeFromInterval = ( + intervalTime: number, + intervalUnit: IIntervalUnit, +): number => intervalTime * intervalUnit.multiplier; + +export function getIntervals( + intervalSpread: number, + baseSpread: number, +): Interval[] { + const integerPartString = intervalSpread.toString().split('.')[0]; + const integerPartLength = integerPartString.length; + const intervalSpreadNormalized = + intervalSpread < 1.0 + ? intervalSpread + : Math.floor(Number(integerPartString) / 10 ** (integerPartLength - 1)) * + 10 ** (integerPartLength - 1); + + let intervalUnit = INTERVAL_UNITS[0]; + for (let idx = INTERVAL_UNITS.length - 1; idx >= 0; idx -= 1) { + const standardInterval = INTERVAL_UNITS[idx]; + if (intervalSpread * standardInterval.multiplier >= 1) { + intervalUnit = INTERVAL_UNITS[idx]; + break; + } + } + intervalUnit = intervalUnit || INTERVAL_UNITS[0]; + + const intervals: Interval[] = [ + { + label: `${toFixed(resolveTimeFromInterval(0, intervalUnit), 2)}${ + intervalUnit.name + }`, + percentage: 0, + }, + ]; + + let tempBaseSpread = baseSpread; + let elapsedIntervals = 0; + + while (tempBaseSpread && intervals.length < 20) { + let intervalTime; + if (tempBaseSpread <= 1.5 * intervalSpreadNormalized) { + intervalTime = elapsedIntervals + tempBaseSpread; + tempBaseSpread = 0; + } else { + intervalTime = elapsedIntervals + intervalSpreadNormalized; + tempBaseSpread -= intervalSpreadNormalized; + } + elapsedIntervals = intervalTime; + const interval: Interval = { + label: `${toFixed(resolveTimeFromInterval(intervalTime, intervalUnit), 2)}${ + intervalUnit.name + }`, + percentage: (intervalTime / baseSpread) * 100, + }; + intervals.push(interval); + } + + return intervals; +}