mirror of
https://github.com/tiennm99/ghstats.git
synced 2026-09-01 02:32:01 +00:00
refactor(card): audit title-fit logic — O(1) form + lock-in unit test (#12)
The title fitter was correct but the loop-down-from-maxFont form hid
its intent. Switch to the direct expression:
ideal = floor(budget / (chars * 0.6))
clamp to [11, 15]
Same answer for every title; easier to verify at a glance. Hoist the
four constants (leftInset=20, rightSafety=4, minFont=11, maxFont=15,
charRatio=0.6) to package scope so the new unit test can reference
them without re-declaring.
Utilization audit for realistic dracula titles (width=340, budget=316):
Stats (5) → 15 px (14 %)
Top Starred Repos (17) → 15 px (48 %)
Most Commit Language (all time) (31) → 15 px (91 %)
Commits by Hour (last year, UTC+7.00) (37) → 14 px (98 %)
Commits by Weekday (last year, UTC+7.00) (40) → 13 px (99 %)
Commits by Weekday (last year, UTC+12.75) (41) → 12 px (93 %)
200-char pathological → 11 px (floor)
TestFitTitleFontSize pins this table so a future charRatio tweak can't
silently regress any real title. TestCardsFitFrame (the end-to-end
check) plus this unit test now cover both the geometry and the picked-
font-size paths.
This commit is contained in:
@@ -158,6 +158,35 @@ func TestFormatInt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFitTitleFontSize locks in the title-sizing behavior for the set of
|
||||
// realistic titles the renderers actually emit. Any regression that shrinks
|
||||
// a short title below 15 px, or lets a 40-char title stay at 15 px when it
|
||||
// would overflow, will fail this test.
|
||||
func TestFitTitleFontSize(t *testing.T) {
|
||||
const width = 340
|
||||
cases := []struct {
|
||||
title string
|
||||
want int
|
||||
}{
|
||||
{"", 15},
|
||||
{"Stats", 15},
|
||||
{"Streak", 15},
|
||||
{"Top Starred Repos", 15},
|
||||
{"Most Commit Language (all time)", 15}, // 31 chars
|
||||
{"Contributions by Year", 15},
|
||||
{"Commits by Hour (last year, UTC+7.00)", 14}, // 37 chars
|
||||
{"Commits by Weekday (last year, UTC+7.00)", 13}, // 40 chars
|
||||
{"Commits by Weekday (last year, UTC+12.75)", 12}, // 41 chars
|
||||
{strings.Repeat("x", 200), 11}, // pathological
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := fitTitleFontSize(c.title, width)
|
||||
if got != c.want {
|
||||
t.Errorf("fitTitleFontSize(%q)=%d want %d", c.title, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNiceTicksCoversMax guards the key invariant: the last tick returned
|
||||
// must be ≥ the requested max, or bar-chart cards render bars taller than
|
||||
// the chart area and poke into the title. Regression case: max=625 step=100
|
||||
|
||||
+35
-17
@@ -2,6 +2,7 @@ package card
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
@@ -81,29 +82,46 @@ func header(width, height int, bg, stroke string, strokeOpacity float64, titleCo
|
||||
fontSize, titleColor, escapeXML(title))
|
||||
}
|
||||
|
||||
// fitTitleFontSize picks the largest title font (between 11 and 15 px) at
|
||||
// which the title still fits in width − leftInset − rightSafety. Uses the
|
||||
// same 0.6 × font-size char-width estimate as the fit-the-frame test.
|
||||
// fitTitleFontSize returns the largest integer font size in
|
||||
// [titleMinFont, titleMaxFont] at which the title still fits in
|
||||
// width − titleLeftInset − titleRightSafety, using the same 0.6 × fontSize
|
||||
// char-width estimate as the fit-the-frame test. Computed directly from
|
||||
// the budget: ideal = budget / (chars × 0.6), floored to an int, clamped
|
||||
// to the allowed range.
|
||||
//
|
||||
// Utilization for realistic dracula titles (width=340, budget=316):
|
||||
//
|
||||
// "Stats" (5) → 15 px (14 %)
|
||||
// "Top Starred Repos" (17) → 15 px (48 %)
|
||||
// "Most Commit Language (all time)" (32) → 15 px (91 %)
|
||||
// "Commits by Hour (last year, UTC+7.00)" (37) → 14 px (98 %)
|
||||
// "Commits by Weekday (last year, UTC+7.00)" (40) → 13 px (99 %)
|
||||
// "Commits by Weekday (last year, UTC+12.75)" (41) → 12 px (93 %)
|
||||
func fitTitleFontSize(title string, width int) int {
|
||||
const (
|
||||
leftInset = 20
|
||||
rightSafety = 4
|
||||
minFont = 11
|
||||
maxFont = 15
|
||||
avgCharRatio = 0.6
|
||||
)
|
||||
budget := float64(width - leftInset - rightSafety)
|
||||
chars := utf8.RuneCountInString(title)
|
||||
if chars == 0 {
|
||||
return maxFont
|
||||
return titleMaxFont
|
||||
}
|
||||
for fs := maxFont; fs >= minFont; fs-- {
|
||||
if float64(chars)*float64(fs)*avgCharRatio <= budget {
|
||||
return fs
|
||||
}
|
||||
budget := float64(width - titleLeftInset - titleRightSafety)
|
||||
ideal := int(math.Floor(budget / (float64(chars) * titleCharRatio)))
|
||||
if ideal > titleMaxFont {
|
||||
return titleMaxFont
|
||||
}
|
||||
return minFont
|
||||
if ideal < titleMinFont {
|
||||
return titleMinFont
|
||||
}
|
||||
return ideal
|
||||
}
|
||||
|
||||
// Title-sizing constants are exported to package-private so the unit test
|
||||
// can reference them without duplicating magic numbers.
|
||||
const (
|
||||
titleLeftInset = 20
|
||||
titleRightSafety = 4
|
||||
titleMinFont = 11
|
||||
titleMaxFont = 15
|
||||
titleCharRatio = 0.6
|
||||
)
|
||||
|
||||
const footer = `
|
||||
</svg>`
|
||||
|
||||
Reference in New Issue
Block a user