ChartDataTransform.utils.ts89 lines · main
1import dayjs from 'dayjs'
2
3import type { LogsBarChartDatum } from './ProjectUsage.metrics'
4
5/**
6 * Configuration for chart bucket sizes based on time interval
7 */
8const BUCKET_CONFIG = {
9 '1hr': {
10 bucketMinutes: 2, // 2-minute buckets
11 expectedBuckets: 30, // 60 minutes / 2 = 30 buckets
12 },
13 '1day': {
14 bucketMinutes: 60, // 1-hour buckets
15 expectedBuckets: 24, // 24 hours
16 },
17 '7day': {
18 bucketMinutes: 360, // 6-hour buckets
19 expectedBuckets: 28, // 168 hours / 6 = 28 buckets
20 },
21} as const
22
23type IntervalKey = keyof typeof BUCKET_CONFIG
24
25/**
26 * Normalizes chart data to consistent bucket sizes regardless of backend data density.
27 *
28 * For 1hr interval: Creates 30 buckets of 2 minutes each
29 * For 1day interval: Creates 24 buckets of 1 hour each
30 * For 7day interval: Creates 28 buckets of 6 hours each
31 *
32 * This ensures consistent bar width in charts and proper data aggregation.
33 *
34 * @param data - Raw chart data from backend
35 * @param interval - Time interval key ('1hr', '1day', '7day')
36 * @param endDate - End date for the chart (defaults to now)
37 * @returns Array of exactly the expected number of buckets with aggregated data
38 */
39export function normalizeChartBuckets(
40 data: LogsBarChartDatum[],
41 interval: IntervalKey,
42 endDate: Date = new Date()
43): LogsBarChartDatum[] {
44 const config = BUCKET_CONFIG[interval]
45 const { bucketMinutes, expectedBuckets } = config
46
47 // Calculate start time based on expected buckets
48 const end = dayjs(endDate)
49 const start = end.subtract(expectedBuckets * bucketMinutes, 'minute')
50
51 // Create empty buckets
52 const buckets: LogsBarChartDatum[] = []
53 let currentBucketStart = start
54
55 for (let i = 0; i < expectedBuckets; i++) {
56 buckets.push({
57 timestamp: currentBucketStart.toISOString(),
58 ok_count: 0,
59 warning_count: 0,
60 error_count: 0,
61 })
62 currentBucketStart = currentBucketStart.add(bucketMinutes, 'minute')
63 }
64
65 // If no data, return empty buckets
66 if (!data || data.length === 0) {
67 return buckets
68 }
69
70 // Aggregate data into buckets
71 for (const datum of data) {
72 const datumTime = dayjs(datum.timestamp)
73
74 // Find which bucket this datum belongs to
75 const bucketIndex = Math.floor(datumTime.diff(start, 'minute') / bucketMinutes)
76
77 // Skip data points outside our time range
78 if (bucketIndex < 0 || bucketIndex >= expectedBuckets) {
79 continue
80 }
81
82 // Aggregate counts into the appropriate bucket
83 buckets[bucketIndex].ok_count += datum.ok_count || 0
84 buckets[bucketIndex].warning_count += datum.warning_count || 0
85 buckets[bucketIndex].error_count += datum.error_count || 0
86 }
87
88 return buckets
89}