2019-01-01 07:01:13 +08:00
|
|
|
package termui
|
|
|
|
|
|
|
|
import (
|
2019-01-01 08:55:50 +08:00
|
|
|
"image"
|
2019-01-01 07:01:13 +08:00
|
|
|
"sort"
|
2021-01-27 01:13:45 +08:00
|
|
|
"strconv"
|
|
|
|
"unicode"
|
2019-01-01 07:01:13 +08:00
|
|
|
|
2019-03-08 15:15:41 +08:00
|
|
|
. "github.com/gizak/termui/v3"
|
2020-04-24 03:07:08 +08:00
|
|
|
drawille "github.com/xxxserxxx/gotop/v4/termui/drawille-go"
|
2019-01-01 07:01:13 +08:00
|
|
|
)
|
|
|
|
|
2020-06-01 04:48:14 +08:00
|
|
|
// LineGraph draws a graph like this ⣀⡠⠤⠔⣁ of data points.
|
2019-01-01 07:01:13 +08:00
|
|
|
type LineGraph struct {
|
|
|
|
*Block
|
2019-03-01 08:29:52 +08:00
|
|
|
|
2020-06-01 04:48:14 +08:00
|
|
|
// 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.
|
2019-03-01 08:29:52 +08:00
|
|
|
Labels map[string]string
|
|
|
|
|
2019-01-13 08:31:37 +08:00
|
|
|
HorizontalScale int
|
2019-01-01 07:01:13 +08:00
|
|
|
|
2019-03-01 08:29:52 +08:00
|
|
|
LineColors map[string]Color
|
2020-07-25 20:21:15 +08:00
|
|
|
LabelStyles map[string]Modifier
|
2019-01-24 13:23:35 +08:00
|
|
|
DefaultLineColor Color
|
2021-01-27 01:13:45 +08:00
|
|
|
|
|
|
|
seriesList numbered
|
2019-01-01 07:01:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewLineGraph() *LineGraph {
|
|
|
|
return &LineGraph{
|
2019-03-01 08:29:52 +08:00
|
|
|
Block: NewBlock(),
|
|
|
|
|
|
|
|
Data: make(map[string][]float64),
|
|
|
|
Labels: make(map[string]string),
|
|
|
|
|
2019-01-13 08:31:37 +08:00
|
|
|
HorizontalScale: 5,
|
2019-03-01 08:29:52 +08:00
|
|
|
|
2020-07-25 20:21:15 +08:00
|
|
|
LineColors: make(map[string]Color),
|
|
|
|
LabelStyles: make(map[string]Modifier),
|
2019-01-01 07:01:13 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-01 08:55:50 +08:00
|
|
|
func (self *LineGraph) Draw(buf *Buffer) {
|
|
|
|
self.Block.Draw(buf)
|
2019-01-01 07:01:13 +08:00
|
|
|
// 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
|
2019-01-24 13:23:35 +08:00
|
|
|
colors := make([][]Color, self.Inner.Dx()+2)
|
2019-01-01 07:01:13 +08:00
|
|
|
for i := range colors {
|
2019-01-24 13:23:35 +08:00
|
|
|
colors[i] = make([]Color, self.Inner.Dy()+2)
|
2019-01-01 07:01:13 +08:00
|
|
|
}
|
|
|
|
|
2021-01-27 01:13:45 +08:00
|
|
|
if len(self.seriesList) != len(self.Data) {
|
|
|
|
// sort the series so that overlapping data will overlap the same way each time
|
|
|
|
self.seriesList = make(numbered, len(self.Data))
|
|
|
|
i := 0
|
|
|
|
for seriesName := range self.Data {
|
|
|
|
self.seriesList[i] = seriesName
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
sort.Sort(self.seriesList)
|
2019-01-01 07:01:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// draw lines in reverse order so that the first color defined in the colorscheme is on top
|
2021-01-27 01:13:45 +08:00
|
|
|
for i := len(self.seriesList) - 1; i >= 0; i-- {
|
|
|
|
seriesName := self.seriesList[i]
|
2019-01-01 07:01:13 +08:00
|
|
|
seriesData := self.Data[seriesName]
|
2019-03-01 08:29:52 +08:00
|
|
|
seriesLineColor, ok := self.LineColors[seriesName]
|
2019-01-01 07:01:13 +08:00
|
|
|
if !ok {
|
|
|
|
seriesLineColor = self.DefaultLineColor
|
2020-04-29 01:04:35 +08:00
|
|
|
self.LineColors[seriesName] = seriesLineColor
|
2019-01-01 07:01:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// coordinates of last point
|
|
|
|
lastY, lastX := -1, -1
|
|
|
|
// assign colors to `colors` and lines/points to the canvas
|
2020-06-01 04:48:14 +08:00
|
|
|
dx := self.Inner.Dx()
|
2019-01-01 07:01:13 +08:00
|
|
|
for i := len(seriesData) - 1; i >= 0; i-- {
|
2020-06-01 04:48:14 +08:00
|
|
|
x := ((dx + 1) * 2) - 1 - (((len(seriesData) - 1) - i) * self.HorizontalScale)
|
2019-01-01 08:55:50 +08:00
|
|
|
y := ((self.Inner.Dy() + 1) * 4) - 1 - int((float64((self.Inner.Dy())*4)-1)*(seriesData[i]/100))
|
2019-01-01 07:01:13 +08:00
|
|
|
if x < 0 {
|
|
|
|
// render the line to the last point up to the wall
|
2020-04-17 07:13:57 +08:00
|
|
|
if x > -self.HorizontalScale {
|
2019-01-01 07:01:13 +08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-06-01 04:48:14 +08:00
|
|
|
if len(seriesData) > 4*dx {
|
|
|
|
self.Data[seriesName] = seriesData[dx-1:]
|
|
|
|
}
|
2019-01-01 07:01:13 +08:00
|
|
|
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
|
2019-01-01 08:55:50 +08:00
|
|
|
buf.SetCell(
|
2019-01-26 12:04:34 +08:00
|
|
|
NewCell(char, NewStyle(colors[x][y])),
|
2019-01-01 08:55:50 +08:00
|
|
|
image.Pt(self.Inner.Min.X+x-1, self.Inner.Min.Y+y-1),
|
|
|
|
)
|
2019-01-01 07:01:13 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// renders key/label ontop
|
2020-04-17 07:13:57 +08:00
|
|
|
maxWid := 0
|
|
|
|
xoff := 0 // X offset for additional columns of text
|
|
|
|
yoff := 0 // Y offset for resetting column to top of widget
|
2021-01-27 01:13:45 +08:00
|
|
|
for i, seriesName := range self.seriesList {
|
2020-04-17 07:13:57 +08:00
|
|
|
if yoff+i+2 > self.Inner.Dy() {
|
|
|
|
xoff += maxWid + 2
|
|
|
|
yoff = -i
|
|
|
|
maxWid = 0
|
2019-01-01 07:01:13 +08:00
|
|
|
}
|
2019-03-01 08:29:52 +08:00
|
|
|
seriesLineColor, ok := self.LineColors[seriesName]
|
2019-01-01 07:01:13 +08:00
|
|
|
if !ok {
|
|
|
|
seriesLineColor = self.DefaultLineColor
|
|
|
|
}
|
2020-07-25 20:21:15 +08:00
|
|
|
seriesLabelStyle, ok := self.LabelStyles[seriesName]
|
|
|
|
if !ok {
|
|
|
|
seriesLabelStyle = ModifierClear
|
|
|
|
}
|
2019-01-01 07:01:13 +08:00
|
|
|
|
|
|
|
// render key ontop, but let braille be drawn over space characters
|
|
|
|
str := seriesName + " " + self.Labels[seriesName]
|
2020-04-17 07:13:57 +08:00
|
|
|
if len(str) > maxWid {
|
|
|
|
maxWid = len(str)
|
|
|
|
}
|
2019-01-01 07:01:13 +08:00
|
|
|
for k, char := range str {
|
|
|
|
if char != ' ' {
|
2019-01-01 08:55:50 +08:00
|
|
|
buf.SetCell(
|
2020-07-25 20:21:15 +08:00
|
|
|
NewCell(char, NewStyle(seriesLineColor, ColorClear, seriesLabelStyle)),
|
2020-04-17 07:13:57 +08:00
|
|
|
image.Pt(xoff+self.Inner.Min.X+2+k, yoff+self.Inner.Min.Y+i+1),
|
2019-01-01 08:55:50 +08:00
|
|
|
)
|
2019-01-01 07:01:13 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
2021-01-27 01:13:45 +08:00
|
|
|
|
|
|
|
// A string containing an integer
|
|
|
|
type numbered []string
|
|
|
|
|
|
|
|
func (n numbered) Len() int { return len(n) }
|
|
|
|
func (n numbered) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
|
|
|
func (n numbered) Less(i, j int) bool {
|
|
|
|
a := n[i]
|
|
|
|
b := n[j]
|
|
|
|
for i := 0; i < len(a); i++ {
|
|
|
|
ac := a[i]
|
|
|
|
if unicode.IsDigit(rune(ac)) {
|
|
|
|
j := i + 1
|
|
|
|
for ; j < len(a); j++ {
|
|
|
|
if !unicode.IsDigit(rune(a[j])) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if j >= len(b) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if !unicode.IsDigit(rune(b[j])) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
an, err := strconv.Atoi(a[i:j])
|
|
|
|
if err != nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if j > len(b) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for ; j < len(b); j++ {
|
|
|
|
if !unicode.IsDigit(rune(b[j])) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
bn, err := strconv.Atoi(b[i:j])
|
|
|
|
if err != nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if an < bn {
|
|
|
|
return true
|
|
|
|
} else if bn < an {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
i = j
|
|
|
|
}
|
|
|
|
if i >= len(a) {
|
|
|
|
return true
|
|
|
|
} else if i >= len(b) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if ac < b[i] {
|
|
|
|
return true
|
|
|
|
} else if b[i] < ac {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|