feat(tui): multi-row color-coded sparkline chart with Y-axis
Replace ntcharts with custom multi-row sparkline. Each bar is color- coded by latency threshold (green < 200ms, yellow < 500ms, red > 500ms) and DOWN checks render in red. 3 rows tall with 24 discrete levels. Y-axis labels (max/min ms) on the left, Min/Avg/Max stats below. Zero external dependencies — removed ntcharts.
This commit was merged in pull request #147.
This commit is contained in:
@@ -3,7 +3,6 @@ module gitea.lerkolabs.com/lerkolabs/uptop
|
|||||||
go 1.26.4
|
go 1.26.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/NimbleMarkets/ntcharts v0.5.1
|
|
||||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/harmonica v0.2.0
|
github.com/charmbracelet/harmonica v0.2.0
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/NimbleMarkets/ntcharts v0.5.1 h1:HWtekubEXfESwi24pyFynwGo2Hulbb9fPh7INMUc1dg=
|
|
||||||
github.com/NimbleMarkets/ntcharts v0.5.1/go.mod h1:zVeRqYkh2n59YPe1bflaSL4O2aD2ZemNmrbdEqZ70hk=
|
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
|||||||
+64
-30
@@ -4,19 +4,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/NimbleMarkets/ntcharts/sparkline"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) latencyChart(latencies []time.Duration, statuses []bool, width, height int) string {
|
func (m Model) latencyChart(latencies []time.Duration, statuses []bool, width, height int) string {
|
||||||
if len(latencies) == 0 || width < 20 || height < 2 {
|
if len(latencies) == 0 || width < 20 || height < 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var minMs, maxMs, sumMs int64
|
var minMs, maxMs, sumMs int64
|
||||||
minMs = latencies[0].Milliseconds()
|
minMs = latencies[0].Milliseconds()
|
||||||
maxMs = minMs
|
maxMs = minMs
|
||||||
|
upCount := 0
|
||||||
for i, l := range latencies {
|
for i, l := range latencies {
|
||||||
ms := l.Milliseconds()
|
ms := l.Milliseconds()
|
||||||
if i < len(statuses) && !statuses[i] {
|
if i < len(statuses) && !statuses[i] {
|
||||||
@@ -29,12 +27,7 @@ func (m Model) latencyChart(latencies []time.Duration, statuses []bool, width, h
|
|||||||
maxMs = ms
|
maxMs = ms
|
||||||
}
|
}
|
||||||
sumMs += ms
|
sumMs += ms
|
||||||
}
|
upCount++
|
||||||
upCount := 0
|
|
||||||
for _, s := range statuses {
|
|
||||||
if s {
|
|
||||||
upCount++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var avgMs int64
|
var avgMs int64
|
||||||
if upCount > 0 {
|
if upCount > 0 {
|
||||||
@@ -57,34 +50,75 @@ func (m Model) latencyChart(latencies []time.Duration, statuses []bool, width, h
|
|||||||
chartW = 10
|
chartW = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
style := lipgloss.NewStyle().Foreground(m.theme.Accent)
|
samples := latencies
|
||||||
sl := sparkline.New(chartW, height, sparkline.WithStyle(style))
|
sampledStatuses := statuses
|
||||||
|
if len(samples) > chartW {
|
||||||
vals := make([]float64, len(latencies))
|
samples = samples[len(samples)-chartW:]
|
||||||
for i, l := range latencies {
|
if len(sampledStatuses) > chartW {
|
||||||
ms := float64(l.Milliseconds())
|
sampledStatuses = sampledStatuses[len(sampledStatuses)-chartW:]
|
||||||
if i < len(statuses) && !statuses[i] {
|
|
||||||
ms = 0
|
|
||||||
}
|
}
|
||||||
vals[i] = ms
|
|
||||||
}
|
}
|
||||||
sl.PushAll(vals)
|
|
||||||
sl.Draw()
|
|
||||||
|
|
||||||
chartLines := strings.Split(sl.View(), "\n")
|
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
|
||||||
|
}
|
||||||
|
|
||||||
var result []string
|
|
||||||
labelStyle := m.st.subtleStyle
|
labelStyle := m.st.subtleStyle
|
||||||
for i, line := range chartLines {
|
|
||||||
|
var rows []string
|
||||||
|
for row := height - 1; row >= 0; row-- {
|
||||||
var label string
|
var label string
|
||||||
if i == 0 {
|
switch row {
|
||||||
|
case height - 1:
|
||||||
label = fmt.Sprintf("%*s", labelW, maxLabel)
|
label = fmt.Sprintf("%*s", labelW, maxLabel)
|
||||||
} else if i == len(chartLines)-1 {
|
case 0:
|
||||||
label = fmt.Sprintf("%*s", labelW, minLabel)
|
label = fmt.Sprintf("%*s", labelW, minLabel)
|
||||||
} else {
|
default:
|
||||||
label = strings.Repeat(" ", labelW)
|
label = strings.Repeat(" ", labelW)
|
||||||
}
|
}
|
||||||
result = append(result, labelStyle.Render(label)+line)
|
|
||||||
|
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",
|
stats := fmt.Sprintf("%*s Min %s Avg %s Max %s",
|
||||||
@@ -92,7 +126,7 @@ func (m Model) latencyChart(latencies []time.Duration, statuses []bool, width, h
|
|||||||
m.fmtLatency(time.Duration(minMs)*time.Millisecond),
|
m.fmtLatency(time.Duration(minMs)*time.Millisecond),
|
||||||
m.fmtLatency(time.Duration(avgMs)*time.Millisecond),
|
m.fmtLatency(time.Duration(avgMs)*time.Millisecond),
|
||||||
m.fmtLatency(time.Duration(maxMs)*time.Millisecond))
|
m.fmtLatency(time.Duration(maxMs)*time.Millisecond))
|
||||||
result = append(result, stats)
|
rows = append(rows, stats)
|
||||||
|
|
||||||
return strings.Join(result, "\n")
|
return strings.Join(rows, "\n")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailInlineHeight = 10
|
const detailInlineHeight = 11
|
||||||
|
|
||||||
func (m *Model) recalcLayout() {
|
func (m *Model) recalcLayout() {
|
||||||
chrome := chromeBase
|
chrome := chromeBase
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func (m Model) viewDetailInline(width int) string {
|
|||||||
if chartW < 20 {
|
if chartW < 20 {
|
||||||
chartW = 20
|
chartW = 20
|
||||||
}
|
}
|
||||||
chart := m.latencyChart(hist.Latencies, hist.Statuses, chartW, 2)
|
chart := m.latencyChart(hist.Latencies, hist.Statuses, chartW, 3)
|
||||||
if chart != "" {
|
if chart != "" {
|
||||||
b.WriteString(chart + "\n")
|
b.WriteString(chart + "\n")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user