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))
|
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("\n")
|
||||||
b.WriteString(m.divider() + "\n")
|
b.WriteString(m.divider() + "\n")
|
||||||
b.WriteString(m.st.subtleStyle.Render(" [q/Esc] Back [e] Edit [h] History [s] SLA [click] Inspect"))
|
b.WriteString(m.st.subtleStyle.Render(" [q/Esc] Back [e] Edit [h] History [s] SLA [click] Inspect"))
|
||||||
|
|||||||
Reference in New Issue
Block a user