feat(tui): ntcharts sparkline with Y-axis labels and stats

Replaced streamline chart with ntcharts sparkline (block elements,
auto-scaling). Height=2, Y-axis labels (max/min ms) on the left,
Min/Avg/Max stats below. Denser and more readable than the line chart.
This commit is contained in:
2026-06-21 17:12:25 -04:00
parent d0805f61c6
commit a8c43bdb8e
3 changed files with 77 additions and 36 deletions
+75 -13
View File
@@ -1,36 +1,98 @@
package tui package tui
import ( import (
"fmt"
"strings"
"time" "time"
"github.com/NimbleMarkets/ntcharts/canvas/runes" "github.com/NimbleMarkets/ntcharts/sparkline"
"github.com/NimbleMarkets/ntcharts/linechart/streamlinechart"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
func (m Model) latencyChart(latencies []time.Duration, statuses []bool, width, height int) string { func (m Model) latencyChart(latencies []time.Duration, statuses []bool, width, height int) string {
if len(latencies) == 0 || width < 10 || height < 3 { if len(latencies) == 0 || width < 20 || height < 2 {
return "" return ""
} }
chartW := len(latencies) var minMs, maxMs, sumMs int64
if chartW > width { minMs = latencies[0].Milliseconds()
chartW = width maxMs = minMs
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 := 0
for _, s := range statuses {
if s {
upCount++
}
}
var avgMs int64
if upCount > 0 {
avgMs = sumMs / int64(upCount)
} }
lineStyle := lipgloss.NewStyle().Foreground(m.theme.Accent) maxLabel := fmt.Sprintf("%dms", maxMs)
slc := streamlinechart.New(chartW, height, minLabel := fmt.Sprintf("%dms", minMs)
streamlinechart.WithStyles(runes.ThinLineStyle, lineStyle), 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
}
style := lipgloss.NewStyle().Foreground(m.theme.Accent)
sl := sparkline.New(chartW, height, sparkline.WithStyle(style))
vals := make([]float64, len(latencies))
for i, l := range latencies { for i, l := range latencies {
ms := float64(l.Milliseconds()) ms := float64(l.Milliseconds())
if i < len(statuses) && !statuses[i] { if i < len(statuses) && !statuses[i] {
ms = 0 ms = 0
} }
slc.Push(ms) vals[i] = ms
} }
slc.Draw() sl.PushAll(vals)
sl.Draw()
return slc.View() chartLines := strings.Split(sl.View(), "\n")
var result []string
labelStyle := m.st.subtleStyle
for i, line := range chartLines {
var label string
if i == 0 {
label = fmt.Sprintf("%*s", labelW, maxLabel)
} else if i == len(chartLines)-1 {
label = fmt.Sprintf("%*s", labelW, minLabel)
} else {
label = strings.Repeat(" ", labelW)
}
result = append(result, labelStyle.Render(label)+line)
}
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))
result = append(result, stats)
return strings.Join(result, "\n")
} }
+1 -1
View File
@@ -142,7 +142,7 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
const detailInlineHeight = 12 const detailInlineHeight = 10
func (m *Model) recalcLayout() { func (m *Model) recalcLayout() {
chrome := chromeBase chrome := chromeBase
+1 -22
View File
@@ -71,31 +71,10 @@ func (m Model) viewDetailInline(width int) string {
if chartW < 20 { if chartW < 20 {
chartW = 20 chartW = 20
} }
chartH := 4 chart := m.latencyChart(hist.Latencies, hist.Statuses, chartW, 2)
chart := m.latencyChart(hist.Latencies, hist.Statuses, chartW, chartH)
if chart != "" { if chart != "" {
b.WriteString(chart + "\n") b.WriteString(chart + "\n")
} }
minMs := hist.Latencies[0].Milliseconds()
maxMs := hist.Latencies[0].Milliseconds()
var sumMs int64
for _, l := range hist.Latencies {
ms := l.Milliseconds()
if ms < minMs {
minMs = ms
}
if ms > maxMs {
maxMs = ms
}
sumMs += ms
}
avgMs := sumMs / int64(len(hist.Latencies))
stats := fmt.Sprintf(" Min %s Avg %s Max %s",
m.fmtLatency(time.Duration(minMs)*time.Millisecond),
m.fmtLatency(time.Duration(avgMs)*time.Millisecond),
m.fmtLatency(time.Duration(maxMs)*time.Millisecond))
b.WriteString(stats + "\n")
} }
keys := m.st.subtleStyle.Render("[h] History [s] SLA [e] Edit [esc] Close") keys := m.st.subtleStyle.Render("[h] History [s] SLA [e] Edit [esc] Close")