feat(tui): response time distribution histogram in full-screen detail
Horizontal bar chart showing latency distribution across 5 buckets: 0-50ms, 50-100ms, 100-200ms, 200-500ms, 500ms+. Color-coded green (fast), yellow (medium), red (slow). Shows count per bucket. Appears in the full-screen detail panel (Enter) after the sparkline stats. Skipped for push monitors (no latency data).
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type latencyBucket struct {
|
||||
label string
|
||||
min int64
|
||||
max int64
|
||||
count int
|
||||
}
|
||||
|
||||
func (m Model) latencyHistogram(latencies []time.Duration, statuses []bool, width int) string {
|
||||
if len(latencies) == 0 || width < 30 {
|
||||
return ""
|
||||
}
|
||||
|
||||
buckets := []latencyBucket{
|
||||
{"0-50ms", 0, 50, 0},
|
||||
{"50-100ms", 50, 100, 0},
|
||||
{"100-200ms", 100, 200, 0},
|
||||
{"200-500ms", 200, 500, 0},
|
||||
{"500ms+", 500, 999999, 0},
|
||||
}
|
||||
|
||||
for i, l := range latencies {
|
||||
if i < len(statuses) && !statuses[i] {
|
||||
continue
|
||||
}
|
||||
ms := l.Milliseconds()
|
||||
for j := range buckets {
|
||||
if ms >= buckets[j].min && ms < buckets[j].max {
|
||||
buckets[j].count++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxCount := 0
|
||||
for _, b := range buckets {
|
||||
if b.count > maxCount {
|
||||
maxCount = b.count
|
||||
}
|
||||
}
|
||||
if maxCount == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
labelW := 10
|
||||
countW := len(fmt.Sprintf("%d", maxCount))
|
||||
barW := width - labelW - countW - 4
|
||||
if barW < 5 {
|
||||
barW = 5
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for _, b := range buckets {
|
||||
fill := 0
|
||||
if maxCount > 0 {
|
||||
fill = b.count * barW / maxCount
|
||||
}
|
||||
|
||||
var barColor lipgloss.Style
|
||||
switch {
|
||||
case b.min < 100:
|
||||
barColor = m.st.specialStyle
|
||||
case b.min < 500:
|
||||
barColor = m.st.warnStyle
|
||||
default:
|
||||
barColor = m.st.dangerStyle
|
||||
}
|
||||
|
||||
bar := barColor.Render(strings.Repeat("█", fill))
|
||||
if fill < barW {
|
||||
bar += m.st.subtleStyle.Render(strings.Repeat("░", barW-fill))
|
||||
}
|
||||
|
||||
label := fmt.Sprintf("%*s", labelW, b.label)
|
||||
count := fmt.Sprintf("%*d", countW, b.count)
|
||||
sb.WriteString(" " + m.st.subtleStyle.Render(label) + " " + bar + " " + count + "\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -258,6 +258,15 @@ func (m Model) viewDetailPanel() string {
|
||||
b.WriteString("\n" + m.renderSparkTooltip(site, hist, detailSparkWidth))
|
||||
}
|
||||
|
||||
if site.Type != "push" && len(hist.Latencies) > 5 {
|
||||
histW := m.termWidth - chromePadH - 4
|
||||
if histW < 30 {
|
||||
histW = 30
|
||||
}
|
||||
b.WriteString("\n\n" + m.st.subtleStyle.Render(" RESPONSE TIME DISTRIBUTION") + "\n")
|
||||
b.WriteString(m.latencyHistogram(hist.Latencies, hist.Statuses, histW))
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
b.WriteString(m.st.subtleStyle.Render(" [q/Esc] Back [e] Edit [h] History [s] SLA [click] Inspect"))
|
||||
|
||||
Reference in New Issue
Block a user