closes #48 - spark chart with demo

This commit is contained in:
Vladimir Markelov 2015-12-11 15:33:29 -08:00
parent 383cc9f343
commit 23bf27ed03
4 changed files with 379 additions and 0 deletions

View File

@ -147,6 +147,7 @@ const (
ObjRadio = "Radio"
ObjProgressBar = "ProgressBar"
ObjBarChart = "BarChart"
ObjSparkChart = "SparkChart"
)
// Available color identifiers that can be used in themes
@ -203,6 +204,14 @@ const (
// barchart colors
ColorBarChartBack = "BarChartBack"
ColorBarChartText = "BarChartText"
// sparkchart colors
ColorSparkChartBack = "SparkChartBack"
ColorSparkChartText = "SparkChartText"
ColorSparkChartBarBack = "SparkChartBarBack"
ColorSparkChartBarText = "SparkChartBarText"
ColorSparkChartMaxBack = "SparkChartMaxBack"
ColorSparkChartMaxText = "SparkChartMaxText"
)
// EventType is event that window or control may process

81
demos/spark.go Normal file
View File

@ -0,0 +1,81 @@
package main
import (
ui "github.com/VladimirMarkelov/clui"
"math/rand"
"time"
)
func createView(c *ui.Composer) *ui.SparkChart {
view := c.CreateView(0, 0, 10, 7, "BarChart Demo")
bch := ui.NewSparkChart(view, view, 25, 12, 1)
bch.SetTop(20)
frmChk := ui.NewFrame(view, view, 8, 5, ui.BorderNone, ui.DoNotScale)
frmChk.SetPack(ui.Vertical)
chkValues := ui.NewCheckBox(view, frmChk, ui.AutoSize, "Show Values", ui.DoNotScale)
chkValues.SetState(0)
chkHilite := ui.NewCheckBox(view, frmChk, ui.AutoSize, "Hilite peaks", ui.DoNotScale)
chkHilite.SetState(1)
chkAuto := ui.NewCheckBox(view, frmChk, ui.AutoSize, "Auto scale", ui.DoNotScale)
chkAuto.SetState(1)
chkValues.OnChange(func(state int) {
if state == 0 {
bch.SetValueWidth(0)
c.PutEvent(ui.Event{Type: ui.EventRedraw})
} else if state == 1 {
bch.SetValueWidth(5)
c.PutEvent(ui.Event{Type: ui.EventRedraw})
}
})
chkHilite.OnChange(func(state int) {
if state == 0 {
bch.SetHilitePeaks(false)
c.PutEvent(ui.Event{Type: ui.EventRedraw})
} else if state == 1 {
bch.SetHilitePeaks(true)
c.PutEvent(ui.Event{Type: ui.EventRedraw})
}
})
chkAuto.OnChange(func(state int) {
if state == 0 {
bch.SetAutoScale(false)
c.PutEvent(ui.Event{Type: ui.EventRedraw})
} else if state == 1 {
bch.SetAutoScale(true)
c.PutEvent(ui.Event{Type: ui.EventRedraw})
}
})
return bch
}
func mainLoop() {
// Every application must create a single Composer and
// call its intialize method
c := ui.InitLibrary()
defer c.Close()
b := createView(c)
b.SetData([]float64{1, 2, 3, 4, 5, 6, 6, 7, 5, 8, 9})
ticker := time.NewTicker(time.Millisecond * 200).C
go func() {
for {
select {
case <-ticker:
b.AddData(float64(rand.Int31n(20)))
c.PutEvent(ui.Event{Type: ui.EventRedraw})
}
}
}()
// start event processing loop - the main core of the library
c.MainLoop()
}
func main() {
mainLoop()
}

281
sparkchart.go Normal file
View File

@ -0,0 +1,281 @@
package clui
import (
"fmt"
// xs "github.com/huandu/xstrings"
term "github.com/nsf/termbox-go"
)
/*
BarChart is a chart that represents grouped data with
rectangular bars. It can be monochrome - defaut behavior.
One can assign individual color to each bar and even use
custom drawn bars to display multicolored bars depending
on bar value.
All bars have the same width: either constant BarSize - in
case of AutoSize is false, or automatically calculated but
cannot be less than BarSize. Bars that do not fit the chart
area are not displayed.
BarChart displays vertical axis with values on the chart left
if ValueWidth greater than 0, horizontal axis with bar titles
if ShowTitles is true (to enable displaying marks on horizontal
axis, set ShowMarks to true), and chart legend on the right if
LegendWidth is greater than 3.
If LegendWidth is greater than half of the chart it is not
displayed. The same is applied to ValueWidth
*/
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
} else {
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
}

View File

@ -137,6 +137,7 @@ func (s *ThemeManager) Reset() {
defTheme.objects[ObjRadio] = "() *"
defTheme.objects[ObjProgressBar] = "░▒"
defTheme.objects[ObjBarChart] = "█─│┌┐└┘┬┴├┤┼"
defTheme.objects[ObjSparkChart] = "█"
defTheme.colors[ColorDisabledText] = ColorBlackBold
defTheme.colors[ColorDisabledBack] = ColorWhite
@ -182,6 +183,13 @@ func (s *ThemeManager) Reset() {
defTheme.colors[ColorBarChartBack] = ColorBlack
defTheme.colors[ColorBarChartText] = ColorWhite
defTheme.colors[ColorSparkChartBack] = ColorBlack
defTheme.colors[ColorSparkChartText] = ColorWhite
defTheme.colors[ColorSparkChartBarBack] = ColorBlack
defTheme.colors[ColorSparkChartBarText] = ColorCyan
defTheme.colors[ColorSparkChartMaxBack] = ColorBlack
defTheme.colors[ColorSparkChartMaxText] = ColorCyanBold
s.themes[defaultTheme] = defTheme
}