diff --git a/internal/tui/histogram.go b/internal/tui/histogram.go new file mode 100644 index 0000000..3c547cf --- /dev/null +++ b/internal/tui/histogram.go @@ -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() +} diff --git a/internal/tui/view_detail.go b/internal/tui/view_detail.go index 8f3dc93..80d70c0 100644 --- a/internal/tui/view_detail.go +++ b/internal/tui/view_detail.go @@ -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"))