mirror of
https://github.com/tiennm99/ghstats.git
synced 2026-09-02 14:20:13 +00:00
fix(contributions): trim last-year series to clean 13-month span
GitHub's contributionCalendar is week-aligned, so a "last year" query on 2026-05-02 returns days back to ~2025-04-27. That extra partial month inflated aggregateByMonth to 14 buckets and the x-axis stride logic placed 04/26 and 05/26 next to each other (x=290 / x=312), overlapping. Trim to start-of-(last.Month - 1y) so the chart spans a clean 13 months ending at the current month. Heatmap path is untouched — it needs the full week-aligned series.
This commit is contained in:
@@ -14,7 +14,28 @@ type contributionsCard struct{}
|
||||
func (contributionsCard) Filename() string { return "contributions.svg" }
|
||||
|
||||
func (contributionsCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) {
|
||||
return renderContributions("Contributions (last year)", p.DailyContributions, t), nil
|
||||
return renderContributions("Contributions (last year)", trimToLastYear(p.DailyContributions), t), nil
|
||||
}
|
||||
|
||||
// trimToLastYear keeps only the trailing 13-month window ending at the
|
||||
// series' last day. GitHub's contributionCalendar is week-aligned, so a
|
||||
// "last year" query bleeds into the calendar month before today minus one
|
||||
// year (e.g. asking on 2026-05-02 returns days back to ~2025-04-27). That
|
||||
// extra partial month inflates the bucket count to 14 and forces the x-axis
|
||||
// stride logic to place the final two month labels right next to each other.
|
||||
// Trimming to start-of-(today.Month - 1y) restores a clean 13-bucket span.
|
||||
func trimToLastYear(days []github.DailyContribution) []github.DailyContribution {
|
||||
if len(days) == 0 {
|
||||
return days
|
||||
}
|
||||
last := days[len(days)-1].Date
|
||||
cutoff := time.Date(last.Year()-1, last.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
for i, d := range days {
|
||||
if !d.Date.Before(cutoff) {
|
||||
return days[i:]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type contributionsAllTimeCard struct{}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package card
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tiennm99/ghstats/internal/github"
|
||||
)
|
||||
|
||||
// GitHub's contributionCalendar is week-aligned, so a "last year" series for a
|
||||
// query made on 2026-05-02 (Saturday) starts on 2025-04-27 (Sunday). Without
|
||||
// trimming the chart shows 14 calendar months and the x-axis label stride
|
||||
// places 04/26 and 05/26 next to each other, overlapping. Trimming should
|
||||
// drop everything before the first day of last.Month minus one year.
|
||||
func TestTrimToLastYearProducesCleanThirteenMonthSpan(t *testing.T) {
|
||||
start := time.Date(2025, 4, 27, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC)
|
||||
var days []github.DailyContribution
|
||||
for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
|
||||
days = append(days, github.DailyContribution{Date: d, Count: 1})
|
||||
}
|
||||
|
||||
trimmed := trimToLastYear(days)
|
||||
if len(trimmed) == 0 {
|
||||
t.Fatal("trimToLastYear returned no days")
|
||||
}
|
||||
first := trimmed[0].Date
|
||||
wantFirst := time.Date(2025, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
if !first.Equal(wantFirst) {
|
||||
t.Fatalf("first day = %s, want %s", first, wantFirst)
|
||||
}
|
||||
if !trimmed[len(trimmed)-1].Date.Equal(end) {
|
||||
t.Fatalf("last day = %s, want %s", trimmed[len(trimmed)-1].Date, end)
|
||||
}
|
||||
|
||||
buckets := aggregateByMonth(trimmed)
|
||||
if len(buckets) != 13 {
|
||||
t.Fatalf("bucket count = %d, want 13 (May 2025 .. May 2026)", len(buckets))
|
||||
}
|
||||
if buckets[0].Year != 2025 || buckets[0].Month != time.May {
|
||||
t.Fatalf("first bucket = %d/%d, want 2025/May", buckets[0].Year, buckets[0].Month)
|
||||
}
|
||||
if buckets[12].Year != 2026 || buckets[12].Month != time.May {
|
||||
t.Fatalf("last bucket = %d/%d, want 2026/May", buckets[12].Year, buckets[12].Month)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimToLastYearEmptyInput(t *testing.T) {
|
||||
if got := trimToLastYear(nil); got != nil {
|
||||
t.Fatalf("trimToLastYear(nil) = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user