mirror of
https://github.com/tiennm99/ghstats.git
synced 2026-09-02 10:19:54 +00:00
fix(card): show 7 *named* languages + Other, not 6 + Other (#19)
With topN=7 the previous collapseOther kept only the first 6 entries and added "Other" as the 7th row. A user expecting to see 7 actual languages in the legend saw six named languages plus "Other" — the exact complaint just raised about the profile repo's donut. Flip the semantic: the "top N" slots are reserved for real languages, and "Other" is an extra row when (and only when) there's a non-zero tail past the Nth entry. Topologically that means up to 8 legend rows — still fits the card frame (row 8 text baseline at y=195, card height 200). - TestDonutTopSevenPlusOther pins the new contract with a 9-language input. - adversarialProfile in TestCardsFitFrame bumped to 9 languages so the stress test exercises the 8-row legend geometry. - design-guidelines: the donut row re-reads "Up to 7 named languages, plus an 'Other' row when the tail is non-zero (8 rows max)".
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user