Files
ghstats/internal/card/productive_weekday.go
T
tiennm99andGitHub fd4c70e53e feat(card): add S-tier cards — heatmap, streak, by-year, weekday, top-starred (#3)
Five new cards, all derived from data FetchProductive / FetchProfile already
pull, so zero additional API calls:

- contributions-heatmap: 7×53 calendar grid with a 5-bucket intensity ramp
  mixed from each theme's Background→Accent so palettes with no dedicated
  heat ramp still render sensibly.
- streak: current streak, longest streak with date ranges, active/total days.
- contributions-by-year: one bar per active year, peak year highlighted.
- productive-weekday + -all-time: 7-bar day-of-week mirror of the hour-of-day
  cards; FetchProductive now also fills Weekday / WeekdayAllTime histograms
  during the same commit-history pass.
- top-starred-repos: top 5 owned non-fork repos by stargazer count; threads
  Stars through RepoInfo.

Card count: 9 → 14. Registered in allCards grouped by recency (last-year
block, then all-time block). Render test extended to cover all new files
and realistic daily-series inputs.
2026-04-19 08:58:59 +07:00

112 lines
3.3 KiB
Go

package card
import (
"fmt"
"strings"
"github.com/tiennm99/ghstats/internal/github"
"github.com/tiennm99/ghstats/internal/theme"
)
type productiveWeekdayCard struct{}
func (productiveWeekdayCard) Filename() string { return "productive-weekday.svg" }
func (productiveWeekdayCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) {
return renderWeekday(weekdayTitle("last year", p.UTCOffsetLabel), p.Weekday, t), nil
}
type productiveWeekdayAllTimeCard struct{}
func (productiveWeekdayAllTimeCard) Filename() string { return "productive-weekday-all-time.svg" }
func (productiveWeekdayAllTimeCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) {
return renderWeekday(weekdayTitle("all time", p.UTCOffsetLabel), p.WeekdayAllTime, t), nil
}
func weekdayTitle(window, utcLabel string) string {
if utcLabel == "" {
return "Commits by Weekday (" + window + ")"
}
return "Commits by Weekday (" + window + ", " + utcLabel + ")"
}
// Index 0 = Sunday to match time.Weekday (which is what FetchProductive stores).
var weekdayLabels = [7]string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
// renderWeekday draws a 7-bar chart: one bar per weekday. Reuses the same
// axis math as the hour-of-day card so the two feel like a matched pair.
func renderWeekday(title string, data [7]int, t theme.Theme) []byte {
const (
width = 340
height = 200
leftAxis = 35
rightPad = 15
topPad = 45
chartH = 110
barGap = 6
)
chartW := width - leftAxis - rightPad
barW := float64(chartW-barGap*6) / 7.0
var b strings.Builder
b.WriteString(header(width, height, t.Background, t.Stroke, t.StrokeOpacity, t.Title, title))
max := 0
peak := 0
for i, v := range data {
if v > max {
max = v
peak = i
}
}
yMax := float64(max)
if yMax == 0 {
yMax = 1
}
ticks := niceTicks(yMax, 5)
if len(ticks) > 0 {
yMax = ticks[len(ticks)-1]
}
// Y axis + ticks.
fmt.Fprintf(&b, `
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s"/>`,
leftAxis, topPad, leftAxis, topPad+chartH, t.Muted)
for _, v := range ticks {
y := topPad + chartH - int(float64(chartH)*v/yMax)
fmt.Fprintf(&b, `
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s"/>
<text x="%d" y="%d" font-size="10" fill="%s" text-anchor="end">%s</text>`,
leftAxis-4, y, leftAxis, y, t.Muted,
leftAxis-6, y+3, t.Muted, escapeXML(formatTick(v)))
}
// X axis baseline + weekday labels.
fmt.Fprintf(&b, `
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s"/>`,
leftAxis, topPad+chartH, leftAxis+chartW, topPad+chartH, t.Muted)
// Bars. Peak weekday gets full Accent; others the dimmed variant so the
// busiest day reads at a glance.
dim := mixHex(t.Background, t.Accent, 0.55)
for i := 0; i < 7; i++ {
count := data[i]
barH := float64(chartH) * float64(count) / yMax
x := float64(leftAxis) + (barW+float64(barGap))*float64(i)
y := float64(topPad+chartH) - barH
fill := dim
if i == peak && max > 0 {
fill = t.Accent
}
fmt.Fprintf(&b, `
<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" rx="2" fill="%s"><title>%s — %d commits</title></rect>
<text x="%.2f" y="%d" font-size="10" fill="%s" text-anchor="middle">%s</text>`,
x, y, barW, barH, fill, weekdayLabels[i], count,
x+barW/2, topPad+chartH+14, t.Muted, weekdayLabels[i])
}
b.WriteString(footer)
return []byte(b.String())
}