summaryrefslogtreecommitdiffstats
path: root/termui/linegraph.go
blob: fa8a3ae8b5abfa049e8affd64a7f093129fee1ca (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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package termui

import (
	"image"
	"sort"

	. "github.com/gizak/termui/v3"
	drawille "github.com/xxxserxxx/gotop/v4/termui/drawille-go"
)

// LineGraph draws a graph like this ⣀⡠⠤⠔⣁ of data points.
type LineGraph struct {
	*Block

	// Data is a size-managed data set for the graph. Each entry is a line;
	// each sub-array are points in the line. The maximum size of the
	// sub-arrays is controlled by the size of the canvas. This
	// array is **not** thread-safe. Do not modify this array, or it's
	// sub-arrays in threads different than the thread that calls `Draw()`
	Data map[string][]float64
	// The labels drawn on the graph for each of the lines; the key is shared
	// by Data; the value is the text that will be rendered.
	Labels map[string]string

	HorizontalScale int

	LineColors       map[string]Color
	LabelStyles      map[string]Modifier
	DefaultLineColor Color
}

func NewLineGraph() *LineGraph {
	return &LineGraph{
		Block: NewBlock(),

		Data:   make(map[string][]float64),
		Labels: make(map[string]string),

		HorizontalScale: 5,

		LineColors:  make(map[string]Color),
		LabelStyles: make(map[string]Modifier),
	}
}

func (self *LineGraph) Draw(buf *Buffer) {
	self.Block.Draw(buf)
	// we render each data point on to the canvas then copy over the braille to the buffer at the end
	// fyi braille characters have 2x4 dots for each character
	c := drawille.NewCanvas()
	// used to keep track of the braille colors until the end when we render the braille to the buffer
	colors := make([][]Color, self.Inner.Dx()+2)
	for i := range colors {
		colors[i] = make([]Color, self.Inner.Dy()+2)
	}

	// sort the series so that overlapping data will overlap the same way each time
	seriesList := make([]string, len(self.Data))
	i := 0
	for seriesName := range self.Data {
		seriesList[i] = seriesName
		i++
	}
	sort.Strings(seriesList)

	// draw lines in reverse order so that the first color defined in the colorscheme is on top
	for i := len(seriesList) - 1; i >= 0; i-- {
		seriesName := seriesList[i]
		seriesData := self.Data[seriesName]
		seriesLineColor, ok := self.LineColors[seriesName]
		if !ok {
			seriesLineColor = self.DefaultLineColor
			self.LineColors[seriesName] = seriesLineColor
		}

		// coordinates of last point
		lastY, lastX := -1, -1
		// assign colors to `colors` and lines/points to the canvas
		dx := self.Inner.Dx()
		for i := len(seriesData) - 1; i >= 0; i-- {
			x := ((dx + 1) * 2) - 1 - (((len(seriesData) - 1) - i) * self.HorizontalScale)
			y := ((self.Inner.Dy() + 1) * 4) - 1 - int((float64((self.Inner.Dy())*4)-1)*(seriesData[i]/100))
			if x < 0 {
				// render the line to the last point up to the wall
				if x > -self.HorizontalScale {
					for _, p := range drawille.Line(lastX, lastY, x, y) {
						if p.X > 0 {
							c.Set(p.X, p.Y)
							colors[p.X/2][p.Y/4] = seriesLineColor
						}
					}
				}
				if len(seriesData) > 4*dx {
					self.Data[seriesName] = seriesData[dx-1:]
				}
				break
			}
			if lastY == -1 { // if this is the first point
				c.Set(x, y)
				colors[x/2][y/4] = seriesLineColor
			} else {
				c.DrawLine(lastX, lastY, x, y)
				for _, p := range drawille.Line(lastX, lastY, x, y) {
					colors[p.X/2][p.Y/4] = seriesLineColor
				}
			}
			lastX, lastY = x, y
		}

		// copy braille and colors to buffer
		for y, line := range c.Rows(c.MinX(), c.MinY(), c.MaxX(), c.MaxY()) {
			for x, char := range line {
				x /= 3 // idk why but it works
				if x == 0 {
					continue
				}
				if char != 10240 { // empty braille character
					buf.SetCell(
						NewCell(char, NewStyle(colors[x][y])),
						image.Pt(self.Inner.Min.X+x-1, self.Inner.Min.Y+y-1),
					)
				}
			}
		}
	}

	// renders key/label ontop
	maxWid := 0
	xoff := 0 // X offset for additional columns of text
	yoff := 0 // Y offset for resetting column to top of widget
	for i, seriesName := range seriesList {
		if yoff+i+2 > self.Inner.Dy() {
			xoff += maxWid + 2
			yoff = -i
			maxWid = 0
		}
		seriesLineColor, ok := self.LineColors[seriesName]
		if !ok {
			seriesLineColor = self.DefaultLineColor
		}
		seriesLabelStyle, ok := self.LabelStyles[seriesName]
		if !ok {
			seriesLabelStyle = ModifierClear
		}

		// render key ontop, but let braille be drawn over space characters
		str := seriesName + " " + self.Labels[seriesName]
		if len(str) > maxWid {
			maxWid = len(str)
		}
		for k, char := range str {
			if char != ' ' {
				buf.SetCell(
					NewCell(char, NewStyle(seriesLineColor, ColorClear, seriesLabelStyle)),
					image.Pt(xoff+self.Inner.Min.X+2+k, yoff+self.Inner.Min.Y+i+1),
				)
			}
		}

	}
}