package clui import ( "fmt" xs "github.com/huandu/xstrings" term "github.com/nsf/termbox-go" ) // BarData is info about one bar in the chart. Every // bar can be customized by setting its own colors and // rune to draw the bar. Use ColorDefault for Fg and Bg, // and 0 for Ch to draw with BarChart defaults type BarData struct { Value float64 Title string Fg term.Attribute Bg term.Attribute Ch rune } // BarDataCell is used in callback to user to draw with // customized colors and runes type BarDataCell struct { // Title of the bar Item string // order number of the bar ID int // value of the bar that is currently drawn Value float64 // maximum value of the bar BarMax float64 // value of the highest bar TotalMax float64 // Default attributes and rune to draw the bar Fg term.Attribute Bg term.Attribute Ch rune } /* 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 BarChart struct { BaseControl data []BarData autosize bool gap int barWidth int legendWidth int valueWidth int showMarks bool showTitles bool onDrawCell func(*BarDataCell) } /* NewBarChart creates a new bar 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 CreateBarChart(parent Control, w, h int, scale int) *BarChart { c := new(BarChart) if w == AutoSize { w = 10 } if h == AutoSize { h = 5 } 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) c.SetScale(scale) if parent != nil { parent.AddChild(c) } return c } // Repaint draws the control on its View surface func (b *BarChart) Draw() { PushAttributes() defer PopAttributes() fg, bg := RealColor(b.fg, ColorBarChartText), RealColor(b.bg, ColorBarChartBack) SetTextColor(fg) SetBackColor(bg) FillRect(b.x, b.y, b.width, b.height, ' ') if len(b.data) == 0 { return } b.drawRulers() b.drawValues() b.drawLegend() b.drawBars() } func (b *BarChart) barHeight() int { if b.showTitles { return b.height - 2 } return b.height } func (b *BarChart) drawBars() { 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 } PushAttributes() defer PopAttributes() h := b.barHeight() pos := start parts := []rune(SysObject(ObjBarChart)) fg, bg := TextColor(), BackColor() 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 { SetTextColor(fColor) SetBackColor(bColor) FillRect(b.x+pos, b.y+h-barH, barW, barH, ch) } else { cellDef := BarDataCell{Item: d.Title, ID: idx, Value: 0, BarMax: d.Value, TotalMax: max, Fg: fColor, Bg: bColor, Ch: ch} for dy := 0; dy < barH; dy++ { req := cellDef req.Value = max * float64(dy+1) / float64(h) b.onDrawCell(&req) SetTextColor(req.Fg) SetBackColor(req.Bg) for dx := 0; dx < barW; dx++ { PutChar(b.x+pos+dx, b.y+h-1-dy, req.Ch) } } } if b.showTitles { SetTextColor(fg) SetBackColor(bg) if b.showMarks { c := parts[7] PutChar(b.x+pos+barW/2, b.y+h, c) } var s string shift := 0 if xs.Len(d.Title) > barW { s = CutText(d.Title, barW) } else { shift, s = AlignText(d.Title, barW, AlignCenter) } DrawRawText(b.x+pos+shift, b.y+h+1, s) } pos += barW + b.gap } } func (b *BarChart) drawLegend() { pos, width := b.calculateBarArea() if pos+width >= b.width-3 { return } PushAttributes() defer PopAttributes() fg, bg := RealColor(b.fg, ColorBarChartText), RealColor(b.bg, ColorBarChartBack) parts := []rune(SysObject(ObjBarChart)) defRune := parts[0] for idx, d := range b.data { if idx >= b.height { break } c := d.Ch if c == 0 { c = defRune } SetTextColor(d.Fg) SetBackColor(d.Bg) PutChar(b.x+pos+width, b.y+idx, c) s := CutText(fmt.Sprintf(" - %v", d.Title), b.legendWidth) SetTextColor(fg) SetBackColor(bg) DrawRawText(b.x+pos+width+1, b.y+idx, s) } } func (b *BarChart) drawValues() { if b.valueWidth <= 0 { return } pos, _ := b.calculateBarArea() if pos == 0 { return } h := b.barHeight() coeff, max := b.calculateMultiplier() if max == coeff { return } 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) DrawRawText(b.x, b.y+dy, s) dy += 2 } } func (b *BarChart) drawRulers() { if b.valueWidth <= 0 && b.legendWidth <= 0 && !b.showTitles { return } pos, vWidth := b.calculateBarArea() parts := []rune(SysObject(ObjBarChart)) h := b.barHeight() if pos > 0 { pos-- vWidth++ } // horizontal and vertical lines, corner cH, cV, cC := parts[1], parts[2], parts[5] if pos > 0 { for dy := 0; dy < h; dy++ { PutChar(b.x+pos, b.y+dy, cV) } } if b.showTitles { for dx := 0; dx < vWidth; dx++ { PutChar(b.x+pos+dx, b.y+h, cH) } } if pos > 0 && b.showTitles { PutChar(b.x+pos, b.y+h, cC) } } 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 } sz := (w - (dataCount-1)*b.gap) / dataCount if sz == 0 { sz = 1 } return sz } 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 } // AddData appends a new bar to a chart func (b *BarChart) AddData(val BarData) { b.data = append(b.data, val) } // ClearData removes all bar from chart func (b *BarChart) ClearData() { b.data = make([]BarData, 0) } // SetData assign a new bar list to a chart func (b *BarChart) SetData(data []BarData) { b.data = make([]BarData, len(data)) copy(b.data, data) } // AutoSize returns whether automatic bar width // calculation is on. If AutoSize is false then all // bars have width BarWidth. If AutoSize is true then // bar width is the maximum of three values: BarWidth, // calculated width that makes all bars fit the // bar chart area, and 1 func (b *BarChart) AutoSize() bool { return b.autosize } // SetAutoSize enables or disables automatic bar // width calculation func (b *BarChart) SetAutoSize(auto bool) { b.autosize = auto } // Gap returns width of visual gap between two adjacent bars func (b *BarChart) BarGap() int { return b.gap } // SetGap sets the space width between two adjacent bars func (b *BarChart) SetBarGap(gap int) { b.gap = gap } // MinBarWidth returns current minimal bar width func (b *BarChart) MinBarWidth() int { return b.barWidth } // SetMinBarWidth changes the minimal bar width func (b *BarChart) SetMinBarWidth(size int) { b.barWidth = size } // 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 *BarChart) ValueWidth() int { return b.valueWidth } // SetValueWidth changes width of the value panel on the left func (b *BarChart) SetValueWidth(width int) { b.valueWidth = width } // ShowTitles returns if chart displays horizontal axis and // bar titles under it func (b *BarChart) ShowTitles() bool { return b.showTitles } // SetShowTitles turns on and off horizontal axis and bar titles func (b *BarChart) SetShowTitles(show bool) { b.showTitles = show } // LegendWidth returns width of chart legend displayed at the // right side of the chart. Set it to 0 to disable legend func (b *BarChart) LegendWidth() int { return b.legendWidth } // SetLegendWidth sets new legend panel width func (b *BarChart) SetLegendWidth(width int) { b.legendWidth = width } // OnDrawCell sets callback that allows to draw multicolored // bars. BarChart sends the current attrubutes and rune that // it is going to use to display as well as the current value // of the bar. A user can change the values of BarDataCell // depending on some external data or calculations - only // changing colors and rune makes sense. Changing anything else // does not affect the chart func (b *BarChart) OnDrawCell(fn func(*BarDataCell)) { b.onDrawCell = fn } // ShowMarks returns if horizontal axis has mark under each // bar. To show marks, ShowTitles must be enabled. func (b *BarChart) ShowMarks() bool { return b.showMarks } // SetShowMarks turns on and off marks under horizontal axis func (b *BarChart) SetShowMarks(show bool) { b.showMarks = show }