summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-26 10:11:37 +0200
committerPaul Buetow <paul@buetow.org>2026-02-26 10:11:37 +0200
commit76db79bbd74ebf58ea4403a7e623316c1e4b41de (patch)
tree20fd850131b6958d95a20aea5ae0c7d3ee3bf8de /internal
parent8e7f9c656df5ab155648af316635b90dbd53f8d0 (diff)
tui: stabilize overview sparklines and prevent wrap artifacts
Diffstat (limited to 'internal')
-rw-r--r--internal/tui/dashboard/layout.go3
-rw-r--r--internal/tui/dashboard/overview.go2
-rw-r--r--internal/tui/dashboard/overview_test.go12
-rw-r--r--internal/tui/dashboard/sparkline.go33
-rw-r--r--internal/tui/dashboard/sparkline_test.go21
5 files changed, 47 insertions, 24 deletions
diff --git a/internal/tui/dashboard/layout.go b/internal/tui/dashboard/layout.go
index 38cce18..75cbafb 100644
--- a/internal/tui/dashboard/layout.go
+++ b/internal/tui/dashboard/layout.go
@@ -1,3 +1,6 @@
package dashboard
const panelHorizontalChrome = 4
+
+// Keep a small guard so sparkline rows never soft-wrap in panel cells.
+const sparklineSafetyMargin = 3
diff --git a/internal/tui/dashboard/overview.go b/internal/tui/dashboard/overview.go
index 9a77da0..990e36d 100644
--- a/internal/tui/dashboard/overview.go
+++ b/internal/tui/dashboard/overview.go
@@ -220,7 +220,7 @@ func summaryBoxInnerWidth(width int) int {
}
func renderOverviewSparkline(label string, data []float64, panelInner int) string {
- w := panelInner - utf8.RuneCountInString(label) - 1
+ w := panelInner - utf8.RuneCountInString(label) - 1 - sparklineSafetyMargin
if w < 8 {
w = 8
}
diff --git a/internal/tui/dashboard/overview_test.go b/internal/tui/dashboard/overview_test.go
index 89f00fb..706661e 100644
--- a/internal/tui/dashboard/overview_test.go
+++ b/internal/tui/dashboard/overview_test.go
@@ -117,3 +117,15 @@ func TestRenderOverviewDoesNotOverflowWidth(t *testing.T) {
}
}
}
+
+func TestRenderOverviewSparklineHasSafetyMargin(t *testing.T) {
+ const panelInner = 80
+ out := renderOverviewSparkline("Latency:", []float64{1, 2, 3, 4, 5}, panelInner)
+ lines := strings.Split(out, "\n")
+ if len(lines) != 2 {
+ t.Fatalf("expected 2-line sparkline, got %q", out)
+ }
+ if got, max := lipgloss.Width(lines[0]), panelInner-sparklineSafetyMargin; got > max {
+ t.Fatalf("expected sparkline width <= %d with safety margin, got %d", max, got)
+ }
+}
diff --git a/internal/tui/dashboard/sparkline.go b/internal/tui/dashboard/sparkline.go
index 93a6789..2ce8c90 100644
--- a/internal/tui/dashboard/sparkline.go
+++ b/internal/tui/dashboard/sparkline.go
@@ -11,15 +11,23 @@ func renderSparkline(data []float64, width int) string {
}
samples := sampleForWidth(data, width)
+ leftPad := 0
+ if len(samples) < width {
+ leftPad = width - len(samples)
+ }
min, max := minMax(samples)
if min == max {
top := repeatRune(' ', width)
- bottom := repeatRune('█', width)
+ bottom := repeatRune(' ', leftPad) + repeatRune('█', len(samples))
return top + "\n" + bottom
}
top := make([]rune, width)
bottom := make([]rune, width)
+ for i := 0; i < leftPad; i++ {
+ top[i] = ' '
+ bottom[i] = ' '
+ }
scale := 16.0
denom := max - min
for i, value := range samples {
@@ -40,7 +48,7 @@ func renderSparkline(data []float64, width int) string {
bottomLevel = 8
}
- col := i
+ col := leftPad + i
top[col] = sparkRowChars[topLevel]
bottom[col] = sparkRowChars[bottomLevel]
}
@@ -61,24 +69,11 @@ func renderLabeledSparkline(label string, data []float64, width int) string {
}
func sampleForWidth(data []float64, width int) []float64 {
- if width == 1 {
- return []float64{data[len(data)-1]}
- }
- if len(data) == 1 {
- out := make([]float64, width)
- for i := range out {
- out[i] = data[0]
- }
- return out
- }
-
- last := len(data) - 1
- samples := make([]float64, width)
- for i := 0; i < width; i++ {
- idx := int(math.Round(float64(i) * float64(last) / float64(width-1)))
- samples[i] = data[idx]
+ if width >= len(data) {
+ return append([]float64(nil), data...)
}
- return samples
+ start := len(data) - width
+ return append([]float64(nil), data[start:]...)
}
func minMax(values []float64) (float64, float64) {
diff --git a/internal/tui/dashboard/sparkline_test.go b/internal/tui/dashboard/sparkline_test.go
index 66d1673..d7acd33 100644
--- a/internal/tui/dashboard/sparkline_test.go
+++ b/internal/tui/dashboard/sparkline_test.go
@@ -16,7 +16,7 @@ func TestRenderSparklineEmptyOrInvalidWidth(t *testing.T) {
func TestRenderSparklineSingleValue(t *testing.T) {
got := renderSparkline([]float64{10}, 8)
- if got != " \n████████" {
+ if got != " \n █" {
t.Fatalf("expected two-line constant sparkline, got %q", got)
}
}
@@ -28,14 +28,14 @@ func TestRenderSparklineAllEqualValues(t *testing.T) {
}
}
-func TestRenderSparklineStretchesShortHistoryToWidth(t *testing.T) {
+func TestRenderSparklineRightAlignsShortHistory(t *testing.T) {
got := renderSparkline([]float64{1, 2, 3}, 6)
lines := strings.Split(got, "\n")
if len(lines) != 2 {
t.Fatalf("expected 2 lines, got %q", got)
}
- if strings.HasPrefix(lines[1], " ") {
- t.Fatalf("expected short history to fill width, got %q", lines[1])
+ if !strings.HasPrefix(lines[1], " ") {
+ t.Fatalf("expected left padding for short history, got %q", lines[1])
}
}
@@ -50,6 +50,19 @@ func TestRenderSparklineRespectsWidthTruncation(t *testing.T) {
}
}
+func TestSampleForWidthUsesRecentTail(t *testing.T) {
+ got := sampleForWidth([]float64{1, 2, 3, 4, 5, 6}, 3)
+ want := []float64{4, 5, 6}
+ if len(got) != len(want) {
+ t.Fatalf("expected tail length %d, got %d", len(want), len(got))
+ }
+ for i := range want {
+ if got[i] != want[i] {
+ t.Fatalf("expected tail %v, got %v", want, got)
+ }
+ }
+}
+
func TestRenderSparklineSpansLowToHigh(t *testing.T) {
got := renderSparkline([]float64{0, 10}, 2)
lines := strings.Split(got, "\n")