ui: chart polish — rotated y-axis labels, wider viewBox, single-day fallback

- Add rotated 'Size' (left) and 'Snapshots' (right) axis titles in
  the chart's outer margins so the two y-axes are self-describing.
- Bump the chart viewBox from 600x220 to 640x220 and lift padL from
  56 to 72 so the rotated labels and byte tick numbers don't crowd.
- Dedupe the X-axis labels for short windows (1 or 2 days collapsed
  the start/mid/end indices onto each other, stacking 'May 7' three
  times); the 1-day case now centres a single label, 2-day uses
  start+end only.
- Pin a lone data dot to the chart centre instead of the left edge
  when len(days)==1, so it sits under the centred date label.

Goldens regenerated.
This commit is contained in:
2026-05-07 22:55:12 +01:00
parent 42eeabea9a
commit 7011510092
4 changed files with 44 additions and 9 deletions
+41 -6
View File
@@ -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,
`<text x="14" y="%d" text-anchor="middle" font-size="11" fill="currentColor" fill-opacity="0.55" transform="rotate(-90, 14, %d)">Size</text>`,
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,
`<text x="%d" y="%d" text-anchor="middle" font-size="11" fill="currentColor" fill-opacity="0.55" transform="rotate(90, %d, %d)">Snapshots</text>`,
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,
`<text x="%.2f" y="%d" text-anchor="%s" font-size="10" fill="currentColor" fill-opacity="0.55">%s</text>`,
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(`</svg>`)