mirror of
https://github.com/tiennm99/ghstats.git
synced 2026-09-02 06:20:38 +00:00
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:
@@ -28,22 +28,34 @@ type Profile struct {
|
||||
TotalContributedTo int
|
||||
TotalContributions int // lifetime contributions from calendar + restricted
|
||||
|
||||
// Sorted desc by bytes. Color is GitHub's linguist color or "" if absent.
|
||||
Languages []LangStat
|
||||
// Count of owned repos grouped by primary language, sorted desc by Value.
|
||||
ReposByLanguage []LangStat
|
||||
|
||||
// Count of commits (last year, by this user) attributed to each repo's
|
||||
// primary language, sorted desc. Populated by FetchProductive.
|
||||
CommitsByLanguage []LangStat
|
||||
|
||||
// Commit-count histogram indexed by [day-of-week 0=Sunday][hour-of-day 0-23].
|
||||
Productive [7][24]int
|
||||
|
||||
// TopRepos is the list of owned repo names sorted by stargazer count desc,
|
||||
// populated by FetchProfile. Used as the seed set for FetchProductive.
|
||||
TopRepos []string
|
||||
// TopRepos are owned repos sorted by stargazer count desc. Populated by
|
||||
// FetchProfile and consumed by FetchProductive.
|
||||
TopRepos []RepoInfo
|
||||
}
|
||||
|
||||
// LangStat is one row in the top-languages card.
|
||||
// LangStat is one row in a language breakdown card. Value is repo count or
|
||||
// commit count depending on which slice holds it.
|
||||
type LangStat struct {
|
||||
Name string
|
||||
Color string
|
||||
Bytes int64
|
||||
Value int64
|
||||
}
|
||||
|
||||
// RepoInfo is the minimal owned-repo summary used for downstream fetches.
|
||||
type RepoInfo struct {
|
||||
Name string
|
||||
PrimaryLanguage string
|
||||
PrimaryColor string
|
||||
}
|
||||
|
||||
// repoNode is the GraphQL shape of one repository node; kept here because
|
||||
|
||||
@@ -24,17 +24,22 @@ type productiveGQL struct {
|
||||
}
|
||||
|
||||
// FetchProductive fills p.Productive with a [7][24] commit histogram over the
|
||||
// last year, gathered from the user's top-starred owned repos. Each repo is
|
||||
// sampled up to maxPerRepo commits to keep the cost bounded.
|
||||
// last year and p.CommitsByLanguage with commit counts attributed to each
|
||||
// repo's primary language. Commits are gathered from the given repos (usually
|
||||
// p.TopRepos[:N]); each repo is sampled up to maxPerRepo commits to keep the
|
||||
// cost bounded.
|
||||
//
|
||||
// The timezone loc is applied to CommittedDate so the heatmap reflects when the
|
||||
// user actually commits, not UTC.
|
||||
func (c *Client) FetchProductive(p *Profile, repos []string, loc *time.Location, maxPerRepo int) error {
|
||||
// The timezone loc is applied to CommittedDate so the heatmap reflects when
|
||||
// the user actually commits, not UTC.
|
||||
func (c *Client) FetchProductive(p *Profile, repos []RepoInfo, loc *time.Location, maxPerRepo int) error {
|
||||
if loc == nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
since := time.Now().AddDate(-1, 0, 0).UTC().Format(time.RFC3339)
|
||||
|
||||
commitsByLang := map[string]int64{}
|
||||
langColor := map[string]string{}
|
||||
|
||||
for _, repo := range repos {
|
||||
var cursor *string
|
||||
seen := 0
|
||||
@@ -44,7 +49,7 @@ func (c *Client) FetchProductive(p *Profile, repos []string, loc *time.Location,
|
||||
}
|
||||
vars := map[string]any{
|
||||
"login": p.Login,
|
||||
"repo": repo,
|
||||
"repo": repo.Name,
|
||||
"userId": p.ID,
|
||||
"since": since,
|
||||
}
|
||||
@@ -68,6 +73,12 @@ func (c *Client) FetchProductive(p *Profile, repos []string, loc *time.Location,
|
||||
}
|
||||
tl := t.In(loc)
|
||||
p.Productive[int(tl.Weekday())][tl.Hour()]++
|
||||
if repo.PrimaryLanguage != "" {
|
||||
commitsByLang[repo.PrimaryLanguage]++
|
||||
if _, ok := langColor[repo.PrimaryLanguage]; !ok {
|
||||
langColor[repo.PrimaryLanguage] = repo.PrimaryColor
|
||||
}
|
||||
}
|
||||
seen++
|
||||
}
|
||||
if !h.PageInfo.HasNextPage {
|
||||
@@ -77,5 +88,7 @@ func (c *Client) FetchProductive(p *Profile, repos []string, loc *time.Location,
|
||||
cursor = &end
|
||||
}
|
||||
}
|
||||
|
||||
p.CommitsByLanguage = sortLangStats(commitsByLang, langColor)
|
||||
return nil
|
||||
}
|
||||
|
||||
+27
-12
@@ -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
|
||||
})
|
||||
|
||||
@@ -2,19 +2,19 @@ package github
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSortLanguages(t *testing.T) {
|
||||
bytes := map[string]int64{
|
||||
"Go": 500,
|
||||
"Python": 300,
|
||||
"TypeScript": 500, // tie with Go → alphabetical wins
|
||||
"HTML": 100,
|
||||
func TestSortLangStats(t *testing.T) {
|
||||
values := map[string]int64{
|
||||
"Go": 5,
|
||||
"Python": 3,
|
||||
"TypeScript": 5, // tie with Go → alphabetical wins
|
||||
"HTML": 1,
|
||||
}
|
||||
colors := map[string]string{
|
||||
"Go": "#00ADD8",
|
||||
"Python": "#3572A5",
|
||||
"TypeScript": "#3178c6",
|
||||
}
|
||||
got := sortLanguages(bytes, colors)
|
||||
got := sortLangStats(values, colors)
|
||||
|
||||
wantOrder := []string{"Go", "TypeScript", "Python", "HTML"}
|
||||
if len(got) != len(wantOrder) {
|
||||
|
||||
Reference in New Issue
Block a user