From 23bf27ed03f034ac32a1fb5c089aca09fd560652 Mon Sep 17 00:00:00 2001 From: Vladimir Markelov Date: Fri, 11 Dec 2015 15:33:29 -0800 Subject: [PATCH] closes #48 - spark chart with demo --- consts.go | 9 ++ demos/spark.go | 81 ++++++++++++++ sparkchart.go | 281 +++++++++++++++++++++++++++++++++++++++++++++++++ theme.go | 8 ++ 4 files changed, 379 insertions(+) create mode 100644 demos/spark.go create mode 100644 sparkchart.go diff --git a/consts.go b/consts.go index a1511d8..cf09034 100644 --- a/consts.go +++ b/consts.go @@ -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 diff --git a/demos/spark.go b/demos/spark.go new file mode 100644 index 0000000..92768d7 --- /dev/null +++ b/demos/spark.go @@ -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() +} diff --git a/sparkchart.go b/sparkchart.go new file mode 100644 index 0000000..0cac437 --- /dev/null +++ b/sparkchart.go @@ -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 +} diff --git a/theme.go b/theme.go index cf96757..dccce4b 100644 --- a/theme.go +++ b/theme.go @@ -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 }