summaryrefslogtreecommitdiff
path: root/internal/tui/dashboard/sparkline.go
blob: fad86e23d279595c4024c75724f38fb084daaaae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package dashboard

import "math"
import "strings"

var sparkRowChars = []rune(" ▁▂▃▄▅▆▇█")

func renderSparkline(data []float64, width int) string {
	if len(data) == 0 || width <= 0 {
		return ""
	}

	samples := sampleForWidth(data, width)
	leftPad := 0
	if len(samples) < width {
		leftPad = width - len(samples)
	}
	min, max := minMax(samples)
	if min == max {
		top := repeatRune(' ', width)
		bottom := repeatRune(' ', leftPad) + repeatRune('█', len(samples))
		return top + "\n" + bottom
	}

	top := make([]rune, width)
	bottom := make([]rune, width)
	for i := 0; i < leftPad; i++ {
		top[i] = ' '
		bottom[i] = ' '
	}
	scale := 16.0
	denom := max - min
	for i, value := range samples {
		level := int(math.Round((value - min) / denom * scale))
		if level < 0 {
			level = 0
		}
		if level > 16 {
			level = 16
		}

		topLevel := level - 8
		if topLevel < 0 {
			topLevel = 0
		}
		bottomLevel := level
		if bottomLevel > 8 {
			bottomLevel = 8
		}
		if bottomLevel == 0 {
			bottomLevel = 1
		}

		col := leftPad + i
		top[col] = sparkRowChars[topLevel]
		bottom[col] = sparkRowChars[bottomLevel]
	}
	return string(top) + "\n" + string(bottom)
}

func renderLabeledSparkline(label string, data []float64, width int) string {
	spark := renderSparkline(data, width)
	if spark == "" {
		return label
	}
	lines := strings.Split(spark, "\n")
	if len(lines) == 1 {
		return label + " " + lines[0]
	}
	pad := repeatRune(' ', len([]rune(label))+1)
	return label + " " + lines[0] + "\n" + pad + lines[1]
}

func sampleForWidth(data []float64, width int) []float64 {
	if width >= len(data) {
		return append([]float64(nil), data...)
	}
	if width == 1 {
		return []float64{data[len(data)-1]}
	}

	last := len(data) - 1
	samples := make([]float64, width)
	for i := 0; i < width; i++ {
		idx := int(math.Round(float64(i) * float64(last) / float64(width-1)))
		samples[i] = data[idx]
	}
	return samples
}

func minMax(values []float64) (float64, float64) {
	min := values[0]
	max := values[0]
	for _, v := range values[1:] {
		if v < min {
			min = v
		}
		if v > max {
			max = v
		}
	}
	return min, max
}

func repeatRune(r rune, count int) string {
	if count <= 0 {
		return ""
	}
	out := make([]rune, count)
	for i := range out {
		out[i] = r
	}
	return string(out)
}