fix(card): heatmap cells back to 4x4 squares with side gutters (#22)

The 4x12 rectangular stretch from v1.2.3 read as "weird". Requirement
is square cells + comfortable side padding. Given the 53-week hard
constraint, those two requirements together pick the cell size:

  53 * (size + gap) + leftPad + rightPad = 340

Candidates audited:
  6 x 6, gap 1 → 371 wide, overflows the frame
  5 x 5, gap 1 → 318 wide, only 11 px of total side padding ("very close")
  5 x 5, gap 0 → 265 wide, cells touch (need a stroke to fake a gap)
  4 x 4, gap 1 → 265 wide, 30 + 45 px gutters, real 1 px gaps ✓
  3 x 3, gap 2 → 265 wide, cells become pinhead-sized

4 x 4 with a 1 px gap is the largest square that keeps breathing room
on both sides without any rendering trickery. Grid is 35 px tall;
there's leftover vertical space on the card but short beats "stretched
horizontal bands" visually. Bump topPad from 62 → 70 to offset the
grid slightly down from the title and reduce that apparent emptiness.

Legend swatches also revert to the same cellSize so the Less/More
bar matches the grid visually again.
This commit is contained in:
2026-04-19 11:23:48 +07:00
committed by GitHub
parent 9d5e117c87
commit 5cda59ae79
2 changed files with 19 additions and 25 deletions
+2 -2
View File
@@ -78,8 +78,8 @@ When there's **exactly one slice** (one language at 100%), the renderer emits tw
| Metric | Value |
| --- | --- |
| Grid | 7 rows × 53 columns (Sunday → Saturday, oldest week → newest) |
| Cell size | 4 wide × 12 tall, 1 px gap. Width is constrained (`leftPad 30 + 53 × 5 = 295 px`, 45 px right gutter); height has headroom so cells are rectangular — makes each weekday row a distinct horizontal band instead of a postage-stamp blur |
| Grid y-range | `topPad(62) .. 62 + 7 × 13 = 153 px`; 32 px gap to the legend at y ≈ 185 |
| Cell size | 4 × 4 px square, 1 px gap. 4 is the largest square that fits with breathing room on both sides (`leftPad 30 + 53 × 5 = 295 px`, 45 px right gutter). 5 × 5 with a gap overflows; 5 × 5 touching cells loses visible separation; rectangular cells look stretched. Card has vertical headroom to spare — we accept that in exchange for a clean square grid |
| Grid y-range | `topPad(70) .. 70 + 7 × 5 = 105 px` |
| Cell colour | 5-bucket ramp `mixHex(Background, Accent, k/4)` for `k ∈ 0..4` — no dedicated ramp field on the theme schema |
| Weekday labels | Mon / Wed / Fri only, right-anchored in the `leftPad` gutter |
| Month labels | Printed above the first week where a 1st-of-month day falls; skipped when `x > width 20` so `Dec` / `Apr` can't spill past the frame |
+17 -23
View File
@@ -22,26 +22,20 @@ func (contributionsHeatmapCard) SVG(p *github.Profile, t theme.Theme) ([]byte, e
// theme.Accent in four intensity buckets so every palette inherits a usable
// heatmap without a separate color ramp in the theme schema.
//
// Cells are intentionally rectangular: width is constrained by 53 weeks
// fitting in 340 leftPad rightPad, but height has lots of spare card
// real estate, so we make cells ~3× taller than wide. That turns the grid
// into distinct horizontal bands instead of a cramped postage-stamp.
//
// Geometry:
//
// width per column = cellW + cellGap = 4 + 1 = 5 px → 53 * 5 = 265 px
// leftPad 30 + 265 = 295 px grid right edge, 45 px right gutter
// height per row = cellH + cellGap = 12 + 1 = 13 px → 7 * 13 = 91 px
// grid y-range: 62 .. 153 (leaves 32 px gap to the legend at y ≈ 185)
// Cells are square. 53 weeks at (cellSize + gap) = 5 px per column fills
// 265 px, which leaves comfortable left (30) and right (45) gutters inside
// the 340 px card. 6 × 6 cells would overflow; 5 × 5 cells with a gap push
// back to the right edge. 4 × 4 is the largest square that keeps breathing
// room on both sides. The resulting grid is short (35 px tall) — the card
// has lots of vertical headroom — but short beats awkwardly stretched.
func renderHeatmap(title string, days []github.DailyContribution, t theme.Theme) []byte {
const (
width = 340
height = 200
cellW = 4
cellH = 12
cellGap = 1
leftPad = 30
topPad = 62
width = 340
height = 200
cellSize = 4 // square
cellGap = 1
leftPad = 30
topPad = 70
)
var b strings.Builder
@@ -74,7 +68,7 @@ func renderHeatmap(title string, days []github.DailyContribution, t theme.Theme)
if label == "" {
continue
}
y := topPad + i*(cellH+cellGap) + cellH - 3
y := topPad + i*(cellSize+cellGap) + cellSize - 1
fmt.Fprintf(&b, `
<text x="%d" y="%d" font-size="9" fill="%s" text-anchor="end">%s</text>`,
leftPad-4, y, t.Muted, label)
@@ -95,7 +89,7 @@ func renderHeatmap(title string, days []github.DailyContribution, t theme.Theme)
continue
}
lastMonth = first.Month()
x := leftPad + w*(cellW+cellGap)
x := leftPad + w*(cellSize+cellGap)
if x > monthLabelMaxX {
continue
}
@@ -112,11 +106,11 @@ func renderHeatmap(title string, days []github.DailyContribution, t theme.Theme)
continue // padding slot before the first real day
}
fill := ramp[bucketFor(cell.Count, buckets)]
x := leftPad + w*(cellW+cellGap)
y := topPad + d*(cellH+cellGap)
x := leftPad + w*(cellSize+cellGap)
y := topPad + d*(cellSize+cellGap)
fmt.Fprintf(&b, `
<rect x="%d" y="%d" width="%d" height="%d" rx="1.5" fill="%s"><title>%s — %d</title></rect>`,
x, y, cellW, cellH, fill,
x, y, cellSize, cellSize, fill,
cell.Date.Format("2006-01-02"), cell.Count)
}
}