feat(card): add contributions area chart card

New 5-contributions.svg renders the last year's contribution calendar
as a monthly smooth-filled area chart. Pure Go SVG; no extra API calls
— one additional contributionCalendar.weeks block in the existing
profile GraphQL query carries the data.

- Y-axis mirrored on both sides with nice ticks.
- X-axis labels in YY/MM format, every other month to avoid overlap.
- Smooth curve via Catmull-Rom interpolation converted to cubic Bezier
  (d3.curveCatmullRom default tension 0.5).
- Missing months between first and last are inserted as zero-count so
  the chart stays time-continuous.
This commit is contained in:
2026-04-18 21:20:52 +07:00
parent 442da96f99
commit d0d3862780
5 changed files with 227 additions and 1 deletions
+20
View File
@@ -36,6 +36,12 @@ type profileGQL struct {
RestrictedContributionsCount int `json:"restrictedContributionsCount"`
ContributionCalendar struct {
TotalContributions int `json:"totalContributions"`
Weeks []struct {
ContributionDays []struct {
ContributionCount int `json:"contributionCount"`
Date string `json:"date"`
} `json:"contributionDays"`
} `json:"weeks"`
} `json:"contributionCalendar"`
} `json:"contributionsCollection"`
@@ -101,6 +107,20 @@ func (c *Client) FetchProfile(login string) (*Profile, error) {
p.TotalCommits = cc.TotalCommitContributions
p.TotalReviews = cc.TotalPullRequestReviewContributions
p.TotalContributions = cc.ContributionCalendar.TotalContributions + cc.RestrictedContributionsCount
// Flatten week → day into a linear daily series sorted by date.
for _, w := range cc.ContributionCalendar.Weeks {
for _, d := range w.ContributionDays {
t, err := time.Parse("2006-01-02", d.Date)
if err != nil {
continue
}
p.DailyContributions = append(p.DailyContributions, DailyContribution{
Date: t,
Count: d.ContributionCount,
})
}
}
}
for _, r := range u.Repositories.Nodes {