mirror of
https://github.com/tiennm99/ghglance.git
synced 2026-10-20 02:17:25 +00:00
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.
85 lines
1.9 KiB
Go
85 lines
1.9 KiB
Go
package card
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/tiennm99/ghstats/internal/github"
|
|
"github.com/tiennm99/ghstats/internal/theme"
|
|
)
|
|
|
|
// renderLanguageCard draws a horizontal stacked bar + legend from a list of
|
|
// LangStats. Shared by the repos-per-language and most-commit-language cards.
|
|
//
|
|
// title is the card heading; empty is rendered as the "no data" fallback.
|
|
func renderLanguageCard(title string, stats []github.LangStat, t theme.Theme) []byte {
|
|
const (
|
|
width = 500
|
|
height = 220
|
|
topN = 6
|
|
barX = 25
|
|
barY = 60
|
|
barW = 450
|
|
barH = 10
|
|
legendX0 = 25
|
|
)
|
|
|
|
var b strings.Builder
|
|
b.WriteString(header(width, height, t.Background, t.Title, title))
|
|
|
|
if len(stats) > topN {
|
|
stats = stats[:topN]
|
|
}
|
|
|
|
if len(stats) == 0 {
|
|
fmt.Fprintf(&b, `
|
|
<text x="25" y="90" font-size="13" fill="%s">No data available.</text>`, t.Muted)
|
|
b.WriteString(footer)
|
|
return []byte(b.String())
|
|
}
|
|
|
|
var total int64
|
|
for _, s := range stats {
|
|
total += s.Value
|
|
}
|
|
|
|
fmt.Fprintf(&b, `
|
|
<rect x="%d" y="%d" width="%d" height="%d" rx="5" fill="%s"/>
|
|
<g>`,
|
|
barX, barY, barW, barH, t.Muted)
|
|
|
|
offset := float64(barX)
|
|
for _, s := range stats {
|
|
w := float64(barW) * float64(s.Value) / float64(total)
|
|
fmt.Fprintf(&b, `
|
|
<rect x="%.2f" y="%d" width="%.2f" height="%d" fill="%s"/>`,
|
|
offset, barY, w, barH, colorOrAccent(s.Color, t.Accent))
|
|
offset += w
|
|
}
|
|
b.WriteString(`
|
|
</g>`)
|
|
|
|
for i, s := range stats {
|
|
col := i % 2
|
|
row := i / 2
|
|
x := legendX0 + col*230
|
|
y := 110 + row*24
|
|
pct := 100 * float64(s.Value) / float64(total)
|
|
fmt.Fprintf(&b, `
|
|
<circle cx="%d" cy="%d" r="6" fill="%s"/>
|
|
<text x="%d" y="%d" font-size="13" fill="%s">%s %.2f%%</text>`,
|
|
x+6, y-4, colorOrAccent(s.Color, t.Accent),
|
|
x+20, y, t.Text, escapeXML(s.Name), pct)
|
|
}
|
|
|
|
b.WriteString(footer)
|
|
return []byte(b.String())
|
|
}
|
|
|
|
func colorOrAccent(c, fallback string) string {
|
|
if c == "" {
|
|
return fallback
|
|
}
|
|
return c
|
|
}
|