import dayjs from 'dayjs' import type { LogsBarChartDatum } from './ProjectUsage.metrics' /** * Configuration for chart bucket sizes based on time interval */ const BUCKET_CONFIG = { '1hr': { bucketMinutes: 2, // 2-minute buckets expectedBuckets: 30, // 60 minutes / 2 = 30 buckets }, '1day': { bucketMinutes: 60, // 1-hour buckets expectedBuckets: 24, // 24 hours }, '7day': { bucketMinutes: 360, // 6-hour buckets expectedBuckets: 28, // 168 hours / 6 = 28 buckets }, } as const type IntervalKey = keyof typeof BUCKET_CONFIG /** * Normalizes chart data to consistent bucket sizes regardless of backend data density. * * For 1hr interval: Creates 30 buckets of 2 minutes each * For 1day interval: Creates 24 buckets of 1 hour each * For 7day interval: Creates 28 buckets of 6 hours each * * This ensures consistent bar width in charts and proper data aggregation. * * @param data - Raw chart data from backend * @param interval - Time interval key ('1hr', '1day', '7day') * @param endDate - End date for the chart (defaults to now) * @returns Array of exactly the expected number of buckets with aggregated data */ export function normalizeChartBuckets( data: LogsBarChartDatum[], interval: IntervalKey, endDate: Date = new Date() ): LogsBarChartDatum[] { const config = BUCKET_CONFIG[interval] const { bucketMinutes, expectedBuckets } = config // Calculate start time based on expected buckets const end = dayjs(endDate) const start = end.subtract(expectedBuckets * bucketMinutes, 'minute') // Create empty buckets const buckets: LogsBarChartDatum[] = [] let currentBucketStart = start for (let i = 0; i < expectedBuckets; i++) { buckets.push({ timestamp: currentBucketStart.toISOString(), ok_count: 0, warning_count: 0, error_count: 0, }) currentBucketStart = currentBucketStart.add(bucketMinutes, 'minute') } // If no data, return empty buckets if (!data || data.length === 0) { return buckets } // Aggregate data into buckets for (const datum of data) { const datumTime = dayjs(datum.timestamp) // Find which bucket this datum belongs to const bucketIndex = Math.floor(datumTime.diff(start, 'minute') / bucketMinutes) // Skip data points outside our time range if (bucketIndex < 0 || bucketIndex >= expectedBuckets) { continue } // Aggregate counts into the appropriate bucket buckets[bucketIndex].ok_count += datum.ok_count || 0 buckets[bucketIndex].warning_count += datum.warning_count || 0 buckets[bucketIndex].error_count += datum.error_count || 0 } return buckets }