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:
2026-04-19 10:04:02 +07:00
committed by GitHub
parent 31cf6e265f
commit 462d2af4dd
2 changed files with 64 additions and 17 deletions
+29
View File
@@ -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
View File
@@ -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>`