package card import ( "fmt" "strings" "github.com/tiennm99/ghglance/internal/github" "github.com/tiennm99/ghglance/internal/theme" ) type productiveCard struct{} func (productiveCard) Filename() string { return "productive-time.svg" } func (productiveCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) { return renderProductiveTime(productiveTitle("last year", p.UTCOffsetLabel), p.Productive, t), nil } type productiveAllTimeCard struct{} func (productiveAllTimeCard) Filename() string { return "productive-time-all-time.svg" } func (productiveAllTimeCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) { return renderProductiveTime(productiveTitle("all time", p.UTCOffsetLabel), p.ProductiveAllTime, t), nil } // productiveTitle formats the window qualifier together with the UTC offset. // Omits the offset when unknown so the card still renders from a raw Profile. func productiveTitle(window, utcLabel string) string { if utcLabel == "" { return "Commits by Hour (" + window + ")" } return "Commits by Hour (" + window + ", " + utcLabel + ")" } // Hour ticks to label on the x-axis; same set the reference project uses. var xTickHours = [...]int{0, 6, 12, 18, 23} // renderProductiveTime draws a 24-hour bar chart with two-sided axes and // hover titles. Shared by the last-year and all-time productive-time cards. func renderProductiveTime(title string, data [24]int, t theme.Theme) []byte { const ( width = 340 height = 200 leftAxis = 35 rightPad = 15 topPad = 45 chartH = 110 barGap = 1 ) chartW := width - leftAxis - rightPad barW := float64(chartW-barGap*23) / 24.0 var b strings.Builder b.WriteString(header(width, height, t.Background, t.Stroke, t.StrokeOpacity, t.Title, title)) max := 0 for _, v := range data { if v > max { max = v } } yMax := float64(max) if yMax == 0 { yMax = 1 } ticks := niceTicks(yMax, 5) if len(ticks) > 0 { yMax = ticks[len(ticks)-1] } // Y-axis: vertical line + tick marks with labels. fmt.Fprintf(&b, ` `, leftAxis, topPad, leftAxis, topPad+chartH, t.Muted) for _, v := range ticks { y := topPad + chartH - int(float64(chartH)*v/yMax) fmt.Fprintf(&b, ` %s`, leftAxis-4, y, leftAxis, y, t.Muted, leftAxis-6, y+3, t.Muted, escapeXML(formatTick(v))) } // X-axis: horizontal line + tick labels. fmt.Fprintf(&b, ` `, leftAxis, topPad+chartH, leftAxis+chartW, topPad+chartH, t.Muted) for _, h := range xTickHours { x := leftAxis + int(barW*float64(h)+float64(barGap*h)+barW/2) fmt.Fprintf(&b, ` %d`, x, topPad+chartH, x, topPad+chartH+4, t.Muted, x, topPad+chartH+16, t.Muted, h) } // Bars. for h := 0; h < 24; h++ { count := data[h] barH := float64(chartH) * float64(count) / yMax x := float64(leftAxis) + barW*float64(h) + float64(barGap*h) y := float64(topPad+chartH) - barH fmt.Fprintf(&b, ` %02d:00 — %d commits`, x, y, barW, barH, t.Accent, h, count) } // X-axis caption. fmt.Fprintf(&b, ` hour of day`, leftAxis+chartW/2, topPad+chartH+34, t.Muted) b.WriteString(footer) return []byte(b.String()) }