185 lines
3.4 KiB
Go
185 lines
3.4 KiB
Go
package tchart
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"math"
|
|
|
|
"github.com/derailed/tview"
|
|
"github.com/gdamore/tcell"
|
|
)
|
|
|
|
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
|
|
|
type block struct {
|
|
full int
|
|
partial rune
|
|
}
|
|
|
|
type blocks struct {
|
|
oks, errs block
|
|
}
|
|
|
|
// Metric tracks a good and error rates.
|
|
type Metric struct {
|
|
OK, Fault int
|
|
}
|
|
|
|
// MaxDigits returns the max series number of digits.
|
|
func (m Metric) MaxDigits() int {
|
|
|
|
s := fmt.Sprintf("%d", m.Max())
|
|
return len(s)
|
|
}
|
|
|
|
// Max returns the max of the series.
|
|
func (m Metric) Max() int {
|
|
return int(math.Max(float64(m.OK), float64(m.Fault)))
|
|
}
|
|
|
|
// Sum returns the sum of the metrics.
|
|
func (m Metric) Sum() int {
|
|
return m.OK + m.Fault
|
|
}
|
|
|
|
// SparkLine represents a sparkline component.
|
|
type SparkLine struct {
|
|
*Component
|
|
|
|
data []Metric
|
|
multiSeries bool
|
|
}
|
|
|
|
// NewSparkLine returns a new graph.
|
|
func NewSparkLine(id string) *SparkLine {
|
|
return &SparkLine{
|
|
Component: NewComponent(id),
|
|
multiSeries: true,
|
|
}
|
|
}
|
|
|
|
// SetSingleSeries indicates multi series are in effect or not.
|
|
func (s *SparkLine) SetMultiSeries(b bool) {
|
|
s.multiSeries = b
|
|
}
|
|
|
|
// Add adds a metric.
|
|
func (s *SparkLine) Add(m Metric) {
|
|
s.mx.Lock()
|
|
defer s.mx.Unlock()
|
|
s.data = append(s.data, m)
|
|
}
|
|
|
|
// Draw draws the graph.
|
|
func (s *SparkLine) Draw(screen tcell.Screen) {
|
|
s.Component.Draw(screen)
|
|
|
|
s.mx.RLock()
|
|
defer s.mx.RUnlock()
|
|
|
|
if len(s.data) == 0 {
|
|
return
|
|
}
|
|
|
|
pad := 1
|
|
if s.legend != "" {
|
|
pad++
|
|
}
|
|
|
|
rect := s.asRect()
|
|
s.cutSet(rect.Dx())
|
|
max := s.computeMax()
|
|
|
|
cX, idx := rect.Min.X+1, 0
|
|
if len(s.data)*2 < rect.Dx() {
|
|
cX = rect.Max.X - len(s.data)*2
|
|
} else {
|
|
idx = len(s.data) - rect.Dx()/2
|
|
}
|
|
|
|
factor := 2
|
|
if !s.multiSeries {
|
|
factor = 1
|
|
}
|
|
scale := float64(len(sparks)*(rect.Dy()-pad)/factor) / float64(max)
|
|
c1, c2 := s.colorForSeries()
|
|
for _, d := range s.data[idx:] {
|
|
b := toBlocks(d, scale)
|
|
cY := rect.Max.Y - pad
|
|
cY = s.drawBlock(rect, screen, cX, cY, b.oks, c1)
|
|
_ = s.drawBlock(rect, screen, cX, cY, b.errs, c2)
|
|
cX += 2
|
|
}
|
|
|
|
if rect.Dx() > 0 && rect.Dy() > 0 && s.legend != "" {
|
|
legend := s.legend
|
|
if s.HasFocus() {
|
|
legend = "[:aqua:]" + s.legend + "[::]"
|
|
}
|
|
tview.Print(screen, legend, rect.Min.X, rect.Max.Y, rect.Dx(), tview.AlignCenter, tcell.ColorWhite)
|
|
}
|
|
}
|
|
|
|
func (s *SparkLine) drawBlock(r image.Rectangle, screen tcell.Screen, x, y int, b block, c tcell.Color) int {
|
|
style := tcell.StyleDefault.Foreground(c).Background(s.bgColor)
|
|
|
|
zeroY := r.Max.Y - r.Dy()
|
|
for i := 0; i < b.full; i++ {
|
|
screen.SetContent(x, y, sparks[len(sparks)-1], nil, style)
|
|
y--
|
|
if y <= zeroY {
|
|
break
|
|
}
|
|
}
|
|
if b.partial != 0 {
|
|
screen.SetContent(x, y, b.partial, nil, style)
|
|
if b.full == 0 {
|
|
y--
|
|
}
|
|
}
|
|
|
|
return y
|
|
}
|
|
|
|
func (s *SparkLine) cutSet(width int) {
|
|
if width <= 0 || len(s.data) == 0 {
|
|
return
|
|
}
|
|
|
|
if len(s.data) >= width*2 {
|
|
s.data = s.data[len(s.data)-width:]
|
|
}
|
|
}
|
|
|
|
func (s *SparkLine) computeMax() int {
|
|
var max int
|
|
for _, d := range s.data {
|
|
m := d.Max()
|
|
if max < m {
|
|
max = m
|
|
}
|
|
}
|
|
|
|
return max
|
|
}
|
|
|
|
func toBlocks(m Metric, scale float64) blocks {
|
|
if m.Sum() <= 0 {
|
|
return blocks{}
|
|
}
|
|
return blocks{oks: makeBlocks(m.OK, scale), errs: makeBlocks(m.Fault, scale)}
|
|
}
|
|
|
|
func makeBlocks(v int, scale float64) block {
|
|
scaled := int(math.Round(float64(v) * scale))
|
|
p, b := scaled%len(sparks), block{full: scaled / len(sparks)}
|
|
if b.full == 0 && v > 0 && p == 0 {
|
|
p = 4
|
|
}
|
|
if v > 0 && p >= 0 && p < len(sparks) {
|
|
b.partial = sparks[p]
|
|
}
|
|
|
|
return b
|
|
}
|