mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-07-07 04:14:31 +00:00
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"
This commit is contained in:
@@ -583,29 +583,38 @@ func (s *GORMStoreV2) GetTimeSeriesStats(ctx context.Context, period string, reg
|
|||||||
// Determine which table to query based on period
|
// Determine which table to query based on period
|
||||||
var tableName string
|
var tableName string
|
||||||
var since time.Time
|
var since time.Time
|
||||||
|
var bucketDuration time.Duration
|
||||||
|
|
||||||
switch period {
|
switch period {
|
||||||
case "1h":
|
case "1h":
|
||||||
tableName = "download_stats_hourly"
|
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":
|
case "1day":
|
||||||
tableName = "download_stats_hourly"
|
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":
|
case "7day":
|
||||||
tableName = "download_stats_daily"
|
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":
|
case "30day":
|
||||||
tableName = "download_stats_daily"
|
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:
|
default:
|
||||||
tableName = "download_stats_hourly"
|
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).
|
query := s.db.WithContext(ctx).
|
||||||
Table(tableName).
|
Table(tableName).
|
||||||
Select("time_bucket as timestamp, download_count as value").
|
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
|
// Filter by registry if specified
|
||||||
if registry != "" {
|
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")
|
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 {
|
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{
|
stats.DataPoints = append(stats.DataPoints, &metadata.TimeSeriesDataPoint{
|
||||||
Timestamp: r.Timestamp,
|
Timestamp: t,
|
||||||
Value: r.Value,
|
Value: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user