From 434a15076e5ef52f01e1b7494b2d5bd845c9f9b0 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 4 Jan 2026 14:23:08 +0000 Subject: [PATCH] fix: generate continuous time series data with zero-filled gaps for charts Issues fixed: 1. Charts not rendering correctly due to sparse data (missing time buckets) 2. Period "1h" returning empty data when aggregate stats not yet available Changes: - Generate continuous time series with 0 values for all time buckets - Truncate start time to hour/day boundaries for consistent bucketing - Fallback to package-level stats aggregation when registry totals unavailable - Add proper time range filtering (since <= time_bucket <= now) Behavior now: - All time periods (1h, 1day, 7day, 30day) return complete data sets - Missing hours/days are filled with value: 0 - Chart libraries can render continuous lines/bars correctly - No more empty data for "1h" period Example output (1 hour period): Before: [{"timestamp":"14:00","value":5}, {"timestamp":"15:00","value":3}] After: [{"timestamp":"13:00","value":0}, {"timestamp":"14:00","value":5}, {"timestamp":"15:00","value":3}, {"timestamp":"16:00","value":0}] Resolves: "Chart doesn't generate correctly - it should automatically fill 0 for the empty periods to render correctly" --- pkg/metadata/gormstore/gormstore_v2.go | 60 ++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/pkg/metadata/gormstore/gormstore_v2.go b/pkg/metadata/gormstore/gormstore_v2.go index 5c16e06..cb900f2 100644 --- a/pkg/metadata/gormstore/gormstore_v2.go +++ b/pkg/metadata/gormstore/gormstore_v2.go @@ -583,29 +583,38 @@ func (s *GORMStoreV2) GetTimeSeriesStats(ctx context.Context, period string, reg // Determine which table to query based on period var tableName string var since time.Time + var bucketDuration time.Duration switch period { case "1h": tableName = "download_stats_hourly" - since = time.Now().Add(-1 * time.Hour) + since = time.Now().Add(-1 * time.Hour).Truncate(time.Hour) + bucketDuration = time.Hour case "1day": tableName = "download_stats_hourly" - since = time.Now().Add(-24 * time.Hour) + since = time.Now().Add(-24 * time.Hour).Truncate(time.Hour) + bucketDuration = time.Hour case "7day": tableName = "download_stats_daily" - since = time.Now().Add(-7 * 24 * time.Hour) + since = time.Now().Add(-7 * 24 * time.Hour).Truncate(24 * time.Hour) + bucketDuration = 24 * time.Hour case "30day": tableName = "download_stats_daily" - since = time.Now().Add(-30 * 24 * time.Hour) + since = time.Now().Add(-30 * 24 * time.Hour).Truncate(24 * time.Hour) + bucketDuration = 24 * time.Hour default: tableName = "download_stats_hourly" - since = time.Now().Add(-24 * time.Hour) + since = time.Now().Add(-24 * time.Hour).Truncate(time.Hour) + bucketDuration = time.Hour } + now := time.Now() + + // Try to get registry-level aggregate stats first query := s.db.WithContext(ctx). Table(tableName). Select("time_bucket as timestamp, download_count as value"). - Where("time_bucket >= ?", since) + Where("time_bucket >= ? AND time_bucket <= ?", since, now) // Filter by registry if specified if registry != "" { @@ -630,10 +639,45 @@ func (s *GORMStoreV2) GetTimeSeriesStats(ctx context.Context, period string, reg return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to get time series stats") } + // If no aggregate data found, try summing package-level stats + if len(results) == 0 { + sumQuery := s.db.WithContext(ctx). + Table(tableName). + Select("time_bucket as timestamp, SUM(download_count) as value"). + Where("time_bucket >= ? AND time_bucket <= ?", since, now) + + if registry != "" { + registryID, err := s.getRegistryID(registry) + if err != nil { + return nil, err + } + sumQuery = sumQuery.Where("registry_id = ? AND package_id IS NOT NULL", registryID) + } else { + sumQuery = sumQuery.Where("package_id IS NOT NULL") + } + + sumQuery = sumQuery.Group("time_bucket").Order("time_bucket ASC") + + if err := sumQuery.Scan(&results).Error; err != nil { + return nil, errors.Wrap(err, errors.ErrCodeStorageFailure, "failed to get time series stats from package data") + } + } + + // Create a map of existing data points + dataMap := make(map[time.Time]int64) for _, r := range results { + dataMap[r.Timestamp] = r.Value + } + + // Generate continuous time series with 0 values for missing periods + for t := since; t.Before(now) || t.Equal(now); t = t.Add(bucketDuration) { + value := int64(0) + if v, exists := dataMap[t]; exists { + value = v + } stats.DataPoints = append(stats.DataPoints, &metadata.TimeSeriesDataPoint{ - Timestamp: r.Timestamp, - Value: r.Value, + Timestamp: t, + Value: value, }) }