package termui import "fmt" import tm "github.com/nsf/termbox-go" const VDASH = '┊' const HDASH = '┈' const ORIGIN = '└' // only 16 possible combinations, why bother var braillePatterns = map[[2]int]rune{ [2]int{0, 0}: '⣀', [2]int{0, 1}: '⡠', [2]int{0, 2}: '⡐', [2]int{0, 3}: '⡈', [2]int{1, 0}: '⢄', [2]int{1, 1}: '⠤', [2]int{1, 2}: '⠔', [2]int{1, 3}: '⠌', [2]int{2, 0}: '⢂', [2]int{2, 1}: '⠢', [2]int{2, 2}: '⠒', [2]int{2, 3}: '⠊', [2]int{3, 0}: '⢁', [2]int{3, 1}: '⠡', [2]int{3, 2}: '⠑', [2]int{3, 3}: '⠉', } type LineChart struct { Block Data []float64 DataLabels []string Mode string // braille | dot DotStyle rune LineColor Attribute scale float64 AxesColor Attribute drawingX int drawingY int axisYHeight int axisXWidth int axisYLebelGap int axisXLebelGap int topValue float64 bottomValue float64 labelX [][]rune labelY [][]rune labelYSpace int maxY float64 minY float64 } func NewLineChart() *LineChart { lc := &LineChart{Block: *NewBlock()} lc.Mode = "braille" lc.DotStyle = '•' lc.axisXLebelGap = 2 lc.axisYLebelGap = 1 return lc } // one cell contains two data points func (lc *LineChart) renderBraille() []Point { ps := []Point{} getBaseMod := func(d float64) (b, m int) { b = int((d - lc.minY) / lc.scale) m = int(((d-lc.minY)-float64(b)*lc.scale)/0.25 + 0.5) return } for i := 0; i+1 < len(lc.Data) && i/2 < lc.axisXWidth; i += 2 { b0, m0 := getBaseMod(lc.Data[i]) b1, m1 := getBaseMod(lc.Data[i+1]) if b0 > b1 { m1 = 0 } if b0 < b1 { m1 = 3 } p := Point{} p.Code.Ch = braillePatterns[[2]int{m0, m1}] p.Code.Bg = toTmAttr(lc.BgColor) p.Code.Fg = toTmAttr(lc.LineColor) p.Y = lc.innerY + lc.innerHeight - 3 - b0 p.X = lc.innerX + lc.labelYSpace + 1 + i/2 ps = append(ps, p) } return ps } func (lc *LineChart) renderDot() []Point { ps := []Point{} for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ { p := Point{} p.Code.Ch = lc.DotStyle p.Code.Fg = toTmAttr(lc.LineColor) p.Code.Bg = toTmAttr(lc.BgColor) p.X = lc.innerX + lc.labelYSpace + 1 + i p.Y = lc.innerY + lc.innerHeight - 3 - int((lc.Data[i]-lc.minY)/lc.scale+0.5) ps = append(ps, p) } return ps } func (lc *LineChart) calcLabelX() { lc.labelX = [][]rune{} for i, l := 0, 0; i < len(lc.DataLabels) && l < lc.axisXWidth; i++ { if lc.Mode == "dot" { if l >= len(lc.DataLabels) { break } s := str2runes(lc.DataLabels[l]) if l+len(s) <= lc.axisXWidth { lc.labelX = append(lc.labelX, s) } l += (len(s) + lc.axisXLebelGap) // -1 needed } else { if 2*l >= len(lc.DataLabels) { break } s := str2runes(lc.DataLabels[2*l]) if l+len(s) <= lc.axisXWidth { lc.labelX = append(lc.labelX, s) } l += (len(s) + lc.axisXLebelGap) // -1 needed } } } func shortenFloatVal(x float64) string { s := fmt.Sprintf("%.2f", x) if len(s)-3 > 3 { s = fmt.Sprintf("%.2e", x) } if x < 0 { s = fmt.Sprintf("%.2f", x) } return s } func (lc *LineChart) calcLabelY() { span := lc.topValue - lc.bottomValue lc.scale = span / float64(lc.axisYHeight) n := (1 + lc.axisYHeight) / (lc.axisYLebelGap + 1) lc.labelY = make([][]rune, n) maxLen := 0 for i := 0; i < n; i++ { s := str2runes(shortenFloatVal(lc.bottomValue + float64(i)*span/float64(n))) if len(s) > maxLen { maxLen = len(s) } lc.labelY[i] = s } lc.labelYSpace = maxLen } func (lc *LineChart) calcLayout() { if lc.DataLabels == nil || len(lc.DataLabels) == 0 { lc.DataLabels = make([]string, len(lc.Data)) for i := range lc.Data { lc.DataLabels[i] = fmt.Sprint(i) } } lc.minY = lc.Data[0] lc.maxY = lc.Data[0] for _, v := range lc.Data { if v > lc.maxY { lc.maxY = v } if v < lc.minY { lc.minY = v } } lc.topValue = lc.maxY * 1.2 lc.bottomValue = lc.minY * 0.8 //- 0.05*(lc.maxY-lc.minY) if lc.minY < 0 { lc.bottomValue = lc.minY * 1.2 } lc.axisYHeight = lc.innerHeight - 2 lc.calcLabelY() lc.axisXWidth = lc.innerWidth - 1 - lc.labelYSpace lc.calcLabelX() lc.drawingX = lc.innerX + 1 + lc.labelYSpace lc.drawingY = lc.innerY } func (lc *LineChart) plotAxes() []Point { origY := lc.innerY + lc.innerHeight - 2 origX := lc.innerX + lc.labelYSpace ps := []Point{Point{Code: tm.Cell{Ch: ORIGIN, Bg: toTmAttr(lc.BgColor), Fg: toTmAttr(lc.AxesColor)}, X: origX, Y: origY}} for x := origX + 1; x < origX+lc.axisXWidth; x++ { p := Point{} p.X = x p.Y = origY p.Code.Bg = toTmAttr(lc.BgColor) p.Code.Fg = toTmAttr(lc.AxesColor) p.Code.Ch = HDASH ps = append(ps, p) } for dy := 1; dy <= lc.axisYHeight; dy++ { p := Point{} p.X = origX p.Y = origY - dy p.Code.Bg = toTmAttr(lc.BgColor) p.Code.Fg = toTmAttr(lc.AxesColor) p.Code.Ch = VDASH ps = append(ps, p) } // x label oft := 0 for _, rs := range lc.labelX { if oft+len(rs) > lc.axisXWidth { break } for j, r := range rs { p := Point{} p.Code.Ch = r p.Code.Fg = toTmAttr(lc.AxesColor) p.Code.Bg = toTmAttr(lc.BgColor) p.X = origX + oft + j p.Y = lc.innerY + lc.innerHeight - 1 ps = append(ps, p) } oft += len(rs) + lc.axisXLebelGap } // y labels for i, rs := range lc.labelY { for j, r := range rs { p := Point{} p.Code.Ch = r p.Code.Fg = toTmAttr(lc.AxesColor) p.Code.Bg = toTmAttr(lc.BgColor) p.X = lc.innerX + j p.Y = origY - i*(lc.axisYLebelGap+1) ps = append(ps, p) } } return ps } func (lc *LineChart) Buffer() []Point { ps := lc.Block.Buffer() lc.calcLayout() ps = append(ps, lc.plotAxes()...) if lc.Mode == "dot" { ps = append(ps, lc.renderDot()...) } else { ps = append(ps, lc.renderBraille()...) } return ps }