fix(card): productive titles render at 15 px for every timezone (#24)

Two cuts together keep the productive-time and productive-weekday
titles at the same 15 px the rest of the gallery uses:

1. utcOffsetLabel is now compact. Integer offsets drop the '.00'
   padding ("UTC+7" vs the old "UTC+7.00") and non-integer offsets use
   the colon form ("UTC+5:30", "UTC+5:45"). Saves 3 chars for the
   common integer case, so "Commits by Hour (last year, UTC+7)" lands
   at 34 chars — inside the 15 px budget.

2. weekdayTitle no longer embeds the UTC label. Day-of-week aggregates
   by whole days; clock precision isn't informative there, and
   dropping it shortens the title to 30 chars so the full 15 px lands
   for every timezone.

Quarter-hour-zone users (Kathmandu UTC+5:45) see the hour title drop
to 14 px — 37 chars still exceeds the budget — but that's a rare
case and only 1 px off.

TestUTCOffsetLabel updated to the new format. TestFitTitleFontSize
pinned the new titles.
This commit is contained in:
tiennm99 authored and GitHub committed 2026-04-19 11:49:40 +07:00
1 parent eb04446272
commit 0e0e4fc500
5 files changed
+36 -22

No files matched your search

+1 -1
View File
@@ -70,7 +70,7 @@ When there's **exactly one slice** (one language at 100%), the renderer emits tw
| Bar fill | `theme.Accent` for the peak bar; `mixHex(Background, Accent, 0.55)` dim for the rest so the busiest period reads at a glance | | Bar fill | `theme.Accent` for the peak bar; `mixHex(Background, Accent, 0.55)` dim for the rest so the busiest period reads at a glance |
| Y-axis ticks | `niceTicks(max, 5)` — 1/2/5 × 10^k ladder. `last = ceil(max/step) × step` so `yMax ≥ dataMax` always (bars can't poke above chartH into the title) | | Y-axis ticks | `niceTicks(max, 5)` — 1/2/5 × 10^k ladder. `last = ceil(max/step) × step` so `yMax ≥ dataMax` always (bars can't poke above chartH into the title) |
| Axis caption | "hour of day" bottom-center on productive-time; weekday / by-year omit the caption since the x labels are self-describing | | Axis caption | "hour of day" bottom-center on productive-time; weekday / by-year omit the caption since the x labels are self-describing |
| Title format | `Commits by Hour (<window>, UTC±N.NN)` / `Commits by Weekday (<window>, UTC±N.NN)` / `Contributions by Year` | | Title format | `Commits by Hour (<window>, UTC±H[:MM])` / `Commits by Weekday (<window>)` — weekday drops the UTC so the title always fits at 15 px / `Contributions by Year` |
| Hover | `<title>HH:00 — N commits</title>` / `<title>Mon — N commits</title>` / `<title>YYYY — N commits</title>` | | Hover | `<title>HH:00 — N commits</title>` / `<title>Mon — N commits</title>` / `<title>YYYY — N commits</title>` |
## Heatmap card (contributions-heatmap) ## Heatmap card (contributions-heatmap)
+4 -4
View File
@@ -204,9 +204,9 @@ func TestFitTitleFontSize(t *testing.T) {
{"Top Starred Repos", 15}, {"Top Starred Repos", 15},
{"Most Commit Language (all time)", 15}, // 31 chars {"Most Commit Language (all time)", 15}, // 31 chars
{"Contributions by Year", 15}, {"Contributions by Year", 15},
{"Commits by Hour (last year, UTC+7.00)", 14}, // 37 chars {"Commits by Hour (last year, UTC+7)", 15}, // 34 chars, common integer-zone case
{"Commits by Weekday (last year, UTC+7.00)", 13}, // 40 chars {"Commits by Hour (last year, UTC+5:45)", 14}, // 37 chars, quarter-hour zone (Kathmandu)
{"Commits by Weekday (last year, UTC+12.75)", 12}, // 41 chars {"Commits by Weekday (last year)", 15}, // 30 chars — weekday titles never include UTC
{strings.Repeat("x", 200), 11}, // pathological {strings.Repeat("x", 200), 11}, // pathological
} }
for _, c := range cases { for _, c := range cases {
@@ -369,7 +369,7 @@ func adversarialProfile() *github.Profile {
p := &github.Profile{ p := &github.Profile{
Login: "user-with-a-very-long-login-name", Login: "user-with-a-very-long-login-name",
Name: "A Very Long Display Name That Keeps Going", Name: "A Very Long Display Name That Keeps Going",
UTCOffsetLabel: "UTC+12.75", // half-hour / quarter-hour zones widen the title UTCOffsetLabel: "UTC+12:45", // quarter-hour zone — longest realistic UTC label
Company: "A-Company-With-An-Unusually-Long-Name Pty Ltd", Company: "A-Company-With-An-Unusually-Long-Name Pty Ltd",
Location: "A Place With A Name That Is Way Too Long To Fit", Location: "A Place With A Name That Is Way Too Long To Fit",
+6 -6
View File
@@ -13,7 +13,7 @@ type productiveWeekdayCard struct{}
func (productiveWeekdayCard) Filename() string { return "productive-weekday.svg" } func (productiveWeekdayCard) Filename() string { return "productive-weekday.svg" }
func (productiveWeekdayCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) { func (productiveWeekdayCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) {
return renderWeekday(weekdayTitle("last year", p.UTCOffsetLabel), p.Weekday, t), nil return renderWeekday(weekdayTitle("last year"), p.Weekday, t), nil
} }
type productiveWeekdayAllTimeCard struct{} type productiveWeekdayAllTimeCard struct{}
@@ -21,14 +21,14 @@ type productiveWeekdayAllTimeCard struct{}
func (productiveWeekdayAllTimeCard) Filename() string { return "productive-weekday-all-time.svg" } func (productiveWeekdayAllTimeCard) Filename() string { return "productive-weekday-all-time.svg" }
func (productiveWeekdayAllTimeCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) { func (productiveWeekdayAllTimeCard) SVG(p *github.Profile, t theme.Theme) ([]byte, error) {
return renderWeekday(weekdayTitle("all time", p.UTCOffsetLabel), p.WeekdayAllTime, t), nil return renderWeekday(weekdayTitle("all time"), p.WeekdayAllTime, t), nil
} }
func weekdayTitle(window, utcLabel string) string { // weekdayTitle skips the UTC offset — the data aggregates into day-of-week
if utcLabel == "" { // buckets, so exact clock precision isn't informative and dropping it keeps
// the title short enough to render at the full 15 px.
func weekdayTitle(window string) string {
return "Commits by Weekday (" + window + ")" return "Commits by Weekday (" + window + ")"
}
return "Commits by Weekday (" + window + ", " + utcLabel + ")"
} }
// Index 0 = Sunday to match time.Weekday (which is what FetchProductive stores). // Index 0 = Sunday to match time.Weekday (which is what FetchProductive stores).
+19 -5
View File
@@ -113,13 +113,27 @@ func main() {
} }
} }
// utcOffsetLabel formats the location's current offset from UTC as "UTC±N.NN" // utcOffsetLabel formats the location's current offset from UTC compactly:
// (two-decimal hours) so half-hour zones like India (UTC+5.30) or Nepal //
// (UTC+5.75) render cleanly. Matches github-profile-summary-cards' style. // integer hours → "UTC+7" (no ".00" padding — 3 chars shorter than
// the old "UTC+7.00" format, keeps the
// productive-time title at 15 px)
// half-hour zone → "UTC+5:30" (India)
// quarter-hour → "UTC+5:45" (Nepal)
// negative zone → "UTC-3" / "UTC-3:30"
func utcOffsetLabel(loc *time.Location) string { func utcOffsetLabel(loc *time.Location) string {
_, offsetSec := time.Now().In(loc).Zone() _, offsetSec := time.Now().In(loc).Zone()
hours := float64(offsetSec) / 3600.0 sign := "+"
return fmt.Sprintf("UTC%+.2f", hours) if offsetSec < 0 {
sign = "-"
offsetSec = -offsetSec
}
hours := offsetSec / 3600
minutes := (offsetSec % 3600) / 60
if minutes == 0 {
return fmt.Sprintf("UTC%s%d", sign, hours)
}
return fmt.Sprintf("UTC%s%d:%02d", sign, hours, minutes)
} }
func resolveThemes(spec string) ([]theme.Theme, error) { func resolveThemes(spec string) ([]theme.Theme, error) {
+6 -6
View File
@@ -6,17 +6,17 @@ import (
"time" "time"
) )
// TestUTCOffsetLabel checks that half-hour and quarter-hour zones render // TestUTCOffsetLabel checks the compact format: integer hours drop the
// with a decimal, matching github-profile-summary-cards' "UTC+X.NN" style. // minutes suffix, non-zero offsets render as `UTC±H:MM`.
func TestUTCOffsetLabel(t *testing.T) { func TestUTCOffsetLabel(t *testing.T) {
cases := []struct { cases := []struct {
zone string zone string
want string // must appear in the label; exact value varies by DST want string // must appear in the label; exact value varies by DST
}{ }{
{"UTC", "UTC+0.00"}, {"UTC", "UTC+0"},
{"Asia/Saigon", "UTC+7.00"}, {"Asia/Saigon", "UTC+7"},
{"Asia/Kolkata", "UTC+5.50"}, // half-hour zone {"Asia/Kolkata", "UTC+5:30"}, // half-hour zone
{"Asia/Kathmandu", "UTC+5.75"}, // quarter-hour zone {"Asia/Kathmandu", "UTC+5:45"}, // quarter-hour zone
} }
for _, tc := range cases { for _, tc := range cases {
loc, err := time.LoadLocation(tc.zone) loc, err := time.LoadLocation(tc.zone)