diff options
Diffstat (limited to 'internal/showcase')
| -rw-r--r-- | internal/showcase/rank_history.go | 2 | ||||
| -rw-r--r-- | internal/showcase/rank_history_svg.go | 63 | ||||
| -rw-r--r-- | internal/showcase/rank_history_svg_test.go | 28 |
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) { |
