diff --git a/internal/server/http/ui_repo.go b/internal/server/http/ui_repo.go
index 4807a63..a630831 100644
--- a/internal/server/http/ui_repo.go
+++ b/internal/server/http/ui_repo.go
@@ -284,7 +284,7 @@ func (s *Server) buildRepoTrendView(ctx context.Context, hostID, rangeKey string
chartSVG := sparkline.RenderChart([]sparkline.Series{
{Name: "size", Stroke: "#3b82f6", Axis: sparkline.AxisLeft, Format: sparkline.FormatBytes, Points: sizes},
{Name: "snapshots", Stroke: "#f59e0b", Axis: sparkline.AxisRight, Format: sparkline.FormatCount, Points: counts},
- }, dayList, sparkline.ChartOpts{Width: 600, Height: 220})
+ }, dayList, sparkline.ChartOpts{Width: 640, Height: 220})
return repoTrendView{HostID: hostID, Range: rangeKey, ChartSVG: chartSVG}
}
diff --git a/internal/web/sparkline/sparkline.go b/internal/web/sparkline/sparkline.go
index e57fb4b..2bbf444 100644
--- a/internal/web/sparkline/sparkline.go
+++ b/internal/web/sparkline/sparkline.go
@@ -156,7 +156,7 @@ func RenderChart(series []Series, days []time.Time, opts ChartOpts) template.HTM
if opts.EmptyLabel == "" {
opts.EmptyLabel = "no data yet"
}
- const padL, padR, padT, padB = 56, 56, 16, 28
+ const padL, padR, padT, padB = 72, 56, 16, 28
w, h := opts.Width, opts.Height
innerW := w - padL - padR
innerH := h - padT - padB
@@ -255,6 +255,11 @@ func RenderChart(series []Series, days []time.Time, opts ChartOpts) template.HTM
continue
}
x := float64(padL) + stepX*float64(i)
+ if len(days) == 1 {
+ // Single-day: pin the lone dot to the chart centre so it
+ // sits under the centred date label.
+ x = float64(padL) + float64(innerW)/2
+ }
y := float64(padT) + float64(innerH) - (v-a.min)/(a.max-a.min)*float64(innerH)
fmt.Fprintf(&seg, "%.2f,%.2f ", x, y)
d := days[i]
@@ -267,18 +272,48 @@ func RenderChart(series []Series, days []time.Time, opts ChartOpts) template.HTM
if axArr[AxisLeft].has {
writeAxisLabels(&b, padL-6, padT, innerH, axArr[AxisLeft].min, axArr[AxisLeft].max, FormatBytes, "end")
+ // Rotated axis title in the left margin. Position inset from
+ // the viewBox edge by ≈ font-size so the rotated glyph extents
+ // don't clip against the SVG boundary.
+ cy := padT + innerH/2
+ fmt.Fprintf(&b,
+ `Size`,
+ cy, cy)
}
if axArr[AxisRight].has {
writeAxisLabels(&b, w-padR+6, padT, innerH, axArr[AxisRight].min, axArr[AxisRight].max, FormatCount, "start")
+ cy := padT + innerH/2
+ fmt.Fprintf(&b,
+ `Snapshots`,
+ w-14, cy, w-14, cy)
}
- xLabels := []int{0, len(days) / 2, len(days) - 1}
- anchors := []string{"start", "middle", "end"}
- for i, idx := range xLabels {
- x := float64(padL) + stepX*float64(idx)
+ // X-axis labels at start / mid / end. With 1-2 days the indices
+ // collapse onto each other — dedupe so we don't stack overlapping
+ // "Jan 2" labels at the same x coordinate.
+ type xLabel struct {
+ idx int
+ anchor string
+ }
+ var xLabels []xLabel
+ switch {
+ case len(days) == 1:
+ xLabels = []xLabel{{0, "middle"}}
+ case len(days) == 2:
+ xLabels = []xLabel{{0, "start"}, {1, "end"}}
+ default:
+ xLabels = []xLabel{{0, "start"}, {len(days) / 2, "middle"}, {len(days) - 1, "end"}}
+ }
+ for _, l := range xLabels {
+ x := float64(padL) + stepX*float64(l.idx)
+ // With a single point, anchor "middle" centres on padL — push to
+ // the chart's centre line so the lone label sits over the dot.
+ if len(days) == 1 {
+ x = float64(padL) + float64(innerW)/2
+ }
fmt.Fprintf(&b,
`%s`,
- x, h-padB+16, anchors[i], days[idx].Format("Jan 2"))
+ x, h-padB+16, l.anchor, days[l.idx].Format("Jan 2"))
}
b.WriteString(``)
diff --git a/internal/web/sparkline/testdata/chart_empty.svg b/internal/web/sparkline/testdata/chart_empty.svg
index 52f8d64..4ffd92b 100644
--- a/internal/web/sparkline/testdata/chart_empty.svg
+++ b/internal/web/sparkline/testdata/chart_empty.svg
@@ -1 +1 @@
-
+
diff --git a/internal/web/sparkline/testdata/chart_two_series.svg b/internal/web/sparkline/testdata/chart_two_series.svg
index f84855c..0f6b564 100644
--- a/internal/web/sparkline/testdata/chart_two_series.svg
+++ b/internal/web/sparkline/testdata/chart_two_series.svg
@@ -1 +1 @@
-
+