Files
uptop/internal/tui/chart.go
T
lerko 4af800a359
CI / test (pull_request) Successful in 1m48s
CI / lint (pull_request) Successful in 1m17s
CI / vulncheck (pull_request) Successful in 56s
feat(tui): multi-row color-coded sparkline chart with Y-axis
Replace ntcharts with custom multi-row sparkline. Each bar is color-
coded by latency threshold (green < 200ms, yellow < 500ms, red > 500ms)
and DOWN checks render in red. 3 rows tall with 24 discrete levels.

Y-axis labels (max/min ms) on the left, Min/Avg/Max stats below.
Zero external dependencies — removed ntcharts.
2026-06-21 17:43:50 -04:00

133 lines
2.7 KiB
Go

package tui
import (
"fmt"
"strings"
"time"
)
func (m Model) latencyChart(latencies []time.Duration, statuses []bool, width, height int) string {
if len(latencies) == 0 || width < 20 || height < 1 {
return ""
}
var minMs, maxMs, sumMs int64
minMs = latencies[0].Milliseconds()
maxMs = minMs
upCount := 0
for i, l := range latencies {
ms := l.Milliseconds()
if i < len(statuses) && !statuses[i] {
continue
}
if ms < minMs {
minMs = ms
}
if ms > maxMs {
maxMs = ms
}
sumMs += ms
upCount++
}
var avgMs int64
if upCount > 0 {
avgMs = sumMs / int64(upCount)
}
maxLabel := fmt.Sprintf("%dms", maxMs)
minLabel := fmt.Sprintf("%dms", minMs)
labelW := len(maxLabel)
if len(minLabel) > labelW {
labelW = len(minLabel)
}
labelW += 1
chartW := width - labelW
if chartW > len(latencies) {
chartW = len(latencies)
}
if chartW < 10 {
chartW = 10
}
samples := latencies
sampledStatuses := statuses
if len(samples) > chartW {
samples = samples[len(samples)-chartW:]
if len(sampledStatuses) > chartW {
sampledStatuses = sampledStatuses[len(sampledStatuses)-chartW:]
}
}
spread := maxMs - minMs
if spread == 0 {
spread = 1
}
totalLevels := height * 8
levels := make([]int, len(samples))
for i, l := range samples {
ms := l.Milliseconds()
if i < len(sampledStatuses) && !sampledStatuses[i] {
levels[i] = 0
continue
}
lvl := int(float64(ms-minMs) / float64(spread) * float64(totalLevels-1))
if lvl < 1 && ms > 0 {
lvl = 1
}
if lvl >= totalLevels {
lvl = totalLevels - 1
}
levels[i] = lvl
}
labelStyle := m.st.subtleStyle
var rows []string
for row := height - 1; row >= 0; row-- {
var label string
switch row {
case height - 1:
label = fmt.Sprintf("%*s", labelW, maxLabel)
case 0:
label = fmt.Sprintf("%*s", labelW, minLabel)
default:
label = strings.Repeat(" ", labelW)
}
var sb strings.Builder
sb.WriteString(labelStyle.Render(label))
rowBase := row * 8
for i := range samples {
fill := levels[i] - rowBase
if fill <= 0 {
sb.WriteString(" ")
continue
}
if fill > 8 {
fill = 8
}
ch := string(sparkChars[fill-1])
isDown := i < len(sampledStatuses) && !sampledStatuses[i]
if isDown {
sb.WriteString(m.st.dangerStyle.Render(ch))
} else {
sb.WriteString(m.latencyStyle(samples[i].Milliseconds(), nil).Render(ch))
}
}
rows = append(rows, sb.String())
}
stats := fmt.Sprintf("%*s Min %s Avg %s Max %s",
labelW, "",
m.fmtLatency(time.Duration(minMs)*time.Millisecond),
m.fmtLatency(time.Duration(avgMs)*time.Millisecond),
m.fmtLatency(time.Duration(maxMs)*time.Millisecond))
rows = append(rows, stats)
return strings.Join(rows, "\n")
}