feat(tui): multi-row color-coded latency chart in detail panel #147
@@ -0,0 +1,132 @@
|
||||
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")
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
const detailInlineHeight = 8
|
||||
const detailInlineHeight = 11
|
||||
|
||||
func (m *Model) recalcLayout() {
|
||||
chrome := chromeBase
|
||||
|
||||
@@ -67,33 +67,14 @@ func (m Model) viewDetailInline(width int) string {
|
||||
}
|
||||
|
||||
if len(hist.Latencies) > 0 {
|
||||
sparkW := width - 30
|
||||
if sparkW < 10 {
|
||||
sparkW = 10
|
||||
chartW := width - 4
|
||||
if chartW < 20 {
|
||||
chartW = 20
|
||||
}
|
||||
if sparkW > detailSparkWidth {
|
||||
sparkW = detailSparkWidth
|
||||
chart := m.latencyChart(hist.Latencies, hist.Statuses, chartW, 3)
|
||||
if chart != "" {
|
||||
b.WriteString(chart + "\n")
|
||||
}
|
||||
spark := m.latencySparkline(hist.Latencies, hist.Statuses, sparkW, m.theme.Bg)
|
||||
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(" " + spark + " " + stats + "\n")
|
||||
}
|
||||
|
||||
keys := m.st.subtleStyle.Render("[h] History [s] SLA [e] Edit [esc] Close")
|
||||
|
||||
Reference in New Issue
Block a user