mirror of
https://github.com/VladimirMarkelov/clui.git
synced 2025-04-26 13:49:01 +08:00
#37 - basic BarChart and demo for it
This commit is contained in:
parent
a225b5047b
commit
9e623b03b6
402
barchart.go
Normal file
402
barchart.go
Normal file
@ -0,0 +1,402 @@
|
||||
package clui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
xs "github.com/huandu/xstrings"
|
||||
term "github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
type BarData struct {
|
||||
Value float64
|
||||
Title string
|
||||
Fg term.Attribute
|
||||
Bg term.Attribute
|
||||
Ch rune
|
||||
}
|
||||
|
||||
type BarDataCell struct {
|
||||
Item string
|
||||
Id int
|
||||
Value float64
|
||||
MaxValue float64
|
||||
Fg term.Attribute
|
||||
Bg term.Attribute
|
||||
Ch rune
|
||||
}
|
||||
|
||||
/*
|
||||
Label is a decorative control that can display text in horizontal
|
||||
or vertical direction. Other available text features are alignment
|
||||
and multi-line ability. Text can be single- or multi-colored with
|
||||
tags inside the text. Multi-colored strings have limited support
|
||||
of alignment feature: if text is longer than Label width the text
|
||||
is always left aligned
|
||||
*/
|
||||
type BarChart struct {
|
||||
ControlBase
|
||||
data []BarData
|
||||
autosize bool
|
||||
gap int
|
||||
barWidth int
|
||||
legendWidth int
|
||||
valueWidth int
|
||||
showMarks bool
|
||||
showTicks bool
|
||||
showTitles bool
|
||||
onDrawCell func(*BarDataCell)
|
||||
}
|
||||
|
||||
/*
|
||||
NewLabel creates a new label.
|
||||
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.
|
||||
title - is Label title.
|
||||
scale - the way of scaling the control when the parent is resized. Use DoNotScale constant if the
|
||||
control should keep its original size.
|
||||
*/
|
||||
func NewBarChart(view View, parent Control, w, h int, scale int) *BarChart {
|
||||
c := new(BarChart)
|
||||
|
||||
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.showTitles = true
|
||||
c.barWidth = 3
|
||||
c.data = make([]BarData, 0)
|
||||
|
||||
if parent != nil {
|
||||
parent.AddChild(c, scale)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Repaint draws the control on its View surface
|
||||
func (b *BarChart) Repaint() {
|
||||
canvas := b.view.Canvas()
|
||||
tm := b.view.Screen().Theme()
|
||||
|
||||
fg, bg := RealColor(tm, b.fg, ColorBarChartText), RealColor(tm, b.bg, ColorBarChartBack)
|
||||
canvas.FillRect(b.x, b.y, b.width, b.height, term.Cell{Ch: ' ', Fg: fg, Bg: bg})
|
||||
|
||||
if len(b.data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
b.drawRulers(tm, fg, bg)
|
||||
b.drawValues(tm, fg, bg)
|
||||
b.drawLegend(tm, fg, bg)
|
||||
b.drawBars(tm, fg, bg)
|
||||
}
|
||||
|
||||
func (b *BarChart) barHeight() int {
|
||||
if b.showTitles {
|
||||
return b.height - 2
|
||||
} else {
|
||||
return b.height
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BarChart) drawBars(tm Theme, fg, bg term.Attribute) {
|
||||
if len(b.data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
start, width := b.calculateBarArea()
|
||||
if width < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
barW := b.calculateBarWidth()
|
||||
if barW == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
coeff, max := b.calculateMultiplier()
|
||||
if coeff == 0.0 {
|
||||
return
|
||||
}
|
||||
|
||||
h := b.barHeight()
|
||||
pos := start
|
||||
canvas := b.view.Canvas()
|
||||
parts := []rune(tm.SysObject(ObjBarChart))
|
||||
|
||||
for idx, d := range b.data {
|
||||
if pos+barW >= start+width {
|
||||
break
|
||||
}
|
||||
|
||||
fColor, bColor := d.Fg, d.Bg
|
||||
ch := d.Ch
|
||||
if fColor == ColorDefault {
|
||||
fColor = fg
|
||||
}
|
||||
if bColor == ColorDefault {
|
||||
bColor = bg
|
||||
}
|
||||
if ch == 0 {
|
||||
ch = parts[0]
|
||||
}
|
||||
|
||||
barH := int(d.Value * coeff)
|
||||
if b.onDrawCell == nil {
|
||||
cell := term.Cell{Ch: ch, Fg: fg, Bg: bg}
|
||||
canvas.FillRect(b.x+pos, b.y+h-barH, barW, barH, cell)
|
||||
} else {
|
||||
cellDef := BarDataCell{Item: d.Title, Id: idx, Value: d.Value, MaxValue: max, Fg: fg, Bg: bg, Ch: ch}
|
||||
for dy := 0; dy < barH; dy++ {
|
||||
req := cellDef
|
||||
b.onDrawCell(&req)
|
||||
cell := term.Cell{Ch: req.Ch, Fg: req.Fg, Bg: req.Bg}
|
||||
for dx := 0; dx < barW; dx++ {
|
||||
canvas.PutSymbol(b.x+pos+dx, b.y+h-1-dy, cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b.showTitles {
|
||||
if b.showTicks {
|
||||
c := parts[7]
|
||||
canvas.PutSymbol(b.x+pos+barW/2, b.y+h, term.Cell{Ch: c, Bg: bg, Fg: fg})
|
||||
}
|
||||
var s string
|
||||
shift := 0
|
||||
if xs.Len(d.Title) > barW {
|
||||
s = CutText(d.Title, barW)
|
||||
} else {
|
||||
shift, s = AlignText(d.Title, barW, AlignCenter)
|
||||
}
|
||||
canvas.PutText(b.x+pos+shift, b.y+h+1, s, fg, bg)
|
||||
}
|
||||
|
||||
pos += barW + b.gap
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BarChart) drawLegend(tm Theme, fg, bg term.Attribute) {
|
||||
pos, width := b.calculateBarArea()
|
||||
if pos+width >= b.width-3 {
|
||||
return
|
||||
}
|
||||
|
||||
canvas := b.view.Canvas()
|
||||
for idx, d := range b.data {
|
||||
if idx >= b.height {
|
||||
break
|
||||
}
|
||||
|
||||
canvas.PutSymbol(b.x+pos+width, b.y+idx, term.Cell{Ch: d.Ch, Fg: d.Fg, Bg: d.Bg})
|
||||
s := CutText(fmt.Sprintf(" - %v", d.Title), b.legendWidth)
|
||||
canvas.PutText(b.x+pos+width+1, b.y+idx, s, fg, bg)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BarChart) drawValues(tm Theme, fg, bg term.Attribute) {
|
||||
pos, _ := b.calculateBarArea()
|
||||
if pos == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
h := b.barHeight()
|
||||
coeff, max := b.calculateMultiplier()
|
||||
if max == coeff {
|
||||
return
|
||||
}
|
||||
|
||||
canvas := b.view.Canvas()
|
||||
dy := 0
|
||||
format := fmt.Sprintf("%%%vf", 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 *BarChart) drawRulers(tm Theme, fg, bg term.Attribute) {
|
||||
if b.valueWidth <= 0 && b.legendWidth <= 0 && !b.showTitles {
|
||||
return
|
||||
}
|
||||
|
||||
pos, vWidth := b.calculateBarArea()
|
||||
|
||||
parts := []rune(tm.SysObject(ObjBarChart))
|
||||
h := b.barHeight()
|
||||
|
||||
if pos > 0 {
|
||||
pos--
|
||||
vWidth++
|
||||
}
|
||||
|
||||
// horizontal and vertical lines, corner
|
||||
cH, cV, cC := parts[1], parts[2], parts[5]
|
||||
canvas := b.view.Canvas()
|
||||
|
||||
if pos > 0 {
|
||||
for dy := 0; dy < h; dy++ {
|
||||
canvas.PutSymbol(b.x+pos, b.y+dy, term.Cell{Ch: cV, Fg: fg, Bg: bg})
|
||||
}
|
||||
}
|
||||
if b.showTitles {
|
||||
for dx := 0; dx < vWidth; dx++ {
|
||||
canvas.PutSymbol(b.x+pos+dx, b.y+h, term.Cell{Ch: cH, Fg: fg, Bg: bg})
|
||||
}
|
||||
}
|
||||
if pos > 0 && b.showTitles {
|
||||
canvas.PutSymbol(b.x+pos, b.y+h, term.Cell{Ch: cC, Fg: fg, Bg: bg})
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BarChart) calculateBarArea() (int, int) {
|
||||
w := b.width
|
||||
pos := 0
|
||||
|
||||
if b.valueWidth < w/2 {
|
||||
w = w - b.valueWidth - 1
|
||||
pos = b.valueWidth + 1
|
||||
}
|
||||
|
||||
if b.legendWidth < w/2 {
|
||||
w -= b.legendWidth
|
||||
}
|
||||
|
||||
return pos, w
|
||||
}
|
||||
|
||||
func (b *BarChart) calculateBarWidth() int {
|
||||
if len(b.data) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if !b.autosize {
|
||||
return b.barWidth
|
||||
}
|
||||
|
||||
w := b.width
|
||||
if b.valueWidth < w/2 {
|
||||
w = w - b.valueWidth - 1
|
||||
}
|
||||
if b.legendWidth < w/2 {
|
||||
w -= b.legendWidth
|
||||
}
|
||||
|
||||
dataCount := len(b.data)
|
||||
minSize := dataCount*b.barWidth + (dataCount-1)*b.gap
|
||||
if minSize >= w {
|
||||
return b.barWidth
|
||||
}
|
||||
|
||||
return (w - (dataCount-1)*b.gap) / dataCount
|
||||
}
|
||||
|
||||
func (b *BarChart) calculateMultiplier() (float64, float64) {
|
||||
if len(b.data) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
h := b.barHeight()
|
||||
if h <= 1 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
max := b.data[0].Value
|
||||
for _, val := range b.data {
|
||||
if val.Value > max {
|
||||
max = val.Value
|
||||
}
|
||||
}
|
||||
|
||||
if max == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
return float64(h) / max, max
|
||||
}
|
||||
|
||||
func (b *BarChart) AddData(val BarData) {
|
||||
b.data = append(b.data, val)
|
||||
}
|
||||
|
||||
func (b *BarChart) ClearData() {
|
||||
b.data = make([]BarData, 0)
|
||||
}
|
||||
|
||||
func (b *BarChart) SetData(data []BarData) {
|
||||
b.data = make([]BarData, len(data))
|
||||
copy(b.data, data)
|
||||
}
|
||||
|
||||
func (b *BarChart) AutoSize() bool {
|
||||
return b.autosize
|
||||
}
|
||||
|
||||
func (b *BarChart) SetAutoSize(auto bool) {
|
||||
b.autosize = auto
|
||||
}
|
||||
|
||||
func (b *BarChart) Gap() int {
|
||||
return b.gap
|
||||
}
|
||||
|
||||
func (b *BarChart) SetGap(gap int) {
|
||||
b.gap = gap
|
||||
}
|
||||
|
||||
func (b *BarChart) MinBarSize() int {
|
||||
return b.barWidth
|
||||
}
|
||||
|
||||
func (b *BarChart) SetMinBarSize(size int) {
|
||||
b.barWidth = size
|
||||
}
|
||||
|
||||
func (b *BarChart) ValueWidth() int {
|
||||
return b.valueWidth
|
||||
}
|
||||
|
||||
func (b *BarChart) SetValueWidth(width int) {
|
||||
b.valueWidth = width
|
||||
}
|
||||
|
||||
func (b *BarChart) ShowTitles() bool {
|
||||
return b.showTitles
|
||||
}
|
||||
|
||||
func (b *BarChart) SetShowTitles(show bool) {
|
||||
b.showTitles = show
|
||||
}
|
||||
|
||||
func (b *BarChart) LegendWidth() int {
|
||||
return b.legendWidth
|
||||
}
|
||||
|
||||
func (b *BarChart) SetLegendWidth(width int) {
|
||||
b.legendWidth = width
|
||||
}
|
||||
|
||||
func (b *BarChart) OnDrawCell(fn func(*BarDataCell)) {
|
||||
b.onDrawCell = fn
|
||||
}
|
||||
|
||||
func (b *BarChart) ShowMarks() bool {
|
||||
return b.showMarks
|
||||
}
|
||||
|
||||
func (b *BarChart) SetShowMarks(show bool) {
|
||||
b.showMarks = show
|
||||
}
|
@ -146,6 +146,7 @@ const (
|
||||
ObjCheckBox = "CheckBox"
|
||||
ObjRadio = "Radio"
|
||||
ObjProgressBar = "ProgressBar"
|
||||
ObjBarChart = "BarChart"
|
||||
)
|
||||
|
||||
// Available color identifiers that can be used in themes
|
||||
@ -198,6 +199,10 @@ const (
|
||||
ColorProgressActiveBack = "ProgressActiveBack"
|
||||
ColorProgressActiveText = "ProgressActiveText"
|
||||
ColorProgressTitleText = "ProgressTitle"
|
||||
|
||||
// barchart colors
|
||||
ColorBarChartBack = "BarChartBack"
|
||||
ColorBarChartText = "BarChartText"
|
||||
)
|
||||
|
||||
// EventType is event that window or control may process
|
||||
|
40
demos/barchart.go
Normal file
40
demos/barchart.go
Normal file
@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// "fmt"
|
||||
ui "github.com/VladimirMarkelov/clui"
|
||||
// term "github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
func createView(c *ui.Composer) *ui.BarChart {
|
||||
|
||||
view := c.CreateView(0, 0, 10, 7, "BarChart Demo")
|
||||
bch := ui.NewBarChart(view, view, 30, 12, 1)
|
||||
|
||||
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.SetGap(1)
|
||||
d := []ui.BarData{
|
||||
{Value: 80, Title: "80%"},
|
||||
{Value: 50, Title: "50%"},
|
||||
{Value: 150, Title: ">100%"},
|
||||
}
|
||||
b.SetData(d)
|
||||
b.SetValueWidth(5)
|
||||
b.SetAutoSize(true)
|
||||
|
||||
// start event processing loop - the main core of the library
|
||||
c.MainLoop()
|
||||
}
|
||||
|
||||
func main() {
|
||||
mainLoop()
|
||||
}
|
4
theme.go
4
theme.go
@ -136,6 +136,7 @@ func (s *ThemeManager) Reset() {
|
||||
defTheme.objects[ObjCheckBox] = "[] X?"
|
||||
defTheme.objects[ObjRadio] = "() *"
|
||||
defTheme.objects[ObjProgressBar] = "░▒"
|
||||
defTheme.objects[ObjBarChart] = "█─│┌┐└┘┬┴├┤┼"
|
||||
|
||||
defTheme.colors[ColorDisabledText] = ColorBlackBold
|
||||
defTheme.colors[ColorDisabledBack] = ColorWhite
|
||||
@ -178,6 +179,9 @@ func (s *ThemeManager) Reset() {
|
||||
defTheme.colors[ColorProgressActiveBack] = ColorBlueBold
|
||||
defTheme.colors[ColorProgressTitleText] = ColorWhite
|
||||
|
||||
defTheme.colors[ColorBarChartBack] = ColorBlack
|
||||
defTheme.colors[ColorBarChartText] = ColorWhite
|
||||
|
||||
s.themes[defaultTheme] = defTheme
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user