From dedce3da736834f977c9beee2af3188e34b72da4 Mon Sep 17 00:00:00 2001 From: Tien Nguyen Minh Date: Sun, 19 Apr 2026 11:14:42 +0700 Subject: [PATCH] =?UTF-8?q?fix(card):=20make=20heatmap=20cells=203=C3=97?= =?UTF-8?q?=20taller=20=E2=80=94=20width=20was=20tight,=20height=20had=20r?= =?UTF-8?q?oom=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cells were 4 × 4 with 1 px gap: the grid used 35 of 140 available vertical pixels (25 %) and ~80 px of dead space below. Widening the cells isn't an option — 53 weeks already consume every horizontal pixel past the weekday-label gutter. So stretch vertically instead: cellW = 4 (unchanged) cellH = 12 cellGap = 1 Grid footprint is now 265 × 91 px inside a 340 × 200 frame, with a 32 px gap below the grid for the legend. Each weekday reads as a distinct horizontal band instead of a cramped postage-stamp row. Legend keeps 8 × 8 SQUARE swatches so the "Less ▢▢▢▢▢ More" row is still recognisable as an intensity legend rather than a stretched echo of the data cells. Label baseline offset switches from `cellSize - 1` to `cellH - 3` so "Mon"/"Wed"/"Fri" sit visually centred in the taller rows. --- docs/design-guidelines.md | 3 +- internal/card/contributions_heatmap.go | 50 ++++++++++++++++---------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/docs/design-guidelines.md b/docs/design-guidelines.md index 97bc4e6b..780b2a1f 100644 --- a/docs/design-guidelines.md +++ b/docs/design-guidelines.md @@ -78,7 +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 × 4 px, 1 px gap. Grid footprint = `53 × 5 = 265 px`; `leftPad(30) + 265 = 295 px`, leaving a 45 px right gutter so the grid doesn't bleed into the frame border | +| 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 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 | diff --git a/internal/card/contributions_heatmap.go b/internal/card/contributions_heatmap.go index ae7feaa3..e15c617e 100644 --- a/internal/card/contributions_heatmap.go +++ b/internal/card/contributions_heatmap.go @@ -22,17 +22,26 @@ 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. // -// Geometry: 4 px cells + 1 px gap = 5 px per week column × 53 weeks = 265 px. -// Left pad 30 (weekday labels), right pad 45 — both well clear of the frame -// so the grid doesn't read as "bleeding" off the card. +// 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) func renderHeatmap(title string, days []github.DailyContribution, t theme.Theme) []byte { const ( - width = 340 - height = 200 - cellSize = 4 - cellGap = 1 - leftPad = 30 - topPad = 62 + width = 340 + height = 200 + cellW = 4 + cellH = 12 + cellGap = 1 + leftPad = 30 + topPad = 62 ) var b strings.Builder @@ -65,7 +74,7 @@ func renderHeatmap(title string, days []github.DailyContribution, t theme.Theme) if label == "" { continue } - y := topPad + i*(cellSize+cellGap) + cellSize - 1 + y := topPad + i*(cellH+cellGap) + cellH - 3 fmt.Fprintf(&b, ` %s`, leftPad-4, y, t.Muted, label) @@ -86,7 +95,7 @@ func renderHeatmap(title string, days []github.DailyContribution, t theme.Theme) continue } lastMonth = first.Month() - x := leftPad + w*(cellSize+cellGap) + x := leftPad + w*(cellW+cellGap) if x > monthLabelMaxX { continue } @@ -103,28 +112,31 @@ 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*(cellSize+cellGap) - y := topPad + d*(cellSize+cellGap) + x := leftPad + w*(cellW+cellGap) + y := topPad + d*(cellH+cellGap) fmt.Fprintf(&b, ` - %s — %d`, - x, y, cellSize, cellSize, fill, + %s — %d`, + x, y, cellW, cellH, fill, cell.Date.Format("2006-01-02"), cell.Count) } } - // Legend: "Less ▢▢▢▢▢ More" at bottom right. + // Legend at the bottom right uses square swatches so "Less ▢▢▢▢▢ More" + // reads as a classic intensity legend rather than a stretched echo of the + // rectangular data cells. + const legendCell = 8 legendX := width - 110 legendY := height - 15 fmt.Fprintf(&b, ` Less`, legendX, legendY, t.Muted) for i, c := range ramp { fmt.Fprintf(&b, ` - `, - legendX+28+i*(cellSize+2), legendY-cellSize+2, cellSize, cellSize, c) + `, + legendX+28+i*(legendCell+2), legendY-legendCell+2, legendCell, legendCell, c) } fmt.Fprintf(&b, ` More`, - legendX+28+5*(cellSize+2)+2, legendY, t.Muted) + legendX+28+5*(legendCell+2)+2, legendY, t.Muted) b.WriteString(footer) return []byte(b.String())