clui/sparkchart.go

282 lines
6.2 KiB
Go

package clui
import (
"fmt"
// xs "github.com/huandu/xstrings"
term "github.com/nsf/termbox-go"
)
/*
SparkChart is a chart that represents a live data that
is continuously added to the chart. Or it can be static
element that displays predefined set of data - in this
case it looks like BarChart. At a moment SparkChart
keeps only th enumber of last data that is enough to
fill the control area. So, if you enlarge the control,
it will show partially filled area until it gets new data.
SparkChart displays vertical axis with values on the chart left
if ValueWidth greater than 0, horizontal axis with bar titles.
Maximum peaks(maximum of the the data that control keeps)
can be hilited with different color.
By default the data is autoscaled to make the highest bar
fit the full height of the control. But it maybe useful
to disable autoscale and set the Top value to have more
handy diagram. E.g, for CPU load in % you can set
AutoScale to false and Top value to 100.
Note: negative and zero values are displayed as empty bar
*/
type SparkChart struct {
ControlBase
data []float64
valueWidth int
hiliteMax bool
maxFg, maxBg term.Attribute
topValue float64
autosize bool
}
/*
NewSparkChart creates a new spark chart.
view - is a View that manages the control
parent - is container that keeps the control. The same View can be a view and a parent at the same time.
w and h - are minimal size of the control.
scale - the way of scaling the control when the parent is resized. Use DoNotScale constant if the
control should keep its original size.
*/
func NewSparkChart(view View, parent Control, w, h int, scale int) *SparkChart {
c := new(SparkChart)
if w == AutoSize {
w = 10
}
if h == AutoSize {
h = 5
}
c.view = view
c.parent = parent
c.SetSize(w, h)
c.SetConstraints(w, h)
c.tabSkip = true
c.hiliteMax = true
c.autosize = true
c.data = make([]float64, 0)
if parent != nil {
parent.AddChild(c, scale)
}
return c
}
// Repaint draws the control on its View surface
func (b *SparkChart) Repaint() {
canvas := b.view.Canvas()
tm := b.view.Screen().Theme()
fg, bg := RealColor(tm, b.fg, ColorSparkChartText), RealColor(tm, b.bg, ColorSparkChartBack)
canvas.FillRect(b.x, b.y, b.width, b.height, term.Cell{Ch: ' ', Fg: fg, Bg: bg})
if len(b.data) == 0 {
return
}
b.drawValues(fg, bg)
b.drawBars(tm)
}
func (b *SparkChart) drawBars(tm Theme) {
if len(b.data) == 0 {
return
}
start, width := b.calculateBarArea()
if width < 2 {
return
}
coeff, max := b.calculateMultiplier()
if coeff == 0.0 {
return
}
h := b.height
pos := b.x + start
canvas := b.view.Canvas()
mxFg, mxBg := RealColor(tm, b.maxFg, ColorSparkChartMaxText), RealColor(tm, b.maxBg, ColorSparkChartMaxBack)
brFg, brBg := RealColor(tm, b.fg, ColorSparkChartBarText), RealColor(tm, b.bg, ColorSparkChartBarBack)
parts := []rune(tm.SysObject(ObjSparkChart))
var dt []float64
if len(b.data) > width {
dt = b.data[len(b.data)-width:]
} else {
dt = b.data
}
for _, d := range dt {
barH := int(d * coeff)
if barH <= 0 {
pos++
continue
}
f, g := brFg, brBg
if b.hiliteMax && max == d {
f, g = mxFg, mxBg
}
cell := term.Cell{Ch: parts[0], Fg: f, Bg: g}
canvas.FillRect(pos, b.y+h-barH, 1, barH, cell)
pos++
}
}
func (b *SparkChart) drawValues(fg, bg term.Attribute) {
if b.valueWidth <= 0 {
return
}
pos, _ := b.calculateBarArea()
if pos == 0 {
return
}
h := b.height
coeff, max := b.calculateMultiplier()
if max == coeff {
return
}
if !b.autosize || b.topValue == 0 {
max = b.topValue
}
canvas := b.view.Canvas()
dy := 0
format := fmt.Sprintf("%%%v.2f", b.valueWidth)
for dy < h-1 {
v := float64(h-dy) / float64(h) * max
s := fmt.Sprintf(format, v)
s = CutText(s, b.valueWidth)
canvas.PutText(b.x, b.y+dy, s, fg, bg)
dy += 2
}
}
func (b *SparkChart) calculateBarArea() (int, int) {
w := b.width
pos := 0
if b.valueWidth < w/2 {
w = w - b.valueWidth
pos = b.valueWidth
}
return pos, w
}
func (b *SparkChart) calculateMultiplier() (float64, float64) {
if len(b.data) == 0 {
return 0, 0
}
h := b.height
if h <= 1 {
return 0, 0
}
max := b.data[0]
for _, val := range b.data {
if val > max {
max = val
}
}
if max == 0 {
return 0, 0
}
if b.autosize || b.topValue == 0 {
return float64(h) / max, max
}
return float64(h) / b.topValue, max
}
// AddData appends a new bar to a chart
func (b *SparkChart) AddData(val float64) {
b.data = append(b.data, val)
_, width := b.calculateBarArea()
if len(b.data) > width {
b.data = b.data[len(b.data)-width:]
}
b.Logger().Printf("%v - %v = %v", b.width, width, len(b.data))
}
// ClearData removes all bar from chart
func (b *SparkChart) ClearData() {
b.data = make([]float64, 0)
}
// SetData assign a new bar list to a chart
func (b *SparkChart) SetData(data []float64) {
b.data = make([]float64, len(data))
copy(b.data, data)
_, width := b.calculateBarArea()
if len(b.data) > width {
b.data = b.data[len(b.data)-width:]
}
}
// ValueWidth returns the width of the area at the left of
// chart used to draw values. Set it to 0 to turn off the
// value panel
func (b *SparkChart) ValueWidth() int {
return b.valueWidth
}
// SetValueWidth changes width of the value panel on the left
func (b *SparkChart) SetValueWidth(width int) {
b.valueWidth = width
}
// Top returns the value of the top of a chart. The value is
// used only if autosize is off to scale all the data
func (b *SparkChart) Top() float64 {
return b.topValue
}
// SetTop sets the theoretical highest value of data flow
// to scale the chart
func (b *SparkChart) SetTop(top float64) {
b.topValue = top
}
// AutoScale returns whether spark chart scales automatically
// depending on displayed data or it scales using Top value
func (b *SparkChart) AutoScale() bool {
return b.autosize
}
// SetAutoScale changes the way of scaling the data flow
func (b *SparkChart) SetAutoScale(auto bool) {
b.autosize = auto
}
// HilitePeaks returns whether chart draws maximum peaks
// with different color
func (b *SparkChart) HilitePeaks() bool {
return b.hiliteMax
}
// SetHilitePeaks enables or disables hiliting maximum
// values with different colors
func (b *SparkChart) SetHilitePeaks(hilite bool) {
b.hiliteMax = hilite
}