Finish Event

This commit is contained in:
gizak
2015-09-18 11:41:44 -04:00
parent 3ea00a7476
commit e89b10ff4f
39 changed files with 425 additions and 55 deletions

137
_widget/barchart.go Normal file
View File

@@ -0,0 +1,137 @@
// +build ignore
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import "fmt"
// BarChart creates multiple bars in a widget:
/*
bc := termui.NewBarChart()
data := []int{3, 2, 5, 3, 9, 5}
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
bc.Border.Label = "Bar Chart"
bc.Data = data
bc.Width = 26
bc.Height = 10
bc.DataLabels = bclabels
bc.TextColor = termui.ColorGreen
bc.BarColor = termui.ColorRed
bc.NumColor = termui.ColorYellow
*/
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
}
// NewBarChart returns a new *BarChart with current theme.
func NewBarChart() *BarChart {
bc := &BarChart{Block: *NewBlock()}
bc.BarColor = theme.BarChartBar
bc.NumColor = theme.BarChartNum
bc.TextColor = theme.BarChartText
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? Sometimes when bar graph is nill it produces panic with panic: runtime error: index out of range
// Asign a negative value to get maxvalue auto-populates
if bc.max == 0 {
bc.max = -1
}
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) SetMax(max int) {
if max > 0 {
bc.max = max
}
}
// Buffer implements Bufferer interface.
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.Ch = ' '
p.Bg = bc.BarColor
if bc.BarColor == ColorDefault { // when color is default, space char treated as transparent!
p.Bg |= AttrReverse
}
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, k := 0, 0; j < len(bc.labels[i]); j++ {
w := charWidth(bc.labels[i][j])
p := Point{}
p.Ch = bc.labels[i][j]
p.Bg = bc.BgColor
p.Fg = bc.TextColor
p.Y = bc.innerY + bc.innerHeight - 1
p.X = bc.innerX + oftX + k
ps = append(ps, p)
k += w
}
// plot num
for j := 0; j < len(bc.dataNum[i]); j++ {
p := Point{}
p.Ch = bc.dataNum[i][j]
p.Fg = bc.NumColor
p.Bg = bc.BarColor
if bc.BarColor == ColorDefault { // the same as above
p.Bg |= AttrReverse
}
if h == 0 {
p.Bg = 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 bc.Block.chopOverflow(ps)
}

76
_widget/canvas.go Normal file
View File

@@ -0,0 +1,76 @@
// +build ignore
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
/*
dots:
,___,
|1 4|
|2 5|
|3 6|
|7 8|
`````
*/
var brailleBase = '\u2800'
var brailleOftMap = [4][2]rune{
{'\u0001', '\u0008'},
{'\u0002', '\u0010'},
{'\u0004', '\u0020'},
{'\u0040', '\u0080'}}
// Canvas contains drawing map: i,j -> rune
type Canvas map[[2]int]rune
// NewCanvas returns an empty Canvas
func NewCanvas() Canvas {
return make(map[[2]int]rune)
}
func chOft(x, y int) rune {
return brailleOftMap[y%4][x%2]
}
func (c Canvas) rawCh(x, y int) rune {
if ch, ok := c[[2]int{x, y}]; ok {
return ch
}
return '\u0000' //brailleOffset
}
// return coordinate in terminal
func chPos(x, y int) (int, int) {
return y / 4, x / 2
}
// Set sets a point (x,y) in the virtual coordinate
func (c Canvas) Set(x, y int) {
i, j := chPos(x, y)
ch := c.rawCh(i, j)
ch |= chOft(x, y)
c[[2]int{i, j}] = ch
}
// Unset removes point (x,y)
func (c Canvas) Unset(x, y int) {
i, j := chPos(x, y)
ch := c.rawCh(i, j)
ch &= ^chOft(x, y)
c[[2]int{i, j}] = ch
}
// Buffer returns un-styled points
func (c Canvas) Buffer() []Point {
ps := make([]Point, len(c))
i := 0
for k, v := range c {
ps[i] = newPoint(v+brailleBase, k[0], k[1])
i++
}
return ps
}

57
_widget/canvas_test.go Normal file
View File

@@ -0,0 +1,57 @@
//+build ignore
package termui
import (
"testing"
"github.com/davecgh/go-spew/spew"
)
func TestCanvasSet(t *testing.T) {
c := NewCanvas()
c.Set(0, 0)
c.Set(0, 1)
c.Set(0, 2)
c.Set(0, 3)
c.Set(1, 3)
c.Set(2, 3)
c.Set(3, 3)
c.Set(4, 3)
c.Set(5, 3)
spew.Dump(c)
}
func TestCanvasUnset(t *testing.T) {
c := NewCanvas()
c.Set(0, 0)
c.Set(0, 1)
c.Set(0, 2)
c.Unset(0, 2)
spew.Dump(c)
c.Unset(0, 3)
spew.Dump(c)
}
func TestCanvasBuffer(t *testing.T) {
c := NewCanvas()
c.Set(0, 0)
c.Set(0, 1)
c.Set(0, 2)
c.Set(0, 3)
c.Set(1, 3)
c.Set(2, 3)
c.Set(3, 3)
c.Set(4, 3)
c.Set(5, 3)
c.Set(6, 3)
c.Set(7, 2)
c.Set(8, 1)
c.Set(9, 0)
bufs := c.Buffer()
rs := make([]rune, len(bufs))
for i, v := range bufs {
rs[i] = v.Ch
}
spew.Dump(string(rs))
}

113
_widget/gauge.go Normal file
View File

@@ -0,0 +1,113 @@
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"strconv"
"strings"
)
// Gauge is a progress bar like widget.
// A simple example:
/*
g := termui.NewGauge()
g.Percent = 40
g.Width = 50
g.Height = 3
g.Border.Label = "Slim Gauge"
g.BarColor = termui.ColorRed
g.PercentColor = termui.ColorBlue
*/
// Align is the position of the gauge's label.
type Align int
// All supported positions.
const (
AlignLeft Align = iota
AlignCenter
AlignRight
)
type Gauge struct {
Block
Percent int
BarColor Attribute
PercentColor Attribute
Label string
LabelAlign Align
}
// NewGauge return a new gauge with current theme.
func NewGauge() *Gauge {
g := &Gauge{
Block: *NewBlock(),
PercentColor: theme.GaugePercent,
BarColor: theme.GaugeBar,
Label: "{{percent}}%",
LabelAlign: AlignCenter,
}
g.Width = 12
g.Height = 5
return g
}
// Buffer implements Bufferer interface.
func (g *Gauge) Buffer() []Point {
ps := g.Block.Buffer()
// plot bar
w := g.Percent * g.innerWidth / 100
for i := 0; i < g.innerHeight; i++ {
for j := 0; j < w; j++ {
p := Point{}
p.X = g.innerX + j
p.Y = g.innerY + i
p.Ch = ' '
p.Bg = g.BarColor
if p.Bg == ColorDefault {
p.Bg |= AttrReverse
}
ps = append(ps, p)
}
}
// plot percentage
s := strings.Replace(g.Label, "{{percent}}", strconv.Itoa(g.Percent), -1)
pry := g.innerY + g.innerHeight/2
rs := str2runes(s)
var pos int
switch g.LabelAlign {
case AlignLeft:
pos = 0
case AlignCenter:
pos = (g.innerWidth - strWidth(s)) / 2
case AlignRight:
pos = g.innerWidth - strWidth(s)
}
for i, v := range rs {
p := Point{}
p.X = 1 + pos + i
p.Y = pry
p.Ch = v
p.Fg = g.PercentColor
if w+g.innerX > pos+i {
p.Bg = g.BarColor
if p.Bg == ColorDefault {
p.Bg |= AttrReverse
}
} else {
p.Bg = g.Block.BgColor
}
ps = append(ps, p)
}
return g.Block.chopOverflow(ps)
}

338
_widget/linechart.go Normal file
View File

@@ -0,0 +1,338 @@
// +build ignore
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"fmt"
"math"
)
// 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}: '⠉',
}
var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
// LineChart has two modes: braille(default) and dot. Using braille gives 2x capicity as dot mode,
// because one braille char can represent two data points.
/*
lc := termui.NewLineChart()
lc.Border.Label = "braille-mode Line Chart"
lc.Data = [1.2, 1.3, 1.5, 1.7, 1.5, 1.6, 1.8, 2.0]
lc.Width = 50
lc.Height = 12
lc.AxesColor = termui.ColorWhite
lc.LineColor = termui.ColorGreen | termui.AttrBold
// termui.Render(lc)...
*/
type LineChart struct {
Block
Data []float64
DataLabels []string // if unset, the data indices will be used
Mode string // braille | dot
DotStyle rune
LineColor Attribute
scale float64 // data span per cell on y-axis
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
}
// NewLineChart returns a new LineChart with current theme.
func NewLineChart() *LineChart {
lc := &LineChart{Block: *NewBlock()}
lc.AxesColor = theme.LineChartAxes
lc.LineColor = theme.LineChartLine
lc.Mode = "braille"
lc.DotStyle = '•'
lc.axisXLebelGap = 2
lc.axisYLebelGap = 1
lc.bottomValue = math.Inf(1)
lc.topValue = math.Inf(-1)
return lc
}
// one cell contains two data points
// so the capicity is 2x as dot-mode
func (lc *LineChart) renderBraille() []Point {
ps := []Point{}
// return: b -> which cell should the point be in
// m -> in the cell, divided into 4 equal height levels, which subcell?
getPos := func(d float64) (b, m int) {
cnt4 := int((d-lc.bottomValue)/(lc.scale/4) + 0.5)
b = cnt4 / 4
m = cnt4 % 4
return
}
// plot points
for i := 0; 2*i+1 < len(lc.Data) && i < lc.axisXWidth; i++ {
b0, m0 := getPos(lc.Data[2*i])
b1, m1 := getPos(lc.Data[2*i+1])
if b0 == b1 {
p := Point{}
p.Ch = braillePatterns[[2]int{m0, m1}]
p.Bg = lc.BgColor
p.Fg = lc.LineColor
p.Y = lc.innerY + lc.innerHeight - 3 - b0
p.X = lc.innerX + lc.labelYSpace + 1 + i
ps = append(ps, p)
} else {
p0 := newPointWithAttrs(lSingleBraille[m0],
lc.innerX+lc.labelYSpace+1+i,
lc.innerY+lc.innerHeight-3-b0,
lc.LineColor,
lc.BgColor)
p1 := newPointWithAttrs(rSingleBraille[m1],
lc.innerX+lc.labelYSpace+1+i,
lc.innerY+lc.innerHeight-3-b1,
lc.LineColor,
lc.BgColor)
ps = append(ps, p0, p1)
}
}
return ps
}
func (lc *LineChart) renderDot() []Point {
ps := []Point{}
for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
p := Point{}
p.Ch = lc.DotStyle
p.Fg = lc.LineColor
p.Bg = lc.BgColor
p.X = lc.innerX + lc.labelYSpace + 1 + i
p.Y = lc.innerY + lc.innerHeight - 3 - int((lc.Data[i]-lc.bottomValue)/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])
w := strWidth(lc.DataLabels[l])
if l+w <= lc.axisXWidth {
lc.labelX = append(lc.labelX, s)
}
l += w + lc.axisXLebelGap
} else { // braille
if 2*l >= len(lc.DataLabels) {
break
}
s := str2runes(lc.DataLabels[2*l])
w := strWidth(lc.DataLabels[2*l])
if l+w <= lc.axisXWidth {
lc.labelX = append(lc.labelX, s)
}
l += w + lc.axisXLebelGap
}
}
}
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() {
// set datalabels if it is not provided
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)
}
}
// lazy increase, to avoid y shaking frequently
// update bound Y when drawing is gonna overflow
lc.minY = lc.Data[0]
lc.maxY = lc.Data[0]
// valid visible range
vrange := lc.innerWidth
if lc.Mode == "braille" {
vrange = 2 * lc.innerWidth
}
if vrange > len(lc.Data) {
vrange = len(lc.Data)
}
for _, v := range lc.Data[:vrange] {
if v > lc.maxY {
lc.maxY = v
}
if v < lc.minY {
lc.minY = v
}
}
span := lc.maxY - lc.minY
if lc.minY < lc.bottomValue {
lc.bottomValue = lc.minY - 0.2*span
}
if lc.maxY > lc.topValue {
lc.topValue = lc.maxY + 0.2*span
}
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{newPointWithAttrs(ORIGIN, origX, origY, lc.AxesColor, lc.BgColor)}
for x := origX + 1; x < origX+lc.axisXWidth; x++ {
p := Point{}
p.X = x
p.Y = origY
p.Bg = lc.BgColor
p.Fg = lc.AxesColor
p.Ch = HDASH
ps = append(ps, p)
}
for dy := 1; dy <= lc.axisYHeight; dy++ {
p := Point{}
p.X = origX
p.Y = origY - dy
p.Bg = lc.BgColor
p.Fg = lc.AxesColor
p.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.Ch = r
p.Fg = lc.AxesColor
p.Bg = 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.Ch = r
p.Fg = lc.AxesColor
p.Bg = lc.BgColor
p.X = lc.innerX + j
p.Y = origY - i*(lc.axisYLebelGap+1)
ps = append(ps, p)
}
}
return ps
}
// Buffer implements Bufferer interface.
func (lc *LineChart) Buffer() []Point {
ps := lc.Block.Buffer()
if lc.Data == nil || len(lc.Data) == 0 {
return ps
}
lc.calcLayout()
ps = append(ps, lc.plotAxes()...)
if lc.Mode == "dot" {
ps = append(ps, lc.renderDot()...)
} else {
ps = append(ps, lc.renderBraille()...)
}
return lc.Block.chopOverflow(ps)
}

View File

@@ -0,0 +1,13 @@
// +build ignore
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build !windows
package termui
const VDASH = '┊'
const HDASH = '┈'
const ORIGIN = '└'

View File

@@ -0,0 +1,13 @@
// +build ignore
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
// +build windows
package termui
const VDASH = '|'
const HDASH = '-'
const ORIGIN = '+'

97
_widget/list.go Normal file
View File

@@ -0,0 +1,97 @@
// +build ignore
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
// List displays []string as its items,
// it has a Overflow option (default is "hidden"), when set to "hidden",
// the item exceeding List's width is truncated, but when set to "wrap",
// the overflowed text breaks into next line.
/*
strs := []string{
"[0] github.com/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"}
ls := termui.NewList()
ls.Items = strs
ls.ItemFgColor = termui.ColorYellow
ls.Border.Label = "List"
ls.Height = 7
ls.Width = 25
ls.Y = 0
*/
type List struct {
Block
Items []string
Overflow string
ItemFgColor Attribute
ItemBgColor Attribute
RendererFactory TextRendererFactory
}
// NewList returns a new *List with current theme.
func NewList() *List {
l := &List{Block: *NewBlock()}
l.Overflow = "hidden"
l.ItemFgColor = theme.ListItemFg
l.ItemBgColor = theme.ListItemBg
l.RendererFactory = PlainRendererFactory{}
return l
}
// Buffer implements Bufferer interface.
func (l *List) Buffer() []Point {
buffer := l.Block.Buffer()
breakLoop := func(y int) bool {
return y+1 > l.innerHeight
}
y := 0
MainLoop:
for _, item := range l.Items {
x := 0
bg, fg := l.ItemFgColor, l.ItemBgColor
renderer := l.RendererFactory.TextRenderer(item)
sequence := renderer.Render(bg, fg)
for n := range []rune(sequence.NormalizedText) {
point, width := sequence.PointAt(n, x+l.innerX, y+l.innerY)
if width+x <= l.innerWidth {
buffer = append(buffer, point)
x += width
} else {
if l.Overflow == "wrap" {
y++
if breakLoop(y) {
break MainLoop
}
x = 0
} else {
dotR := []rune(dot)[0]
dotX := l.innerWidth + l.innerX - charWidth(dotR)
p := newPointWithAttrs(dotR, dotX, y+l.innerY, bg, fg)
buffer = append(buffer, p)
break
}
}
}
y++
if breakLoop(y) {
break MainLoop
}
}
return l.Block.chopOverflow(buffer)
}

233
_widget/mbar.go Normal file
View File

@@ -0,0 +1,233 @@
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import (
"fmt"
)
// This is the implemetation of multi-colored or stacked bar graph. This is different from default barGraph which is implemented in bar.go
// Multi-Colored-BarChart creates multiple bars in a widget:
/*
bc := termui.NewMBarChart()
data := make([][]int, 2)
data[0] := []int{3, 2, 5, 7, 9, 4}
data[1] := []int{7, 8, 5, 3, 1, 6}
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
bc.Border.Label = "Bar Chart"
bc.Data = data
bc.Width = 26
bc.Height = 10
bc.DataLabels = bclabels
bc.TextColor = termui.ColorGreen
bc.BarColor = termui.ColorRed
bc.NumColor = termui.ColorYellow
*/
type MBarChart struct {
Block
BarColor [NumberofColors]Attribute
TextColor Attribute
NumColor [NumberofColors]Attribute
Data [NumberofColors][]int
DataLabels []string
BarWidth int
BarGap int
labels [][]rune
dataNum [NumberofColors][][]rune
numBar int
scale float64
max int
minDataLen int
numStack int
ShowScale bool
maxScale []rune
}
// NewBarChart returns a new *BarChart with current theme.
func NewMBarChart() *MBarChart {
bc := &MBarChart{Block: *NewBlock()}
bc.BarColor[0] = theme.MBarChartBar
bc.NumColor[0] = theme.MBarChartNum
bc.TextColor = theme.MBarChartText
bc.BarGap = 1
bc.BarWidth = 3
return bc
}
func (bc *MBarChart) layout() {
bc.numBar = bc.innerWidth / (bc.BarGap + bc.BarWidth)
bc.labels = make([][]rune, bc.numBar)
DataLen := 0
LabelLen := len(bc.DataLabels)
bc.minDataLen = 9999 //Set this to some very hight value so that we find the minimum one We want to know which array among data[][] has got the least length
// We need to know how many stack/data array data[0] , data[1] are there
for i := 0; i < len(bc.Data); i++ {
if bc.Data[i] == nil {
break
}
DataLen++
}
bc.numStack = DataLen
//We need to know what is the mimimum size of data array data[0] could have 10 elements data[1] could have only 5, so we plot only 5 bar graphs
for i := 0; i < DataLen; i++ {
if bc.minDataLen > len(bc.Data[i]) {
bc.minDataLen = len(bc.Data[i])
}
}
if LabelLen > bc.minDataLen {
LabelLen = bc.minDataLen
}
for i := 0; i < LabelLen && i < bc.numBar; i++ {
bc.labels[i] = trimStr2Runes(bc.DataLabels[i], bc.BarWidth)
}
for i := 0; i < bc.numStack; i++ {
bc.dataNum[i] = make([][]rune, len(bc.Data[i]))
//For each stack of bar calcualte the rune
for j := 0; j < LabelLen && i < bc.numBar; j++ {
n := bc.Data[i][j]
s := fmt.Sprint(n)
bc.dataNum[i][j] = trimStr2Runes(s, bc.BarWidth)
}
//If color is not defined by default then populate a color that is different from the prevous bar
if bc.BarColor[i] == ColorDefault && bc.NumColor[i] == ColorDefault {
if i == 0 {
bc.BarColor[i] = ColorBlack
} else {
bc.BarColor[i] = bc.BarColor[i-1] + 1
if bc.BarColor[i] > NumberofColors {
bc.BarColor[i] = ColorBlack
}
}
bc.NumColor[i] = (NumberofColors + 1) - bc.BarColor[i] //Make NumColor opposite of barColor for visibility
}
}
//If Max value is not set then we have to populate, this time the max value will be max(sum(d1[0],d2[0],d3[0]) .... sum(d1[n], d2[n], d3[n]))
if bc.max == 0 {
bc.max = -1
}
for i := 0; i < bc.minDataLen && i < LabelLen; i++ {
var dsum int
for j := 0; j < bc.numStack; j++ {
dsum += bc.Data[j][i]
}
if dsum > bc.max {
bc.max = dsum
}
}
//Finally Calculate max sale
if bc.ShowScale {
s := fmt.Sprintf("%d", bc.max)
bc.maxScale = trimStr2Runes(s, len(s))
bc.scale = float64(bc.max) / float64(bc.innerHeight-2)
} else {
bc.scale = float64(bc.max) / float64(bc.innerHeight-1)
}
}
func (bc *MBarChart) SetMax(max int) {
if max > 0 {
bc.max = max
}
}
// Buffer implements Bufferer interface.
func (bc *MBarChart) Buffer() []Point {
ps := bc.Block.Buffer()
bc.layout()
var oftX int
for i := 0; i < bc.numBar && i < bc.minDataLen && i < len(bc.DataLabels); i++ {
ph := 0 //Previous Height to stack up
oftX = i * (bc.BarWidth + bc.BarGap)
for i1 := 0; i1 < bc.numStack; i1++ {
h := int(float64(bc.Data[i1][i]) / bc.scale)
// plot bars
for j := 0; j < bc.BarWidth; j++ {
for k := 0; k < h; k++ {
p := Point{}
p.Ch = ' '
p.Bg = bc.BarColor[i1]
if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
p.Bg |= AttrReverse
}
p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j
p.Y = bc.innerY + bc.innerHeight - 2 - k - ph
ps = append(ps, p)
}
}
ph += h
}
// plot text
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
w := charWidth(bc.labels[i][j])
p := Point{}
p.Ch = bc.labels[i][j]
p.Bg = bc.BgColor
p.Fg = bc.TextColor
p.Y = bc.innerY + bc.innerHeight - 1
p.X = bc.innerX + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k
ps = append(ps, p)
k += w
}
// plot num
ph = 0 //re-initialize previous height
for i1 := 0; i1 < bc.numStack; i1++ {
h := int(float64(bc.Data[i1][i]) / bc.scale)
for j := 0; j < len(bc.dataNum[i1][i]) && h > 0; j++ {
p := Point{}
p.Ch = bc.dataNum[i1][i][j]
p.Fg = bc.NumColor[i1]
p.Bg = bc.BarColor[i1]
if bc.BarColor[i1] == ColorDefault { // the same as above
p.Bg |= AttrReverse
}
if h == 0 {
p.Bg = bc.BgColor
}
p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j
p.Y = bc.innerY + bc.innerHeight - 2 - ph
ps = append(ps, p)
}
ph += h
}
}
if bc.ShowScale {
//Currently bar graph only supprts data range from 0 to MAX
//Plot 0
p := Point{}
p.Ch = '0'
p.Bg = bc.BgColor
p.Fg = bc.TextColor
p.Y = bc.innerY + bc.innerHeight - 2
p.X = bc.X
ps = append(ps, p)
//Plot the maximum sacle value
for i := 0; i < len(bc.maxScale); i++ {
p := Point{}
p.Ch = bc.maxScale[i]
p.Bg = bc.BgColor
p.Fg = bc.TextColor
p.Y = bc.innerY
p.X = bc.X + i
ps = append(ps, p)
}
}
return bc.Block.chopOverflow(ps)
}

71
_widget/par.go Normal file
View File

@@ -0,0 +1,71 @@
// +build ignore
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
// Par displays a paragraph.
/*
par := termui.NewPar("Simple Text")
par.Height = 3
par.Width = 17
par.Border.Label = "Label"
*/
type Par struct {
Block
Text string
TextFgColor Attribute
TextBgColor Attribute
RendererFactory TextRendererFactory
}
// NewPar returns a new *Par with given text as its content.
func NewPar(s string) *Par {
return &Par{
Block: *NewBlock(),
Text: s,
TextFgColor: theme.ParTextFg,
TextBgColor: theme.ParTextBg,
RendererFactory: PlainRendererFactory{},
}
}
// Buffer implements Bufferer interface.
func (p *Par) Buffer() []Point {
ps := p.Block.Buffer()
fg, bg := p.TextFgColor, p.TextBgColor
sequence := p.RendererFactory.TextRenderer(p.Text).Render(fg, bg)
runes := []rune(sequence.NormalizedText)
y, x, n := 0, 0, 0
for y < p.innerHeight && n < len(runes) {
point, width := sequence.PointAt(n, x+p.innerX, y+p.innerY)
if runes[n] == '\n' || x+width > p.innerWidth {
y++
x = 0 // set x = 0
if runes[n] == '\n' {
n++
}
if y >= p.innerHeight {
ps = append(ps, newPointWithAttrs('…',
p.innerX+p.innerWidth-1,
p.innerY+p.innerHeight-1,
p.TextFgColor, p.TextBgColor))
break
}
continue
}
ps = append(ps, point)
n++
x += width
}
return p.Block.chopOverflow(ps)
}

158
_widget/sparkline.go Normal file
View File

@@ -0,0 +1,158 @@
// +build ignore
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package termui
import "math"
// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃
/*
data := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1}
spl := termui.NewSparkline()
spl.Data = data
spl.Title = "Sparkline 0"
spl.LineColor = termui.ColorGreen
*/
type Sparkline struct {
Data []int
Height int
Title string
TitleColor Attribute
LineColor Attribute
displayHeight int
scale float32
max int
}
// Sparklines is a renderable widget which groups together the given sparklines.
/*
spls := termui.NewSparklines(spl0,spl1,spl2) //...
spls.Height = 2
spls.Width = 20
*/
type Sparklines struct {
Block
Lines []Sparkline
displayLines int
displayWidth int
}
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
// Add appends a given Sparkline to s *Sparklines.
func (s *Sparklines) Add(sl Sparkline) {
s.Lines = append(s.Lines, sl)
}
// NewSparkline returns a unrenderable single sparkline that intended to be added into Sparklines.
func NewSparkline() Sparkline {
return Sparkline{
Height: 1,
TitleColor: theme.SparklineTitle,
LineColor: theme.SparklineLine}
}
// NewSparklines return a new *Spaklines with given Sparkline(s), you can always add a new Sparkline later.
func NewSparklines(ss ...Sparkline) *Sparklines {
s := &Sparklines{Block: *NewBlock(), Lines: ss}
return s
}
func (sl *Sparklines) update() {
for i, v := range sl.Lines {
if v.Title == "" {
sl.Lines[i].displayHeight = v.Height
} else {
sl.Lines[i].displayHeight = v.Height + 1
}
}
sl.displayWidth = sl.innerWidth
// get how many lines gotta display
h := 0
sl.displayLines = 0
for _, v := range sl.Lines {
if h+v.displayHeight <= sl.innerHeight {
sl.displayLines++
} else {
break
}
h += v.displayHeight
}
for i := 0; i < sl.displayLines; i++ {
data := sl.Lines[i].Data
max := math.MinInt32
for _, v := range data {
if max < v {
max = v
}
}
sl.Lines[i].max = max
sl.Lines[i].scale = float32(8*sl.Lines[i].Height) / float32(max)
}
}
// Buffer implements Bufferer interface.
func (sl *Sparklines) Buffer() []Point {
ps := sl.Block.Buffer()
sl.update()
oftY := 0
for i := 0; i < sl.displayLines; i++ {
l := sl.Lines[i]
data := l.Data
if len(data) > sl.innerWidth {
data = data[len(data)-sl.innerWidth:]
}
if l.Title != "" {
rs := trimStr2Runes(l.Title, sl.innerWidth)
oftX := 0
for _, v := range rs {
w := charWidth(v)
p := Point{}
p.Ch = v
p.Fg = l.TitleColor
p.Bg = sl.BgColor
p.X = sl.innerX + oftX
p.Y = sl.innerY + oftY
ps = append(ps, p)
oftX += w
}
}
for j, v := range data {
h := int(float32(v)*l.scale + 0.5)
barCnt := h / 8
barMod := h % 8
for jj := 0; jj < barCnt; jj++ {
p := Point{}
p.X = sl.innerX + j
p.Y = sl.innerY + oftY + l.Height - jj
p.Ch = ' ' // => sparks[7]
p.Bg = l.LineColor
//p.Bg = sl.BgColor
ps = append(ps, p)
}
if barMod != 0 {
p := Point{}
p.X = sl.innerX + j
p.Y = sl.innerY + oftY + l.Height - barCnt
p.Ch = sparks[barMod-1]
p.Fg = l.LineColor
p.Bg = sl.BgColor
ps = append(ps, p)
}
}
oftY += l.displayHeight
}
return sl.Block.chopOverflow(ps)
}