Files
aicyclingcoach-go/internal/analysis/downsample.go
2025-09-17 17:30:18 -07:00

96 lines
2.3 KiB
Go

package analysis
import (
"time"
)
// DownsampledPoint represents a single data point in a downsampled metric series
type DownsampledPoint struct {
TimeOffset int `json:"time_offset"` // Seconds from activity start
Value float64 `json:"value"` // Average value during this segment
Min float64 `json:"min,omitempty"`
Max float64 `json:"max,omitempty"`
}
// DownsampleMetric downsamples a metric array to the specified number of points
func DownsampleMetric(data []float64, duration time.Duration, targetPoints int) []DownsampledPoint {
if len(data) == 0 || targetPoints <= 0 {
return nil
}
totalSeconds := int(duration.Seconds())
if totalSeconds <= 0 {
return nil
}
// Calculate segment duration in seconds (floating point for accuracy)
segmentDuration := float64(totalSeconds) / float64(targetPoints)
if segmentDuration < 1 {
segmentDuration = 1
}
// Preallocate segments array
segments := make([]struct {
sum float64
count int
min float64
max float64
}, targetPoints)
// Initialize min/max with zero values - we'll update them when we see data
// Assign data points to segments
for i, value := range data {
// Calculate time offset for this data point
timeOffset := float64(i) * float64(totalSeconds) / float64(len(data))
segmentIndex := int(timeOffset / segmentDuration)
// Clamp to valid segment range
if segmentIndex >= targetPoints {
segmentIndex = targetPoints - 1
}
seg := &segments[segmentIndex]
seg.sum += value
seg.count++
// Initialize min/max on first value in segment
if seg.count == 1 {
seg.min = value
seg.max = value
} else {
if value < seg.min {
seg.min = value
}
if value > seg.max {
seg.max = value
}
}
}
// Build results array
results := make([]DownsampledPoint, targetPoints)
for j := 0; j < targetPoints; j++ {
seg := &segments[j]
timeOffset := int(float64(j) * segmentDuration)
if seg.count == 0 {
// For empty segments, omit min/max
results[j] = DownsampledPoint{
TimeOffset: timeOffset,
Value: 0,
}
} else {
avg := seg.sum / float64(seg.count)
results[j] = DownsampledPoint{
TimeOffset: timeOffset,
Value: avg,
Min: seg.min,
Max: seg.max,
}
}
}
return results
}