mirror of
https://github.com/tiennm99/ghglance.git
synced 2026-10-11 03:13:20 +00:00
Two ghstats cards side-by-side at 500px overflow GitHub's README column
width (~816px). Matching github-profile-summary-cards' 340x200 lets
consumers place two cards per row cleanly.
- Shared header() rect corner radius 8→6, title font 18→15, title anchor
(25,35)→(20,30).
- All card layouts reworked:
* profile + stats: row padding 70→55 / 24→20, icon 14→12, value
font 13→12, stats right-anchor x=475→320.
* donut_chart: centre (380,120)→(250,110), outerR 70→55, innerR 38→30,
legend y0 70→55, swatch 12→10, topN 6→5.
* productive-time: leftAxis 50→35, topPad 60→45, barGap 2→1.
* contributions: leftPad/rightPad 35→28, topPad 60→45, chartH 120→105.
Visual parity with github-profile-summary-cards; two-per-row layouts in
profile READMEs now fit.
Also: document the one-time GitHub Marketplace publishing step in
deployment-guide (no CLI flag exists — UI checkbox only).
117 lines
3.5 KiB
Go
117 lines
3.5 KiB
Go
package card
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/tiennm99/ghstats/internal/github"
|
|
"github.com/tiennm99/ghstats/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, `
|
|
<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: horizontal line + tick labels.
|
|
fmt.Fprintf(&b, `
|
|
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s"/>`,
|
|
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, `
|
|
<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s"/>
|
|
<text x="%d" y="%d" font-size="10" fill="%s" text-anchor="middle">%d</text>`,
|
|
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, `
|
|
<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" rx="2" fill="%s"><title>%02d:00 — %d commits</title></rect>`,
|
|
x, y, barW, barH, t.Accent, h, count)
|
|
}
|
|
|
|
// X-axis caption.
|
|
fmt.Fprintf(&b, `
|
|
<text x="%d" y="%d" font-size="11" fill="%s" text-anchor="middle">hour of day</text>`,
|
|
leftAxis+chartW/2, topPad+chartH+34, t.Muted)
|
|
|
|
b.WriteString(footer)
|
|
return []byte(b.String())
|
|
}
|