import moment from 'moment' const GREEN_COLOR = 'var(--green)' const RED_COLOR = 'var(--red)' const PURPLE_COLOR = 'var(--purple)' const LAVENDER_COLOR = 'var(--lavender)' const transparent = 'var(--white)' export const colors = [ GREEN_COLOR, RED_COLOR, PURPLE_COLOR, LAVENDER_COLOR, transparent, ] // Formats numbers with appropriate suffixes (K, M, B) for better readability export const abbreviateNumber = ( num: number, suffix?: string, ): string | number => { if (suffix === '%') { return num } const absNum = Math.abs(num) if (absNum >= 1_000_000_000) { return (num / 1000000000).toFixed(1).replace(/\.0$/, '') + 'B' } if (absNum >= 1_000_000) { return (num / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M' } if (absNum >= 1_000) { return (num / 1_000).toFixed(1).replace(/\.0$/, '') + 'K' } return num } interface KeyLegend { key: string stroke: string } // Creates a legend configuration for data visualization with color mapping export const createDataKeyLegend = ( obj: Record, colorSet?: Record | string[], ): KeyLegend[] => { const colorPalette = colorSet ? Array.isArray(colorSet) ? colorSet : Object.values(colorSet) : colors const objCopy = { ...obj } if (objCopy.tooltip) { delete objCopy.tooltip } delete objCopy.date const keys = Object.keys(obj) const length = keys.length const keyLegend: KeyLegend[] = [] for (let i = 0; i < length; i++) { keyLegend.push({ key: keys[i], stroke: colorPalette[i], }) } return keyLegend } export const dateRangeIncludesTodayCheck = ( endDate: string | Date, ): boolean => { return ( moment(endDate).startOf('day').format('MM-DD-YYYY') === moment().startOf('day').format('MM-DD-YYYY') ) } export const getHeight = (width: number): number => { if (width > 768 && width < 1440) { return 160 } else { return 200 } } // Dynamically adjusts animation timing based on data complexity export const lineAnimationDelay = (length: number): number | undefined => { if (length > 20) { return 800 } else if (length > 4) { return 600 } return undefined } export const lineAnimationDuration = (length: number): number | undefined => { if (length > 20) { return 450 } else if (length > 4) { return 700 } return undefined } export const statColors = (): Record => { return { success: GREEN_COLOR, error: RED_COLOR, standard: LAVENDER_COLOR, } } const currencyString = (value: number): string => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(value) } const safeNumber = (value: unknown): number => { if (value === null || value === undefined) return 0 const num = Number(value) return isNaN(num) ? 0 : num } interface GraphDataItem { date: string | number [key: string]: unknown } interface MonthAgoResult { date: number value: number | string } // Normalization factor for converting milliseconds to a comparable date value const TIME_NORMALIZATION_FACTOR = 10_000_000 /** * Creates data point for one month ago by finding or interpolating values from the provided graph data * @param graphData Array of data points containing dates and values * @param keyName String or array of strings representing the key(s) to extract values from * @param weeklyDataPoints Optional boolean indicating if data points are weekly * @returns Object containing normalized date and value for one month ago * @example * const data = [ * { date: '2024-05-01', revenue: 100 }, * { date: '2024-05-08', revenue: 200 } * ]; * createMonthAgoData(data, 'revenue', true); * // Returns: { date: 16830, value: 150 } */ export const createMonthAgoData = ( graphData: GraphDataItem[], keyName: string | string[], weeklyDataPoints?: boolean, ): MonthAgoResult => { let oneMonthAgo = 0 let oneMonthAgoValue = 0 let aggregateValue = 0 const monthDate = moment().subtract(1, 'month') const parsedMonth = monthDate.valueOf() oneMonthAgo = parsedMonth / TIME_NORMALIZATION_FACTOR // Process data points to find or interpolate the target value for (let i = 0; i < graphData.length; i++) { const item = graphData[i] const itemDateNormalized = Number(item.date) / TIME_NORMALIZATION_FACTOR const closeToMonthAgoCheck = Math.round((oneMonthAgo - itemDateNormalized) / 10) * 10 const day = item.date if (moment(day).format('MMM DD YYYY') === monthDate.format('MMM DD YYYY')) { oneMonthAgo = itemDateNormalized if (Array.isArray(keyName)) { for (const key of keyName) { aggregateValue += safeNumber(item[key]) } oneMonthAgoValue = aggregateValue } else { oneMonthAgoValue = safeNumber(item[keyName]) } break } else if ( oneMonthAgoValue === 0 && oneMonthAgo - itemDateNormalized > 0 && oneMonthAgo - itemDateNormalized < 60 ) { const daysFromLastDataPoint = Math.floor(closeToMonthAgoCheck / 10) let nextNum: number let num: number let averageNum: number if (Array.isArray(keyName)) { let weeklyAggregate = 0 let nextNumAggregate = 0 for (const key of keyName) { weeklyAggregate += safeNumber(item[key]) nextNumAggregate += graphData[i + 1] ? safeNumber(graphData[i + 1][key]) : safeNumber(item[key]) } nextNum = nextNumAggregate num = (aggregateValue - nextNum) / 7 averageNum = aggregateValue - num * (daysFromLastDataPoint + 1) oneMonthAgoValue = weeklyDataPoints ? weeklyAggregate + (Math.abs(weeklyAggregate - nextNum) / 7) * daysFromLastDataPoint * (weeklyAggregate < nextNum ? 1 : -1) : daysFromLastDataPoint === 0 ? aggregateValue : Math.round(averageNum) } else { nextNum = graphData[i + 1] ? safeNumber(graphData[i + 1][keyName]) : safeNumber(item[keyName]) num = (safeNumber(item[keyName]) - nextNum) / 7 averageNum = safeNumber(item[keyName]) - num * (daysFromLastDataPoint + 1) oneMonthAgoValue = weeklyDataPoints ? (safeNumber(item[keyName]) + nextNum) / 2 : daysFromLastDataPoint === 0 ? safeNumber(item[keyName]) : Math.round(averageNum) } break } } if ( (Array.isArray(keyName) && keyName.some((k) => k.toLowerCase().includes('revenue'))) || (!Array.isArray(keyName) && (keyName.toLowerCase().includes('revenue') || keyName.toLowerCase().includes('sales'))) ) { return { date: oneMonthAgo, value: currencyString(oneMonthAgoValue) } } else { return { date: oneMonthAgo, value: oneMonthAgoValue } } }