summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-27 21:41:26 +0300
committerPaul Buetow <paul@buetow.org>2026-05-27 21:41:26 +0300
commit573dce2ed0a85165970060b034c9bc3ec2317ba9 (patch)
tree71436fe053c35904e9f8e51c841ab7857a85af26 /internal
parentbd86e04f112edbe0f6b4f8c2b6a8a5161ff11699 (diff)
showcase: extend rank history to 32 weeks, trim leading empty columns
Previously the graph was fixed at 5 weekly data points. Now: - rankHistoryPoints raised from 5 → 32 so the store accumulates up to 32 weekly snapshots and each project carries 32 history entries. - GenerateRankHistorySVG trims all leading all-zero columns before layout, so the graph only spans from the oldest week that any project actually has data — no empty space on the left when history is short. - displayPoints (trimmed width) drives all layout math: xPos(), xLabels, and the X-axis loop; numPoints is only used for the reversal placement. - xLabelStep() thins X-axis text labels when many columns are visible (step 1 for ≤6, 2 for ≤12, 4 for ≤24, 8 for >24 columns) while still drawing a grid line at every column and always labelling 'now'. - Test: give alpha a Spot at '4w' so trimming keeps that column and the time-axis label test still finds '4w ago' in the output. - New TestXLabelStep_ReturnsSensibleSteps covers the new helper. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/showcase/rank_history.go2
-rw-r--r--internal/showcase/rank_history_svg.go63
-rw-r--r--internal/showcase/rank_history_svg_test.go28
3 files changed, 79 insertions, 14 deletions
diff --git a/internal/showcase/rank_history.go b/internal/showcase/rank_history.go
index 559237f..ba8594d 100644
--- a/internal/showcase/rank_history.go
+++ b/internal/showcase/rank_history.go
@@ -11,7 +11,7 @@ import (
const (
rankHistoryFilename = ".gitsyncer-showcase-rank-history.json"
- rankHistoryPoints = 5
+ rankHistoryPoints = 32 // up to 32 weekly snapshots kept in history
rankHistoryVersion = 1
)
diff --git a/internal/showcase/rank_history_svg.go b/internal/showcase/rank_history_svg.go
index fd77c83..d932edc 100644
--- a/internal/showcase/rank_history_svg.go
+++ b/internal/showcase/rank_history_svg.go
@@ -159,7 +159,7 @@ func buildLegendSVG(allProjects []svgProjectData, legendX, plotH int) string {
// legend entry highlights the corresponding plot line.
// - The SVG uses width/height="100%" so it fills the browser window.
func GenerateRankHistorySVG(summaries []ProjectSummary) string {
- numPoints := rankHistoryPoints // 5 weekly snapshots
+ numPoints := rankHistoryPoints // up to 32 weekly snapshots
// Collect per-project data, reversing the history so oldest is on the left.
allProjects := make([]svgProjectData, 0, len(summaries))
@@ -209,11 +209,29 @@ func GenerateRankHistorySVG(summaries []ProjectSummary) string {
colorIdx++
}
- // Human-readable X-axis labels (left = oldest, right = "now").
- // Position i is (numPoints-1-i) weeks ago; position numPoints-1 is "now".
- xLabels := make([]string, numPoints)
- for i := 0; i < numPoints; i++ {
- weeksAgo := numPoints - 1 - i
+ // Trim leading all-zero columns so the graph starts at the oldest week
+ // that has real data for any project (not at week 32 if history only goes
+ // back 5 weeks). The rightmost column is always "now" (index numPoints-1).
+ firstDataCol := numPoints - 1 // pessimistic: show at least "now"
+outer:
+ for col := 0; col < numPoints; col++ {
+ for _, proj := range allProjects {
+ if proj.Points[col].Spot > 0 {
+ firstDataCol = col
+ break outer
+ }
+ }
+ }
+ for i := range allProjects {
+ allProjects[i].Points = allProjects[i].Points[firstDataCol:]
+ }
+ displayPoints := numPoints - firstDataCol // actual columns to render
+
+ // Human-readable X-axis labels (left = oldest visible, right = "now").
+ // Position i is (displayPoints-1-i) weeks ago; position displayPoints-1 is "now".
+ xLabels := make([]string, displayPoints)
+ for i := 0; i < displayPoints; i++ {
+ weeksAgo := displayPoints - 1 - i
if weeksAgo == 0 {
xLabels[i] = "now"
} else {
@@ -226,10 +244,10 @@ func GenerateRankHistorySVG(summaries []ProjectSummary) string {
plotH := svgViewHeight - svgMarginTop - svgMarginBottom
xPos := func(i int) float64 {
- if numPoints <= 1 {
+ if displayPoints <= 1 {
return float64(svgMarginLeft) + float64(plotW)/2
}
- return float64(svgMarginLeft) + float64(i)*float64(plotW)/float64(numPoints-1)
+ return float64(svgMarginLeft) + float64(i)*float64(plotW)/float64(displayPoints-1)
}
// rank 1 → top of plot, maxRank → bottom of plot.
@@ -264,16 +282,21 @@ func GenerateRankHistorySVG(summaries []ProjectSummary) string {
}
// Vertical grid lines and X-axis labels.
+ // When there are many columns (long history), only label every Nth column
+ // so the axis stays readable; "now" (rightmost) is always labelled.
var xAxisBuf strings.Builder
plotBottom := float64(svgMarginTop + plotH)
- for i := 0; i < numPoints; i++ {
+ labelStep := xLabelStep(displayPoints)
+ for i := 0; i < displayPoints; i++ {
x := xPos(i)
fmt.Fprintf(&xAxisBuf,
`<line class="gl" x1="%.1f" y1="%d" x2="%.1f" y2="%.1f"/>`,
x, svgMarginTop, x, plotBottom)
- fmt.Fprintf(&xAxisBuf,
- `<text class="al" x="%.1f" y="%.1f" text-anchor="middle">%s</text>`,
- x, plotBottom+16, xLabels[i])
+ if i%labelStep == 0 || i == displayPoints-1 {
+ fmt.Fprintf(&xAxisBuf,
+ `<text class="al" x="%.1f" y="%.1f" text-anchor="middle">%s</text>`,
+ x, plotBottom+16, xLabels[i])
+ }
}
// Project lines and dot groups.
@@ -545,3 +568,19 @@ func gridStep(maxRank int) int {
return 1
}
}
+
+// xLabelStep returns how many X-axis columns to skip between printed labels so
+// the time axis stays readable when many weeks of history are displayed.
+// Grid lines are always drawn at every column; only labels are thinned.
+func xLabelStep(displayPoints int) int {
+ switch {
+ case displayPoints > 24:
+ return 8
+ case displayPoints > 12:
+ return 4
+ case displayPoints > 6:
+ return 2
+ default:
+ return 1
+ }
+}
diff --git a/internal/showcase/rank_history_svg_test.go b/internal/showcase/rank_history_svg_test.go
index 06aab4c..d086a7d 100644
--- a/internal/showcase/rank_history_svg_test.go
+++ b/internal/showcase/rank_history_svg_test.go
@@ -16,7 +16,7 @@ func makeTestSummaries() []ProjectSummary {
{Spot: 2, Anchor: "1w", SnapshotDate: "2026-05-20"},
{Spot: 2, Anchor: "2w", SnapshotDate: "2026-05-13"},
{Spot: 3, Anchor: "3w", SnapshotDate: "2026-05-06"},
- {Spot: 0, Anchor: "4w"}, // missing snapshot week
+ {Spot: 4, Anchor: "4w", SnapshotDate: "2026-04-29"}, // oldest data point
},
},
{
@@ -239,6 +239,32 @@ func TestGenerateRankHistorySVG_PartialRankHistory(t *testing.T) {
require(strings.Contains(svg, `"name":"single-entry"`), "SVG should include 'single-entry' project")
}
+// TestXLabelStep_ReturnsSensibleSteps verifies the X-axis label density
+// function thins out labels as the number of display columns grows.
+func TestXLabelStep_ReturnsSensibleSteps(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ displayPoints int
+ wantMax int // step must be ≤ wantMax
+ }{
+ {5, 1},
+ {10, 2},
+ {20, 4},
+ {32, 8},
+ }
+
+ for _, tc := range tests {
+ got := xLabelStep(tc.displayPoints)
+ if got > tc.wantMax {
+ t.Errorf("xLabelStep(%d) = %d, want ≤ %d", tc.displayPoints, got, tc.wantMax)
+ }
+ if got <= 0 {
+ t.Errorf("xLabelStep(%d) = %d, want > 0", tc.displayPoints, got)
+ }
+ }
+}
+
// TestGridStep_ReturnsSensibleSteps ensures the Y-axis step function keeps
// the graph legible across various project counts.
func TestGridStep_ReturnsSensibleSteps(t *testing.T) {