refactor: split language card into repos-per-language and most-commit-language

Align card set with github-profile-summary-cards' 5-card layout:

  0-profile-details.svg       (unchanged)
  1-repos-per-language.svg    (new) owned repos grouped by primary language
  2-most-commit-language.svg  (new) last-year commits attributed to each repo's primary language
  3-stats.svg                 (renumbered)
  4-productive-time.svg       (renumbered)

- FetchProductive now fills p.CommitsByLanguage from the same commit history
  it uses for the heatmap, so no extra API calls are introduced.
- TopRepos carries primary language so productive-time can aggregate by lang.
- LangStat.Bytes renamed to Value (repo count or commit count, context-dependent).
- Shared bar+legend renderer extracted to language_bar.go.
- Ignore generated output/ directory.
This commit is contained in:
tiennm99 committed 2026-04-18 18:57:42 +07:00
1 parent ff4975fae2
commit 7eb83be9de
14 files changed
+151 -74

No files matched your search

+27 -12
View File
@@ -50,15 +50,16 @@ type profileGQL struct {
} `json:"user"`
}
// FetchProfile collects profile, stats and language data for the given user.
// Repositories are paginated up to 10 pages (1000 owned repos) as a safety cap.
// FetchProfile collects profile, stats and repos-per-language data for the
// given user. Owned repos are paginated up to 10 pages (1000 repos) as a
// safety cap.
func (c *Client) FetchProfile(login string) (*Profile, error) {
if login == "" {
return nil, errors.New("empty user")
}
p := &Profile{Login: login}
langBytes := map[string]int64{}
reposPerLang := map[string]int64{}
langColor := map[string]string{}
var cursor *string
@@ -105,9 +106,22 @@ func (c *Client) FetchProfile(login string) (*Profile, error) {
for _, r := range u.Repositories.Nodes {
p.TotalStars += r.StargazerCount
p.TotalForks += r.ForkCount
p.TopRepos = append(p.TopRepos, r.Name)
info := RepoInfo{Name: r.Name}
if r.PrimaryLanguage != nil {
info.PrimaryLanguage = r.PrimaryLanguage.Name
info.PrimaryColor = r.PrimaryLanguage.Color
reposPerLang[r.PrimaryLanguage.Name]++
if _, ok := langColor[r.PrimaryLanguage.Name]; !ok {
langColor[r.PrimaryLanguage.Name] = r.PrimaryLanguage.Color
}
}
p.TopRepos = append(p.TopRepos, info)
// Capture secondary language colors so productive-time's
// per-language aggregation can color them even if that language
// isn't the primary of any other repo.
for _, e := range r.Languages.Edges {
langBytes[e.Node.Name] += e.Size
if _, ok := langColor[e.Node.Name]; !ok {
langColor[e.Node.Name] = e.Node.Color
}
@@ -121,18 +135,19 @@ func (c *Client) FetchProfile(login string) (*Profile, error) {
cursor = &end
}
p.Languages = sortLanguages(langBytes, langColor)
p.ReposByLanguage = sortLangStats(reposPerLang, langColor)
return p, nil
}
func sortLanguages(bytes map[string]int64, color map[string]string) []LangStat {
out := make([]LangStat, 0, len(bytes))
for name, b := range bytes {
out = append(out, LangStat{Name: name, Color: color[name], Bytes: b})
// sortLangStats returns a slice sorted desc by value; ties break alphabetically.
func sortLangStats(values map[string]int64, color map[string]string) []LangStat {
out := make([]LangStat, 0, len(values))
for name, v := range values {
out = append(out, LangStat{Name: name, Color: color[name], Value: v})
}
sort.Slice(out, func(i, j int) bool {
if out[i].Bytes != out[j].Bytes {
return out[i].Bytes > out[j].Bytes
if out[i].Value != out[j].Value {
return out[i].Value > out[j].Value
}
return out[i].Name < out[j].Name
})