The Great Rewrite
This commit is contained in:
89
widgets/barchart.go
Normal file
89
widgets/barchart.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
rw "github.com/mattn/go-runewidth"
|
||||
|
||||
. "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type BarChart struct {
|
||||
Block
|
||||
BarColors []Color
|
||||
LabelStyles []Style
|
||||
NumStyles []Style // only Fg and Modifier are used
|
||||
NumFmt func(float64) string
|
||||
Data []float64
|
||||
Labels []string
|
||||
BarWidth int
|
||||
BarGap int
|
||||
MaxVal float64
|
||||
}
|
||||
|
||||
func NewBarChart() *BarChart {
|
||||
return &BarChart{
|
||||
Block: *NewBlock(),
|
||||
BarColors: Theme.BarChart.Bars,
|
||||
NumStyles: Theme.BarChart.Nums,
|
||||
LabelStyles: Theme.BarChart.Labels,
|
||||
NumFmt: func(n float64) string { return fmt.Sprint(n) },
|
||||
BarGap: 1,
|
||||
BarWidth: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *BarChart) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
maxVal := self.MaxVal
|
||||
if maxVal == 0 {
|
||||
maxVal, _ = GetMaxFloat64FromSlice(self.Data)
|
||||
}
|
||||
|
||||
barXCoordinate := self.Inner.Min.X
|
||||
|
||||
for i, data := range self.Data {
|
||||
// draw bar
|
||||
height := int((data / maxVal) * float64(self.Inner.Dy()-1))
|
||||
for x := barXCoordinate; x < MinInt(barXCoordinate+self.BarWidth, self.Inner.Max.X); x++ {
|
||||
for y := self.Inner.Max.Y - 2; y > (self.Inner.Max.Y-2)-height; y-- {
|
||||
c := NewCell(' ', NewStyle(ColorClear, SelectColor(self.BarColors, i)))
|
||||
buf.SetCell(c, image.Pt(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
// draw label
|
||||
if i < len(self.Labels) {
|
||||
labelXCoordinate := barXCoordinate +
|
||||
int((float64(self.BarWidth) / 2)) -
|
||||
int((float64(rw.StringWidth(self.Labels[i])) / 2))
|
||||
buf.SetString(
|
||||
self.Labels[i],
|
||||
SelectStyle(self.LabelStyles, i),
|
||||
image.Pt(labelXCoordinate, self.Inner.Max.Y-1),
|
||||
)
|
||||
}
|
||||
|
||||
// draw number
|
||||
numberXCoordinate := barXCoordinate + int((float64(self.BarWidth) / 2))
|
||||
if numberXCoordinate <= self.Inner.Max.X {
|
||||
buf.SetString(
|
||||
self.NumFmt(data),
|
||||
NewStyle(
|
||||
SelectStyle(self.NumStyles, i+1).Fg,
|
||||
SelectColor(self.BarColors, i),
|
||||
SelectStyle(self.NumStyles, i+1).Modifier,
|
||||
),
|
||||
image.Pt(numberXCoordinate, self.Inner.Max.Y-2),
|
||||
)
|
||||
}
|
||||
|
||||
barXCoordinate += (self.BarWidth + self.BarGap)
|
||||
}
|
||||
}
|
||||
57
widgets/gauge.go
Normal file
57
widgets/gauge.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type Gauge struct {
|
||||
Block
|
||||
Percent int
|
||||
BarColor Color
|
||||
Label string
|
||||
LabelStyle Style
|
||||
}
|
||||
|
||||
func NewGauge() *Gauge {
|
||||
return &Gauge{
|
||||
Block: *NewBlock(),
|
||||
BarColor: Theme.Gauge.Bar,
|
||||
LabelStyle: Theme.Gauge.Label,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Gauge) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
label := self.Label
|
||||
if label == "" {
|
||||
label = fmt.Sprintf("%d%%", self.Percent)
|
||||
}
|
||||
|
||||
// plot bar
|
||||
barWidth := int((float64(self.Percent) / 100) * float64(self.Inner.Dx()))
|
||||
buf.Fill(
|
||||
NewCell(' ', NewStyle(ColorClear, self.BarColor)),
|
||||
image.Rect(self.Inner.Min.X, self.Inner.Min.Y, self.Inner.Min.X+barWidth, self.Inner.Max.Y),
|
||||
)
|
||||
|
||||
// plot label
|
||||
labelXCoordinate := self.Inner.Min.X + (self.Inner.Dx() / 2) - int(float64(len(label))/2)
|
||||
labelYCoordinate := self.Inner.Min.Y + ((self.Inner.Dy() - 1) / 2)
|
||||
if labelYCoordinate < self.Inner.Max.Y {
|
||||
for i, char := range label {
|
||||
style := self.LabelStyle
|
||||
if labelXCoordinate+i+1 <= self.Inner.Min.X+barWidth {
|
||||
style = NewStyle(self.BarColor, ColorClear, ModifierReverse)
|
||||
}
|
||||
buf.SetCell(NewCell(char, style), image.Pt(labelXCoordinate+i, labelYCoordinate))
|
||||
}
|
||||
}
|
||||
}
|
||||
181
widgets/linechart.go
Normal file
181
widgets/linechart.go
Normal file
@@ -0,0 +1,181 @@
|
||||
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
// LineChart has two modes: braille(default) and dot.
|
||||
// A single braille character is a 2x4 grid of dots, so using braille
|
||||
// gives 2x X resolution and 4x Y resolution over dot mode.
|
||||
type LineChart struct {
|
||||
Block
|
||||
Data [][]float64
|
||||
DataLabels []string
|
||||
HorizontalScale int
|
||||
LineType LineType
|
||||
DotChar rune
|
||||
LineColors []Color
|
||||
AxesColor Color // TODO
|
||||
MaxVal float64
|
||||
ShowAxes bool
|
||||
DrawDirection DrawDirection // TODO
|
||||
}
|
||||
|
||||
const (
|
||||
xAxisLabelsHeight = 1
|
||||
yAxisLabelsWidth = 4
|
||||
xAxisLabelsGap = 2
|
||||
yAxisLabelsGap = 1
|
||||
)
|
||||
|
||||
type LineType int
|
||||
|
||||
const (
|
||||
BrailleLine LineType = iota
|
||||
DotLine
|
||||
)
|
||||
|
||||
type DrawDirection uint
|
||||
|
||||
const (
|
||||
DrawLeft DrawDirection = iota
|
||||
DrawRight
|
||||
)
|
||||
|
||||
func NewLineChart() *LineChart {
|
||||
return &LineChart{
|
||||
Block: *NewBlock(),
|
||||
LineColors: Theme.LineChart.Lines,
|
||||
AxesColor: Theme.LineChart.Axes,
|
||||
LineType: BrailleLine,
|
||||
DotChar: DOT,
|
||||
Data: [][]float64{},
|
||||
HorizontalScale: 1,
|
||||
DrawDirection: DrawRight,
|
||||
ShowAxes: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LineChart) renderBraille(buf *Buffer, drawArea image.Rectangle, maxVal float64) {
|
||||
canvas := NewCanvas()
|
||||
canvas.Rectangle = drawArea
|
||||
|
||||
for i, line := range self.Data {
|
||||
previousHeight := int((line[1] / maxVal) * float64(drawArea.Dy()-1))
|
||||
for j, val := range line[1:] {
|
||||
height := int((val / maxVal) * float64(drawArea.Dy()-1))
|
||||
canvas.Line(
|
||||
image.Pt(
|
||||
(drawArea.Min.X+(j*self.HorizontalScale))*2,
|
||||
(drawArea.Max.Y-previousHeight-1)*4,
|
||||
),
|
||||
image.Pt(
|
||||
(drawArea.Min.X+((j+1)*self.HorizontalScale))*2,
|
||||
(drawArea.Max.Y-height-1)*4,
|
||||
),
|
||||
SelectColor(self.LineColors, i),
|
||||
)
|
||||
previousHeight = height
|
||||
}
|
||||
}
|
||||
|
||||
canvas.Draw(buf)
|
||||
}
|
||||
|
||||
func (self *LineChart) renderDot(buf *Buffer, drawArea image.Rectangle, maxVal float64) {
|
||||
for i, line := range self.Data {
|
||||
for j := 0; j < len(line) && j*self.HorizontalScale < drawArea.Dx(); j++ {
|
||||
val := line[j]
|
||||
height := int((val / maxVal) * float64(drawArea.Dy()-1))
|
||||
buf.SetCell(
|
||||
NewCell(self.DotChar, NewStyle(SelectColor(self.LineColors, i))),
|
||||
image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LineChart) plotAxes(buf *Buffer, maxVal float64) {
|
||||
// draw origin cell
|
||||
buf.SetCell(
|
||||
NewCell(BOTTOM_LEFT, NewStyle(ColorWhite)),
|
||||
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-xAxisLabelsHeight-1),
|
||||
)
|
||||
// draw x axis line
|
||||
for i := yAxisLabelsWidth + 1; i < self.Inner.Dx(); i++ {
|
||||
buf.SetCell(
|
||||
NewCell(HORIZONTAL_DASH, NewStyle(ColorWhite)),
|
||||
image.Pt(i+self.Inner.Min.X, self.Inner.Max.Y-xAxisLabelsHeight-1),
|
||||
)
|
||||
}
|
||||
// draw y axis line
|
||||
for i := 0; i < self.Inner.Dy()-xAxisLabelsHeight-1; i++ {
|
||||
buf.SetCell(
|
||||
NewCell(VERTICAL_DASH, NewStyle(ColorWhite)),
|
||||
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, i+self.Inner.Min.Y),
|
||||
)
|
||||
}
|
||||
// draw x axis labels
|
||||
// draw 0
|
||||
buf.SetString(
|
||||
"0",
|
||||
NewStyle(ColorWhite),
|
||||
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-1),
|
||||
)
|
||||
// draw rest
|
||||
for x := self.Inner.Min.X + yAxisLabelsWidth + (xAxisLabelsGap)*self.HorizontalScale + 1; x < self.Inner.Max.X-1; {
|
||||
label := fmt.Sprintf(
|
||||
"%d",
|
||||
(x-(self.Inner.Min.X+yAxisLabelsWidth)-1)/(self.HorizontalScale)+1,
|
||||
)
|
||||
buf.SetString(
|
||||
label,
|
||||
NewStyle(ColorWhite),
|
||||
image.Pt(x, self.Inner.Max.Y-1),
|
||||
)
|
||||
x += (len(label) + xAxisLabelsGap) * self.HorizontalScale
|
||||
}
|
||||
// draw y axis labels
|
||||
verticalScale := maxVal / float64(self.Inner.Dy()-xAxisLabelsHeight-1)
|
||||
for i := 0; i*(yAxisLabelsGap+1) < self.Inner.Dy()-1; i++ {
|
||||
buf.SetString(
|
||||
fmt.Sprintf("%.2f", float64(i)*verticalScale*(yAxisLabelsGap+1)),
|
||||
NewStyle(ColorWhite),
|
||||
image.Pt(self.Inner.Min.X, self.Inner.Max.Y-(i*(yAxisLabelsGap+1))-2),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LineChart) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
maxVal := self.MaxVal
|
||||
if maxVal == 0 {
|
||||
maxVal, _ = GetMaxFloat64From2dSlice(self.Data)
|
||||
}
|
||||
|
||||
if self.ShowAxes {
|
||||
self.plotAxes(buf, maxVal)
|
||||
}
|
||||
|
||||
drawArea := self.Inner
|
||||
if self.ShowAxes {
|
||||
drawArea = image.Rect(
|
||||
self.Inner.Min.X+yAxisLabelsWidth+1, self.Inner.Min.Y,
|
||||
self.Inner.Max.X, self.Inner.Max.Y-xAxisLabelsHeight-1,
|
||||
)
|
||||
}
|
||||
|
||||
if self.LineType == BrailleLine {
|
||||
self.renderBraille(buf, drawArea, maxVal)
|
||||
} else {
|
||||
self.renderDot(buf, drawArea, maxVal)
|
||||
}
|
||||
}
|
||||
52
widgets/list.go
Normal file
52
widgets/list.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type List struct {
|
||||
Block
|
||||
Rows []string
|
||||
Wrap bool
|
||||
TextStyle Style
|
||||
}
|
||||
|
||||
func NewList() *List {
|
||||
return &List{
|
||||
Block: *NewBlock(),
|
||||
TextStyle: Theme.List.Text,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *List) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
point := self.Inner.Min
|
||||
|
||||
for row := 0; row < len(self.Rows) && point.Y < self.Inner.Max.Y; row++ {
|
||||
cells := ParseText(self.Rows[row], self.TextStyle)
|
||||
if self.Wrap {
|
||||
cells = WrapCells(cells, uint(self.Inner.Dx()))
|
||||
}
|
||||
for j := 0; j < len(cells) && point.Y < self.Inner.Max.Y; j++ {
|
||||
if cells[j].Rune == '\n' {
|
||||
point = image.Pt(self.Inner.Min.X, point.Y+1)
|
||||
} else {
|
||||
if point.X+1 == self.Inner.Max.X+1 && len(cells) > self.Inner.Dx() {
|
||||
buf.SetCell(NewCell(DOTS, cells[j].Style), point.Add(image.Pt(-1, 0)))
|
||||
break
|
||||
} else {
|
||||
buf.SetCell(cells[j], point)
|
||||
point = point.Add(image.Pt(1, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
point = image.Pt(self.Inner.Min.X, point.Y+1)
|
||||
}
|
||||
}
|
||||
40
widgets/paragraph.go
Normal file
40
widgets/paragraph.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type Paragraph struct {
|
||||
Block
|
||||
Text string
|
||||
TextStyle Style
|
||||
}
|
||||
|
||||
func NewParagraph() *Paragraph {
|
||||
return &Paragraph{
|
||||
Block: *NewBlock(),
|
||||
TextStyle: Theme.Paragraph.Text,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Paragraph) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
point := self.Inner.Min
|
||||
cells := WrapCells(ParseText(self.Text, self.TextStyle), uint(self.Inner.Dx()))
|
||||
|
||||
for i := 0; i < len(cells) && point.Y < self.Inner.Max.Y; i++ {
|
||||
if cells[i].Rune == '\n' {
|
||||
point = image.Pt(self.Inner.Min.X, point.Y+1)
|
||||
} else {
|
||||
buf.SetCell(cells[i], point)
|
||||
point = point.Add(image.Pt(1, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
149
widgets/piechart.go
Normal file
149
widgets/piechart.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
|
||||
. "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
const (
|
||||
piechartOffsetUp = -.5 * math.Pi // the northward angle
|
||||
resolutionFactor = .0001 // circle resolution: precision vs. performance
|
||||
fullCircle = 2.0 * math.Pi // the full circle angle
|
||||
xStretch = 2.0 // horizontal adjustment
|
||||
)
|
||||
|
||||
// PieChartLabel callback
|
||||
type PieChartLabel func(dataIndex int, currentValue float64) string
|
||||
|
||||
type PieChart struct {
|
||||
Block
|
||||
Data []float64 // list of data items
|
||||
Colors []Color // colors to by cycled through
|
||||
Label PieChartLabel // callback function for labels
|
||||
Offset float64 // which angle to start drawing at? (see piechartOffsetUp)
|
||||
}
|
||||
|
||||
// NewPieChart Creates a new pie chart with reasonable defaults and no labels.
|
||||
func NewPieChart() *PieChart {
|
||||
return &PieChart{
|
||||
Block: *NewBlock(),
|
||||
Colors: Theme.PieChart.Slices,
|
||||
Offset: piechartOffsetUp,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PieChart) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
center := self.Inner.Min.Add(self.Inner.Size().Div(2))
|
||||
radius := MinFloat64(float64(self.Inner.Dx()/2/xStretch), float64(self.Inner.Dy()/2))
|
||||
|
||||
// compute slice sizes
|
||||
sum := SumFloat64Slice(self.Data)
|
||||
sliceSizes := make([]float64, len(self.Data))
|
||||
for i, v := range self.Data {
|
||||
sliceSizes[i] = v / sum * fullCircle
|
||||
}
|
||||
|
||||
borderCircle := &circle{center, radius}
|
||||
middleCircle := circle{Point: center, radius: radius / 2.0}
|
||||
|
||||
// draw sectors
|
||||
phi := self.Offset
|
||||
for i, size := range sliceSizes {
|
||||
for j := 0.0; j < size; j += resolutionFactor {
|
||||
borderPoint := borderCircle.at(phi + j)
|
||||
line := line{P1: center, P2: borderPoint}
|
||||
line.draw(NewCell(SHADED_BLOCK, NewStyle(SelectColor(self.Colors, i))), buf)
|
||||
}
|
||||
phi += size
|
||||
}
|
||||
|
||||
// draw labels
|
||||
if self.Label != nil {
|
||||
phi = self.Offset
|
||||
for i, size := range sliceSizes {
|
||||
labelPoint := middleCircle.at(phi + size/2.0)
|
||||
if len(self.Data) == 1 {
|
||||
labelPoint = center
|
||||
}
|
||||
buf.SetString(
|
||||
self.Label(i, self.Data[i]),
|
||||
NewStyle(SelectColor(self.Colors, i)),
|
||||
image.Pt(labelPoint.X, labelPoint.Y),
|
||||
)
|
||||
phi += size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type circle struct {
|
||||
image.Point
|
||||
radius float64
|
||||
}
|
||||
|
||||
// computes the point at a given angle phi
|
||||
func (self circle) at(phi float64) image.Point {
|
||||
x := self.X + int(RoundFloat64(xStretch*self.radius*math.Cos(phi)))
|
||||
y := self.Y + int(RoundFloat64(self.radius*math.Sin(phi)))
|
||||
return image.Point{X: x, Y: y}
|
||||
}
|
||||
|
||||
// computes the perimeter of a circle
|
||||
func (self circle) perimeter() float64 {
|
||||
return 2.0 * math.Pi * self.radius
|
||||
}
|
||||
|
||||
// a line between two points
|
||||
type line struct {
|
||||
P1, P2 image.Point
|
||||
}
|
||||
|
||||
// draws the line
|
||||
func (self line) draw(cell Cell, buf *Buffer) {
|
||||
isLeftOf := func(p1, p2 image.Point) bool {
|
||||
return p1.X <= p2.X
|
||||
}
|
||||
isTopOf := func(p1, p2 image.Point) bool {
|
||||
return p1.Y <= p2.Y
|
||||
}
|
||||
p1, p2 := self.P1, self.P2
|
||||
buf.SetCell(NewCell('*', cell.Style), self.P2)
|
||||
width, height := self.size()
|
||||
if width > height { // paint left to right
|
||||
if !isLeftOf(p1, p2) {
|
||||
p1, p2 = p2, p1
|
||||
}
|
||||
flip := 1.0
|
||||
if !isTopOf(p1, p2) {
|
||||
flip = -1.0
|
||||
}
|
||||
for x := p1.X; x <= p2.X; x++ {
|
||||
ratio := float64(height) / float64(width)
|
||||
factor := float64(x - p1.X)
|
||||
y := ratio * factor * flip
|
||||
buf.SetCell(cell, image.Pt(x, int(RoundFloat64(y))+p1.Y))
|
||||
}
|
||||
} else { // paint top to bottom
|
||||
if !isTopOf(p1, p2) {
|
||||
p1, p2 = p2, p1
|
||||
}
|
||||
flip := 1.0
|
||||
if !isLeftOf(p1, p2) {
|
||||
flip = -1.0
|
||||
}
|
||||
for y := p1.Y; y <= p2.Y; y++ {
|
||||
ratio := float64(width) / float64(height)
|
||||
factor := float64(y - p1.Y)
|
||||
x := ratio * factor * flip
|
||||
buf.SetCell(cell, image.Pt(int(RoundFloat64(x))+p1.X, y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// width and height of a line
|
||||
func (self line) size() (w, h int) {
|
||||
return AbsInt(self.P2.X - self.P1.X), AbsInt(self.P2.Y - self.P1.Y)
|
||||
}
|
||||
94
widgets/sparkline.go
Normal file
94
widgets/sparkline.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers.
|
||||
type Sparkline struct {
|
||||
Data []float64
|
||||
Title string
|
||||
TitleStyle Style
|
||||
LineColor Color
|
||||
MaxVal float64
|
||||
MaxHeight int // TODO
|
||||
}
|
||||
|
||||
// SparklineGroup is a renderable widget which groups together the given sparklines.
|
||||
type SparklineGroup struct {
|
||||
Block
|
||||
Sparklines []*Sparkline
|
||||
}
|
||||
|
||||
// NewSparkline returns a unrenderable single sparkline that needs to be added to a SparklineGroup
|
||||
func NewSparkline() *Sparkline {
|
||||
return &Sparkline{
|
||||
TitleStyle: Theme.Sparkline.Title,
|
||||
LineColor: Theme.Sparkline.Line,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSparklineGroup(sls ...*Sparkline) *SparklineGroup {
|
||||
return &SparklineGroup{
|
||||
Block: *NewBlock(),
|
||||
Sparklines: sls,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SparklineGroup) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
sparklineHeight := self.Inner.Dy() / len(self.Sparklines)
|
||||
|
||||
for i, sl := range self.Sparklines {
|
||||
heightOffset := (sparklineHeight * (i + 1))
|
||||
barHeight := sparklineHeight
|
||||
if i == len(self.Sparklines)-1 {
|
||||
heightOffset = self.Inner.Dy()
|
||||
barHeight = self.Inner.Dy() - (sparklineHeight * i)
|
||||
}
|
||||
if sl.Title != "" {
|
||||
barHeight--
|
||||
}
|
||||
|
||||
maxVal := sl.MaxVal
|
||||
if maxVal == 0 {
|
||||
maxVal, _ = GetMaxFloat64FromSlice(sl.Data)
|
||||
}
|
||||
|
||||
// draw line
|
||||
for j := 0; j < len(sl.Data) && j < self.Inner.Dx(); j++ {
|
||||
data := sl.Data[j]
|
||||
height := int((data / maxVal) * float64(barHeight))
|
||||
sparkChar := BARS[len(BARS)-1]
|
||||
for k := 0; k < height; k++ {
|
||||
buf.SetCell(
|
||||
NewCell(sparkChar, NewStyle(sl.LineColor)),
|
||||
image.Pt(j+self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset-k),
|
||||
)
|
||||
}
|
||||
if height == 0 {
|
||||
sparkChar = BARS[0]
|
||||
buf.SetCell(
|
||||
NewCell(sparkChar, NewStyle(sl.LineColor)),
|
||||
image.Pt(j+self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if sl.Title != "" {
|
||||
// draw title
|
||||
buf.SetString(
|
||||
TrimString(sl.Title, self.Inner.Dx()),
|
||||
sl.TitleStyle,
|
||||
image.Pt(self.Inner.Min.X, self.Inner.Min.Y-1+heightOffset-barHeight),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
94
widgets/stacked_barchart.go
Normal file
94
widgets/stacked_barchart.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
rw "github.com/mattn/go-runewidth"
|
||||
|
||||
. "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type StackedBarChart struct {
|
||||
Block
|
||||
BarColors []Color
|
||||
LabelStyles []Style
|
||||
NumStyles []Style // only Fg and Modifier are used
|
||||
NumFmt func(float64) string
|
||||
Data [][]float64
|
||||
Labels []string
|
||||
BarWidth int
|
||||
BarGap int
|
||||
MaxVal float64
|
||||
}
|
||||
|
||||
func NewStackedBarChart() *StackedBarChart {
|
||||
return &StackedBarChart{
|
||||
Block: *NewBlock(),
|
||||
BarColors: Theme.StackedBarChart.Bars,
|
||||
LabelStyles: Theme.StackedBarChart.Labels,
|
||||
NumStyles: Theme.StackedBarChart.Nums,
|
||||
NumFmt: func(n float64) string { return fmt.Sprint(n) },
|
||||
BarGap: 1,
|
||||
BarWidth: 3,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *StackedBarChart) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
maxVal := self.MaxVal
|
||||
if maxVal == 0 {
|
||||
for _, data := range self.Data {
|
||||
maxVal = MaxFloat64(maxVal, SumFloat64Slice(data))
|
||||
}
|
||||
}
|
||||
|
||||
barXCoordinate := self.Inner.Min.X
|
||||
|
||||
for i, bar := range self.Data {
|
||||
// draw stacked bars
|
||||
stackedBarYCoordinate := 0
|
||||
for j, data := range bar {
|
||||
// draw each stacked bar
|
||||
height := int((data / maxVal) * float64(self.Inner.Dy()-1))
|
||||
for x := barXCoordinate; x < MinInt(barXCoordinate+self.BarWidth, self.Inner.Max.X); x++ {
|
||||
for y := (self.Inner.Max.Y - 2) - stackedBarYCoordinate; y > (self.Inner.Max.Y-2)-stackedBarYCoordinate-height; y-- {
|
||||
c := NewCell(' ', NewStyle(ColorClear, SelectColor(self.BarColors, j)))
|
||||
buf.SetCell(c, image.Pt(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
// draw number
|
||||
numberXCoordinate := barXCoordinate + int((float64(self.BarWidth) / 2)) - 1
|
||||
buf.SetString(
|
||||
self.NumFmt(data),
|
||||
NewStyle(
|
||||
SelectStyle(self.NumStyles, j+1).Fg,
|
||||
SelectColor(self.BarColors, j),
|
||||
SelectStyle(self.NumStyles, j+1).Modifier,
|
||||
),
|
||||
image.Pt(numberXCoordinate, (self.Inner.Max.Y-2)-stackedBarYCoordinate),
|
||||
)
|
||||
|
||||
stackedBarYCoordinate += height
|
||||
}
|
||||
|
||||
// draw label
|
||||
labelXCoordinate := barXCoordinate + MaxInt(
|
||||
int((float64(self.BarWidth)/2))-int((float64(rw.StringWidth(self.Labels[i]))/2)),
|
||||
0,
|
||||
)
|
||||
buf.SetString(
|
||||
TrimString(self.Labels[i], self.BarWidth),
|
||||
SelectStyle(self.LabelStyles, i),
|
||||
image.Pt(labelXCoordinate, self.Inner.Max.Y-1),
|
||||
)
|
||||
|
||||
barXCoordinate += (self.BarWidth + self.BarGap)
|
||||
}
|
||||
}
|
||||
106
widgets/table.go
Normal file
106
widgets/table.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
/* Table is like:
|
||||
|
||||
┌ Awesome Table ───────────────────────────────────────────────┐
|
||||
│ Col0 | Col1 | Col2 | Col3 | Col4 | Col5 | Col6 |
|
||||
│──────────────────────────────────────────────────────────────│
|
||||
│ Some Item #1 | AAA | 123 | CCCCC | EEEEE | GGGGG | IIIII |
|
||||
│──────────────────────────────────────────────────────────────│
|
||||
│ Some Item #2 | BBB | 456 | DDDDD | FFFFF | HHHHH | JJJJJ |
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
*/
|
||||
|
||||
type Table struct {
|
||||
Block
|
||||
Rows [][]string
|
||||
ColumnWidths []int
|
||||
TextStyle Style
|
||||
RowSeparator bool
|
||||
TextAlign Alignment
|
||||
}
|
||||
|
||||
func NewTable() *Table {
|
||||
return &Table{
|
||||
Block: *NewBlock(),
|
||||
TextStyle: Theme.Table.Text,
|
||||
RowSeparator: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Table) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
columnWidths := self.ColumnWidths
|
||||
if len(columnWidths) == 0 {
|
||||
columnCount := len(self.Rows[0])
|
||||
colWidth := self.Inner.Dx() / columnCount
|
||||
for i := 0; i < columnCount; i++ {
|
||||
columnWidths = append(columnWidths, colWidth)
|
||||
}
|
||||
}
|
||||
|
||||
yCoordinate := self.Inner.Min.Y
|
||||
|
||||
// draw rows
|
||||
for i := 0; i < len(self.Rows) && yCoordinate < self.Inner.Max.Y; i++ {
|
||||
row := self.Rows[i]
|
||||
colXCoordinate := self.Inner.Min.X
|
||||
// draw row cells
|
||||
for j := 0; j < len(row); j++ {
|
||||
col := ParseText(row[j], self.TextStyle)
|
||||
// draw row cell
|
||||
if len(col) > columnWidths[j] || self.TextAlign == AlignLeft {
|
||||
for k, cell := range col {
|
||||
if k == columnWidths[j] || colXCoordinate+k == self.Inner.Max.X {
|
||||
cell.Rune = DOTS
|
||||
buf.SetCell(cell, image.Pt(colXCoordinate+k-1, yCoordinate))
|
||||
break
|
||||
} else {
|
||||
buf.SetCell(cell, image.Pt(colXCoordinate+k, yCoordinate))
|
||||
}
|
||||
}
|
||||
} else if self.TextAlign == AlignCenter {
|
||||
xCoordinateOffset := (columnWidths[j] - len(col)) / 2
|
||||
stringXCoordinate := xCoordinateOffset + colXCoordinate
|
||||
for k, cell := range col {
|
||||
buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate))
|
||||
}
|
||||
} else if self.TextAlign == AlignRight {
|
||||
stringXCoordinate := MinInt(colXCoordinate+columnWidths[j], self.Inner.Max.X) - len(col)
|
||||
for k, cell := range col {
|
||||
buf.SetCell(cell, image.Pt(stringXCoordinate+k, yCoordinate))
|
||||
}
|
||||
}
|
||||
colXCoordinate += columnWidths[j] + 1
|
||||
}
|
||||
|
||||
// draw vertical separators
|
||||
separatorXCoordinate := self.Inner.Min.X
|
||||
verticalCell := NewCell(VERTICAL_LINE, NewStyle(ColorWhite))
|
||||
for _, width := range columnWidths {
|
||||
separatorXCoordinate += width
|
||||
buf.SetCell(verticalCell, image.Pt(separatorXCoordinate, yCoordinate))
|
||||
separatorXCoordinate++
|
||||
}
|
||||
|
||||
yCoordinate++
|
||||
|
||||
// draw horizontal separator
|
||||
horizontalCell := NewCell(HORIZONTAL_LINE, NewStyle(ColorWhite))
|
||||
if self.RowSeparator && yCoordinate < self.Inner.Max.Y && i != len(self.Rows)-1 {
|
||||
buf.Fill(horizontalCell, image.Rect(self.Inner.Min.X, yCoordinate, self.Inner.Max.X, yCoordinate+1))
|
||||
yCoordinate++
|
||||
}
|
||||
}
|
||||
}
|
||||
71
widgets/tabs.go
Normal file
71
widgets/tabs.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
. "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
// TabPane is a renderable widget which can be used to conditionally render certain tabs/views.
|
||||
// TabPane shows a list of Tab names.
|
||||
// The currently selected tab can be found through the `ActiveTabIndex` field.
|
||||
type TabPane struct {
|
||||
Block
|
||||
TabNames []string
|
||||
ActiveTabIndex uint
|
||||
ActiveTabStyle Style
|
||||
InactiveTabStyle Style
|
||||
}
|
||||
|
||||
func NewTabPane(names ...string) *TabPane {
|
||||
return &TabPane{
|
||||
Block: *NewBlock(),
|
||||
TabNames: names,
|
||||
ActiveTabStyle: Theme.Tab.Active,
|
||||
InactiveTabStyle: Theme.Tab.Inactive,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TabPane) FocusLeft() {
|
||||
if self.ActiveTabIndex > 0 {
|
||||
self.ActiveTabIndex--
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TabPane) FocusRight() {
|
||||
if self.ActiveTabIndex < uint(len(self.TabNames)-1) {
|
||||
self.ActiveTabIndex++
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TabPane) Draw(buf *Buffer) {
|
||||
self.Block.Draw(buf)
|
||||
|
||||
xCoordinate := self.Inner.Min.X
|
||||
for i, name := range self.TabNames {
|
||||
ColorPair := self.InactiveTabStyle
|
||||
if uint(i) == self.ActiveTabIndex {
|
||||
ColorPair = self.ActiveTabStyle
|
||||
}
|
||||
buf.SetString(
|
||||
TrimString(name, self.Inner.Max.X-xCoordinate),
|
||||
ColorPair,
|
||||
image.Pt(xCoordinate, self.Inner.Min.Y),
|
||||
)
|
||||
|
||||
xCoordinate += 1 + len(name)
|
||||
|
||||
if i < len(self.TabNames)-1 && xCoordinate < self.Inner.Max.X {
|
||||
buf.SetCell(
|
||||
NewCell(VERTICAL_LINE, NewStyle(ColorWhite)),
|
||||
image.Pt(xCoordinate, self.Inner.Min.Y),
|
||||
)
|
||||
}
|
||||
|
||||
xCoordinate += 2
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user