fix: activity chart uses rolling chronological windows (real dates), not cyclical
Day/Week/Month/Year now show the anchor day hour-by-hour, the last 7 days, the last 30 days, and the last 12 months — anchored to the period end (else now), in real date order. Replaces the cyclical hour-of-day/weekday/day-of-month/month buckets, which left future days empty and ordered the weekend after Monday.
This commit is contained in:
+42
-26
@@ -163,12 +163,13 @@ type Stats struct {
|
||||
ByYear []Bucket `json:"by_year"` // chronological
|
||||
ByMonth []Bucket `json:"by_month"` // "YYYY-MM", chronological
|
||||
|
||||
// Cyclical activity distributions (UTC), for the "activity over time" chart's
|
||||
// day/week/month/year granularity selector.
|
||||
ByHour []Bucket `json:"by_hour"` // hour of day 00..23
|
||||
ByDOW []Bucket `json:"by_dow"` // day of week Mon..Sun
|
||||
ByDOM []Bucket `json:"by_dom"` // day of month 1..31
|
||||
ByMonthYr []Bucket `json:"by_month_yr"` // month Jan..Dec
|
||||
// Rolling chronological activity windows (UTC), anchored to the newest QSO, for
|
||||
// the "activity over time" chart's day/week/month/year granularity selector.
|
||||
// Real dates in order (empty buckets are real, just zero) — no cyclical wrap.
|
||||
ByHour []Bucket `json:"by_hour"` // the newest QSO's day, hour by hour (00..23)
|
||||
ByDay7 []Bucket `json:"by_day7"` // the last 7 days ending at the newest QSO
|
||||
ByDay30 []Bucket `json:"by_day30"` // the last 30 days
|
||||
ByMonth12 []Bucket `json:"by_month12"` // the last 12 months
|
||||
|
||||
// ── Period / contest metrics ──
|
||||
// Meaningful only over a WINDOW: "12 QSO/h" across seventeen years says
|
||||
@@ -476,40 +477,55 @@ func (r *Repo) Stats(ctx context.Context, from, to time.Time, contestID string,
|
||||
s.ByMonth = fillMonths(monthC, first, last)
|
||||
|
||||
s.periodMetrics(times, from, to, first, last)
|
||||
s.timeDistributions(times)
|
||||
// Anchor the rolling activity windows to the period end when filtered, else to
|
||||
// now (so "week" is the last 7 days ending today, like the operator expects).
|
||||
ref := to
|
||||
if ref.IsZero() {
|
||||
ref = time.Now()
|
||||
}
|
||||
s.recentSeries(times, ref)
|
||||
s.ensureNonNil()
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// timeDistributions builds the cyclical activity buckets (hour-of-day, day-of-week,
|
||||
// day-of-month, month-of-year) from the dated QSOs, in UTC. Each is a fixed-length
|
||||
// series so the chart axis is stable even for empty slots.
|
||||
func (s *Stats) timeDistributions(times []entry) {
|
||||
// recentSeries builds the rolling chronological activity windows for the chart's
|
||||
// day/week/month/year selector, in UTC and anchored to `ref` (the period end when
|
||||
// filtered, else now). Real dates in order — today and the days before it — so the
|
||||
// axis reads chronologically and empty days are just zero, not misordered/wrapped.
|
||||
func (s *Stats) recentSeries(times []entry, ref time.Time) {
|
||||
ref = ref.UTC()
|
||||
day0 := time.Date(ref.Year(), ref.Month(), ref.Day(), 0, 0, 0, 0, time.UTC) // midnight of the anchor day
|
||||
hour := make([]int, 24)
|
||||
dow := make([]int, 7) // 0 = Monday
|
||||
dom := make([]int, 31)
|
||||
moy := make([]int, 12)
|
||||
daily := map[string]int{}
|
||||
monthly := map[string]int{}
|
||||
for _, e := range times {
|
||||
t := e.t.UTC()
|
||||
hour[t.Hour()]++
|
||||
dow[(int(t.Weekday())+6)%7]++ // shift Sunday(0) → 6 so Monday is first
|
||||
if d := t.Day(); d >= 1 && d <= 31 {
|
||||
dom[d-1]++
|
||||
if t.Year() == ref.Year() && t.YearDay() == ref.YearDay() {
|
||||
hour[t.Hour()]++
|
||||
}
|
||||
moy[int(t.Month())-1]++
|
||||
daily[t.Format("2006-01-02")]++
|
||||
monthly[t.Format("2006-01")]++
|
||||
}
|
||||
// Day: the anchor day, hour by hour.
|
||||
for h := 0; h < 24; h++ {
|
||||
s.ByHour = append(s.ByHour, Bucket{Key: fmt.Sprintf("%02d", h), Count: hour[h]})
|
||||
s.ByHour = append(s.ByHour, Bucket{Key: fmt.Sprintf("%02dh", h), Count: hour[h]})
|
||||
}
|
||||
for i, n := range []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} {
|
||||
s.ByDOW = append(s.ByDOW, Bucket{Key: n, Count: dow[i]})
|
||||
// Week: the last 7 days ending today (today is last). Label = weekday + day.
|
||||
for i := 6; i >= 0; i-- {
|
||||
d := day0.AddDate(0, 0, -i)
|
||||
s.ByDay7 = append(s.ByDay7, Bucket{Key: d.Format("Mon 2"), Count: daily[d.Format("2006-01-02")]})
|
||||
}
|
||||
for d := 1; d <= 31; d++ {
|
||||
s.ByDOM = append(s.ByDOM, Bucket{Key: fmt.Sprintf("%d", d), Count: dom[d-1]})
|
||||
// Month: the last 30 days.
|
||||
for i := 29; i >= 0; i-- {
|
||||
d := day0.AddDate(0, 0, -i)
|
||||
s.ByDay30 = append(s.ByDay30, Bucket{Key: d.Format("2/1"), Count: daily[d.Format("2006-01-02")]})
|
||||
}
|
||||
for i, n := range []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} {
|
||||
s.ByMonthYr = append(s.ByMonthYr, Bucket{Key: n, Count: moy[i]})
|
||||
// Year: the last 12 months.
|
||||
m0 := time.Date(ref.Year(), ref.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
for i := 11; i >= 0; i-- {
|
||||
m := m0.AddDate(0, -i, 0)
|
||||
s.ByMonth12 = append(s.ByMonth12, Bucket{Key: m.Format("Jan 06"), Count: monthly[m.Format("2006-01")]})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user