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:
+75
-13
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user