diff --git a/internal/tui/chart.go b/internal/tui/chart.go new file mode 100644 index 0000000..4ba4f10 --- /dev/null +++ b/internal/tui/chart.go @@ -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") +} diff --git a/internal/tui/update.go b/internal/tui/update.go index 1004871..0e7c1b0 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -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 diff --git a/internal/tui/view_detail_inline.go b/internal/tui/view_detail_inline.go index da962c5..4c39db6 100644 --- a/internal/tui/view_detail_inline.go +++ b/internal/tui/view_detail_inline.go @@ -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")