Add LineChart and BarChart

This commit is contained in:
gizak 2015-02-07 20:19:16 -05:00
parent 8351d8f305
commit 92a301a247
3 changed files with 454 additions and 9 deletions

96
bar.go Normal file
View File

@ -0,0 +1,96 @@
package termui
import "fmt"
type BarChart struct {
Block
BarColor Attribute
TextColor Attribute
NumColor Attribute
Data []int
DataLabels []string
BarWidth int
BarGap int
labels [][]rune
dataNum [][]rune
numBar int
scale float64
max int
}
func NewBarChart() *BarChart {
bc := &BarChart{Block: *NewBlock()}
bc.BarColor = ColorCyan
bc.NumColor = ColorWhite
bc.TextColor = ColorWhite
bc.BarGap = 1
bc.BarWidth = 3
return bc
}
func (bc *BarChart) layout() {
bc.numBar = bc.innerWidth / (bc.BarGap + bc.BarWidth)
bc.labels = make([][]rune, bc.numBar)
bc.dataNum = make([][]rune, len(bc.Data))
for i := 0; i < bc.numBar && i < len(bc.DataLabels) && i < len(bc.Data); i++ {
bc.labels[i] = trimStr2Runes(bc.DataLabels[i], bc.BarWidth)
n := bc.Data[i]
s := fmt.Sprint(n)
bc.dataNum[i] = trimStr2Runes(s, bc.BarWidth)
}
bc.max = bc.Data[0] // what if Data is nil?
for i := 0; i < len(bc.Data); i++ {
if bc.max < bc.Data[i] {
bc.max = bc.Data[i]
}
}
bc.scale = float64(bc.max) / float64(bc.innerHeight-1)
}
func (bc *BarChart) Buffer() []Point {
ps := bc.Block.Buffer()
bc.layout()
for i := 0; i < bc.numBar && i < len(bc.Data) && i < len(bc.DataLabels); i++ {
h := int(float64(bc.Data[i]) / bc.scale)
oftX := i * (bc.BarWidth + bc.BarGap)
// plot bar
for j := 0; j < bc.BarWidth; j++ {
for k := 0; k < h; k++ {
p := Point{}
p.Code.Ch = ' '
p.Code.Bg = toTmAttr(bc.BarColor)
p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j
p.Y = bc.innerY + bc.innerHeight - 2 - k
ps = append(ps, p)
}
}
// plot text
for j := 0; j < len(bc.labels[i]); j++ {
p := Point{}
p.Code.Ch = bc.labels[i][j]
p.Code.Bg = toTmAttr(bc.BgColor)
p.Code.Fg = toTmAttr(bc.TextColor)
p.Y = bc.innerY + bc.innerHeight - 1
p.X = bc.innerX + oftX + j
ps = append(ps, p)
}
// plot num
for j := 0; j < len(bc.dataNum[i]); j++ {
p := Point{}
p.Code.Ch = bc.dataNum[i][j]
p.Code.Fg = toTmAttr(bc.NumColor)
p.Code.Bg = toTmAttr(bc.BarColor)
if h == 0 {
p.Code.Bg = toTmAttr(bc.BgColor)
}
p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j
p.Y = bc.innerY + bc.innerHeight - 2
ps = append(ps, p)
}
}
return ps
}

279
chart.go Normal file
View File

@ -0,0 +1,279 @@
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
}

View File

@ -2,6 +2,8 @@ package main
import ui "github.com/gizak/termui"
import tm "github.com/nsf/termbox-go"
import "math"
import "time"
func main() {
@ -11,11 +13,11 @@ func main() {
}
defer ui.Close()
p := ui.NewP(":PRESS q TO QUIT DEMO\nThis is an example of termui package rendering.")
p.Height = 4
p.Width = 59
p := ui.NewP(":PRESS q TO QUIT DEMO")
p.Height = 3
p.Width = 50
p.TextFgColor = ui.ColorWhite
p.Border.Label = "Text"
p.Border.Label = "Text Box"
p.Border.FgColor = ui.ColorCyan
strs := []string{"[0] gizak/termui", "[1] editbox.go", "[2] iterrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] nsf/termbox-go"}
@ -29,7 +31,7 @@ func main() {
g := ui.NewGauge()
g.Percent = 50
g.Width = 52
g.Width = 50
g.Height = 3
g.Y = 11
g.Border.Label = "Gauge"
@ -53,16 +55,84 @@ func main() {
spark1.LineColor = ui.ColorRed
sp := ui.NewSparklines(spark, spark1)
sp.Width = 20
sp.Height = 6
sp.Width = 25
sp.Height = 7
sp.Border.Label = "Sparkline"
sp.Y = 14
sp.Y = 4
sp.X = 25
lc := ui.NewLineChart()
sinps := (func() []float64 {
n := 100
ps := make([]float64, n)
for i := range ps {
ps[i] = 1 + math.Sin(float64(i)/4)
}
return ps
})()
lc.Border.Label = "Line Chart"
lc.Data = sinps
lc.Width = 50
lc.Height = 11
lc.X = 0
lc.Y = 14
lc.AxesColor = ui.ColorWhite
lc.LineColor = ui.ColorRed | ui.AttrBold
lc.Mode = "dot"
bc := ui.NewBarChart()
bcdata := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6}
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
bc.Border.Label = "Bar Chart"
bc.Width = 26
bc.Height = 10
bc.X = 51
bc.Y = 0
bc.DataLabels = bclabels
bc.BarColor = ui.ColorGreen
bc.NumColor = ui.ColorBlack
lc1 := ui.NewLineChart()
lc1.Border.Label = "Line Chart"
rndwalk := (func() []float64 {
n := 100
d := make([]float64, n)
for i := 1; i < n; i++ {
if i < 20 {
d[i] = d[i-1] + 0.01
}
if i > 20 {
d[i] = d[i-1] - 0.05
}
}
return d
})()
lc1.Data = rndwalk
lc1.Width = 26
lc1.Height = 11
lc1.X = 51
lc1.Y = 14
lc1.AxesColor = ui.ColorWhite
lc1.LineColor = ui.ColorYellow | ui.AttrBold
p1 := ui.NewP("Hey!\nI am a borderless block!")
p1.HasBorder = false
p1.Width = 26
p1.Height = 2
p1.TextFgColor = ui.ColorMagenta
p1.X = 52
p1.Y = 11
draw := func(t int) {
g.Percent = t % 101
list.Items = strs[t%9:]
sp.Lines[0].Data = spdata[t%10:]
ui.Render(p, list, g, sp)
sp.Lines[1].Data = spdata[t/2%10:]
lc.Data = sinps[t/2:]
lc1.Data = rndwalk[t:]
bc.Data = bcdata[t/2%10:]
ui.Render(p, list, g, sp, lc, bc, lc1, p1)
}
evt := make(chan tm.Event)