diff --git a/docs/design-guidelines.md b/docs/design-guidelines.md index 4ff13584..03c985ad 100644 --- a/docs/design-guidelines.md +++ b/docs/design-guidelines.md @@ -50,7 +50,7 @@ Cap rows at what fits: up to 7 rows per card. Stats splits commits into lifetime | Donut centre | `(250, 110)` | | Outer radius | 55 | | Inner radius | 30 | -| Top-N entries shown | 7 (overflow collapses into "Other") | +| Top-N entries shown | Up to 7 named languages, plus an "Other" row when the tail is non-zero (8 rows max) | | Slice stroke | `theme.Background`, 1.5 px (gap between slices) | | Legend origin | `(20, 55)` | | Legend row height | 20 px | diff --git a/internal/card/card_test.go b/internal/card/card_test.go index 5872ea57..e35f2d3f 100644 --- a/internal/card/card_test.go +++ b/internal/card/card_test.go @@ -141,6 +141,37 @@ func TestDonutEmpty(t *testing.T) { } } +// TestDonutTopSevenPlusOther confirms the "7 named + Other" contract: when +// the input has more than 7 languages, the legend shows all 7 real entries +// followed by a single "Other" bucket. Regression guard against anyone +// sliding topN back to "N-1 real + Other" packing. +func TestDonutTopSevenPlusOther(t *testing.T) { + th, _ := theme.Lookup("dracula") + stats := []github.LangStat{ + {Name: "Go", Color: "#00ADD8", Value: 100}, + {Name: "TypeScript", Color: "#3178c6", Value: 90}, + {Name: "Python", Color: "#3572A5", Value: 80}, + {Name: "Rust", Color: "#dea584", Value: 70}, + {Name: "JavaScript", Color: "#f1e05a", Value: 60}, + {Name: "HTML", Color: "#e34c26", Value: 50}, + {Name: "Shell", Color: "#89e051", Value: 40}, + {Name: "Kotlin", Color: "#A97BFF", Value: 30}, + {Name: "Java", Color: "#b07219", Value: 20}, + } + svg := string(renderDonutCard("Test", stats, th)) + for _, want := range []string{"Go", "TypeScript", "Python", "Rust", "JavaScript", "HTML", "Shell", "Other"} { + if !strings.Contains(svg, ">"+want+" ") { + t.Errorf("expected legend row for %q; not found in:\n%s", want, svg) + } + } + // Kotlin + Java spill into Other (they must NOT appear as named rows). + for _, dropped := range []string{">Kotlin ", ">Java "} { + if strings.Contains(svg, dropped) { + t.Errorf("expected %q to be collapsed into Other, but it renders:\n%s", dropped, svg) + } + } +} + func TestFormatInt(t *testing.T) { cases := map[int]string{ 0: "0", @@ -356,12 +387,19 @@ func adversarialProfile() *github.Profile { TotalContributedTo: 777, TotalContributionsLastYear: 200_000, CreatedAt: time.Date(2008, 1, 1, 0, 0, 0, 0, time.UTC), + // Nine languages so the donut's "top 7 + Other" layout renders all + // eight legend rows — catches any regression that breaks the tall + // legend geometry at y≈195. ReposByLanguage: []github.LangStat{ {Name: "JavaScript", Color: "#f1e05a", Value: 1234}, {Name: "TypeScript", Color: "#3178c6", Value: 999}, {Name: "Go", Color: "#00ADD8", Value: 500}, {Name: "Rust", Color: "#dea584", Value: 321}, {Name: "Python", Color: "#3572A5", Value: 200}, + {Name: "HTML", Color: "#e34c26", Value: 180}, + {Name: "Shell", Color: "#89e051", Value: 120}, + {Name: "Kotlin", Color: "#A97BFF", Value: 60}, + {Name: "Java", Color: "#b07219", Value: 30}, }, CommitsByLanguage: []github.LangStat{ {Name: "JavaScript", Color: "#f1e05a", Value: 1_000_000}, diff --git a/internal/card/donut_chart.go b/internal/card/donut_chart.go index 3aad4365..2726992f 100644 --- a/internal/card/donut_chart.go +++ b/internal/card/donut_chart.go @@ -99,19 +99,23 @@ func polar(cx, cy float64, r, angle float64) (float64, float64) { return cx + r*math.Cos(angle), cy + r*math.Sin(angle) } -// collapseOther returns the top (n-1) slices plus an "Other" row summing the -// rest. When the slice fits, it's returned as-is. +// collapseOther returns the top n named entries, optionally followed by an +// "Other" row summing everything past that. "Top N" means N actual languages +// — the Other row is a bonus when there's a non-zero tail, not one of the N. +// When the input fits in N entries the caller gets the slice back as-is. func collapseOther(in []github.LangStat, n int) []github.LangStat { if len(in) <= n { return in } - out := make([]github.LangStat, 0, n) - out = append(out, in[:n-1]...) + out := make([]github.LangStat, 0, n+1) + out = append(out, in[:n]...) var rest int64 - for _, s := range in[n-1:] { + for _, s := range in[n:] { rest += s.Value } - out = append(out, github.LangStat{Name: "Other", Value: rest}) + if rest > 0 { + out = append(out, github.LangStat{Name: "Other", Value: rest}) + } return out }