1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-25 13:48:50 +08:00

Implementation of the SparkLine widget.

And completing the demo.
This commit is contained in:
Jakub Sobon 2018-06-22 09:40:41 -04:00
parent 2ce014d35a
commit a9813c4c76
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
6 changed files with 225 additions and 198 deletions

View File

@ -28,7 +28,9 @@ type options struct {
// newOptions returns options with the default values set.
func newOptions() *options {
return &options{}
return &options{
color: DefaultColor,
}
}
// Label adds a label above the SparkLine.

View File

@ -8,6 +8,8 @@ import (
"sync"
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/draw"
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)
@ -45,6 +47,62 @@ func New(opts ...Option) *SparkLine {
func (sl *SparkLine) Draw(cvs *canvas.Canvas) error {
sl.mu.Lock()
defer sl.mu.Unlock()
ar := sl.area(cvs)
visible, max := visibleMax(sl.data, ar.Dx())
var curX int
if len(visible) < ar.Dx() {
curX = ar.Max.X - len(visible)
} else {
curX = ar.Min.X
}
for _, v := range visible {
blocks := toBlocks(v, max, ar.Dy())
curY := ar.Max.Y - 1
for i := 0; i < blocks.full; i++ {
cells, err := cvs.SetCell(
image.Point{curX, curY},
sparks[len(sparks)-1],
cell.FgColor(sl.opts.color),
)
if err != nil {
return err
}
if cells != 1 {
panic(fmt.Sprintf("set an unexpected number of cells %d while filling a full block, expected one", cells))
}
curY--
}
if blocks.partSpark != 0 {
cells, err := cvs.SetCell(
image.Point{curX, curY},
blocks.partSpark,
cell.FgColor(sl.opts.color),
)
if err != nil {
return err
}
if cells != 1 {
panic(fmt.Sprintf("set an unexpected number of cells %d while filling a partial block, expected one", cells))
}
}
curX++
}
if sl.opts.label != "" {
lStart := image.Point{ar.Min.X, ar.Min.Y - 1}
if err := draw.Text(cvs, sl.opts.label, lStart,
draw.TextCellOpts(sl.opts.labelCellOpts...),
draw.TextOverrunMode(draw.OverrunModeThreeDot),
); err != nil {
return err
}
}
return nil
}
@ -84,6 +142,29 @@ func (*SparkLine) Mouse(m *terminalapi.Mouse) error {
return errors.New("the SparkLine widget doesn't support mouse events")
}
// area returns the area of the canvas available to the SparkLine.
func (sl *SparkLine) area(cvs *canvas.Canvas) image.Rectangle {
cvsAr := cvs.Area()
maxY := cvsAr.Max.Y
var minY int
if sl.opts.height > 0 {
minY = maxY - sl.opts.height
} else {
minY = cvsAr.Min.Y
if sl.opts.label != "" {
minY++ // Reserve one line for the label.
}
}
return image.Rect(
cvsAr.Min.X,
minY,
cvsAr.Max.X,
maxY,
)
}
// minSize returns the minimum canvas size for the sparkline based on the options.
func (sl *SparkLine) minSize() image.Point {
// At least one data point.

View File

@ -45,19 +45,20 @@ func TestSparkLine(t *testing.T) {
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantUpdateErr: true,
},
{
desc: "single height sparkline",
sparkLine: New(),
update: func(sl *SparkLine) error {
return sl.Add(0, 1, 2, 3, 4, 5, 6, 7)
return sl.Add(0, 1, 2, 3, 4, 5, 6, 7, 8)
},
canvas: image.Rect(0, 0, 8, 1),
canvas: image.Rect(0, 0, 9, 1),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, " ▁▂▃▄▅▆▇", image.Point{0, 0}, draw.TextCellOpts(
testdraw.MustText(c, "▁▂▃▄▅▆▇", image.Point{1, 0}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testcanvas.MustApply(c, ft)
@ -70,72 +71,32 @@ func TestSparkLine(t *testing.T) {
Color(cell.ColorMagenta),
),
update: func(sl *SparkLine) error {
return sl.Add(0, 1, 2, 3, 4, 5, 6, 7)
return sl.Add(0, 1, 2, 3, 4, 5, 6, 7, 8)
},
canvas: image.Rect(0, 0, 8, 1),
canvas: image.Rect(0, 0, 9, 1),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, " ▁▂▃▄▅▆▇", image.Point{0, 0}, draw.TextCellOpts(
testdraw.MustText(c, "▁▂▃▄▅▆▇", image.Point{1, 0}, draw.TextCellOpts(
cell.FgColor(cell.ColorMagenta),
))
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "full cell has background set",
sparkLine: New(),
update: func(sl *SparkLine) error {
return sl.Add(8)
},
canvas: image.Rect(0, 0, 1, 1),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "█", image.Point{0, 0}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
))
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "full cell has background set to custom color",
sparkLine: New(
Color(cell.ColorMagenta),
),
update: func(sl *SparkLine) error {
return sl.Add(8)
},
canvas: image.Rect(0, 0, 1, 1),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "█", image.Point{0, 0}, draw.TextCellOpts(
cell.FgColor(cell.ColorMagenta),
cell.BgColor(cell.ColorMagenta),
))
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "draws data points from the right",
sparkLine: New(),
update: func(sl *SparkLine) error {
return sl.Add(6, 7)
return sl.Add(7, 8)
},
canvas: image.Rect(0, 0, 9, 1),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "▇", image.Point{0, 0}, draw.TextCellOpts(
testdraw.MustText(c, "▇█", image.Point{7, 0}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
@ -149,7 +110,7 @@ func TestSparkLine(t *testing.T) {
Label("Hello"),
),
update: func(sl *SparkLine) error {
return sl.Add(0, 1, 2, 3, 7, 3, 2, 0, 1)
return sl.Add(0, 1, 2, 3, 8, 3, 2, 1, 1)
},
canvas: image.Rect(0, 0, 9, 2),
want: func(size image.Point) *faketerm.Terminal {
@ -157,7 +118,7 @@ func TestSparkLine(t *testing.T) {
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "Hello", image.Point{0, 0})
testdraw.MustText(c, " ▁▂▃▇▃▂ ▁", image.Point{0, 1}, draw.TextCellOpts(
testdraw.MustText(c, "▁▂▃█▃▂▁▁", image.Point{1, 1}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
@ -171,7 +132,7 @@ func TestSparkLine(t *testing.T) {
Label("Hello world"),
),
update: func(sl *SparkLine) error {
return sl.Add(7)
return sl.Add(8)
},
canvas: image.Rect(0, 0, 9, 2),
want: func(size image.Point) *faketerm.Terminal {
@ -179,7 +140,7 @@ func TestSparkLine(t *testing.T) {
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "Hello wo…", image.Point{0, 0})
testdraw.MustText(c, " ▇", image.Point{0, 1}, draw.TextCellOpts(
testdraw.MustText(c, "█", image.Point{8, 1}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
@ -198,46 +159,24 @@ func TestSparkLine(t *testing.T) {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, " ", image.Point{0, 0}, draw.TextCellOpts(
testdraw.MustText(c, "█", image.Point{1, 0}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testcanvas.MustSetCell(c, image.Point{1, 0}, '█',
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
)
testdraw.MustText(c, " █ █", image.Point{0, 1}, draw.TextCellOpts(
testdraw.MustText(c, "▃", image.Point{3, 0}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testcanvas.MustSetCell(c, image.Point{1, 1}, '█',
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
)
testcanvas.MustSetCell(c, image.Point{3, 1}, '█',
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
)
testdraw.MustText(c, " ███", image.Point{0, 2}, draw.TextCellOpts(
testdraw.MustText(c, "█", image.Point{1, 1}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustRectangle(c, image.Rect(1, 2, 4, 3),
draw.RectChar('█'),
draw.RectCellOpts(
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
),
)
testdraw.MustText(c, " ███", image.Point{0, 3}, draw.TextCellOpts(
testdraw.MustText(c, "█", image.Point{3, 1}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustText(c, "███", image.Point{1, 2}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustText(c, "███", image.Point{1, 3}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustRectangle(c, image.Rect(1, 3, 4, 4),
draw.RectChar('█'),
draw.RectCellOpts(
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
),
)
testcanvas.MustApply(c, ft)
return ft
@ -246,47 +185,32 @@ func TestSparkLine(t *testing.T) {
{
desc: "stretches up to the height of the container with label",
sparkLine: New(
Label("Hello"),
Label("zoo"),
),
update: func(sl *SparkLine) error {
return sl.Add(0, 90, 30, 85)
},
canvas: image.Rect(0, 0, 9, 4),
canvas: image.Rect(0, 0, 4, 4),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "Hello", image.Point{0, 0})
testdraw.MustText(c, " ", image.Point{0, 1}, draw.TextCellOpts(
testdraw.MustText(c, "zoo", image.Point{0, 0})
testdraw.MustText(c, "█", image.Point{1, 1}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testcanvas.MustSetCell(c, image.Point{1, 1}, '█',
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
)
testdraw.MustText(c, " █ █", image.Point{0, 2}, draw.TextCellOpts(
testdraw.MustText(c, "▇", image.Point{3, 1}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testcanvas.MustSetCell(c, image.Point{1, 2}, '█',
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
)
testcanvas.MustSetCell(c, image.Point{3, 2}, '█',
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
)
testdraw.MustText(c, " ███", image.Point{0, 3}, draw.TextCellOpts(
testdraw.MustText(c, "█", image.Point{1, 2}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustText(c, "█", image.Point{3, 2}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustText(c, "███", image.Point{1, 3}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustRectangle(c, image.Rect(1, 3, 4, 4),
draw.RectChar('█'),
draw.RectCellOpts(
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
),
)
testcanvas.MustApply(c, ft)
return ft
@ -300,29 +224,20 @@ func TestSparkLine(t *testing.T) {
update: func(sl *SparkLine) error {
return sl.Add(0, 100, 50, 85)
},
canvas: image.Rect(0, 0, 9, 4),
canvas: image.Rect(0, 0, 4, 4),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, " ", image.Point{0, 2}, draw.TextCellOpts(
testdraw.MustText(c, "█", image.Point{1, 2}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testcanvas.MustSetCell(c, image.Point{1, 2}, '█',
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
)
testdraw.MustText(c, " ███", image.Point{0, 3}, draw.TextCellOpts(
testdraw.MustText(c, "▆", image.Point{3, 2}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustText(c, "███", image.Point{1, 3}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustRectangle(c, image.Rect(1, 3, 4, 4),
draw.RectChar('█'),
draw.RectCellOpts(
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
),
)
testcanvas.MustApply(c, ft)
return ft
@ -335,34 +250,22 @@ func TestSparkLine(t *testing.T) {
Height(2),
),
update: func(sl *SparkLine) error {
return sl.Add(0, 100, 50, 85)
return sl.Add(0, 100, 50, 0)
},
canvas: image.Rect(0, 0, 9, 4),
canvas: image.Rect(0, 0, 4, 4),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "zoo", image.Point{0, 1}, draw.TextCellOpts(
cell.FgColor(cell.ColorDefault),
))
testdraw.MustText(c, "█", image.Point{1, 2}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustText(c, " █ ▆", image.Point{0, 2}, draw.TextCellOpts(
testdraw.MustText(c, "██", image.Point{1, 3}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testcanvas.MustSetCell(c, image.Point{1, 2}, '█',
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
)
testdraw.MustText(c, " ███", image.Point{0, 3}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testdraw.MustRectangle(c, image.Rect(1, 3, 4, 4),
draw.RectChar('█'),
draw.RectCellOpts(
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
),
)
testcanvas.MustApply(c, ft)
return ft
@ -389,7 +292,7 @@ func TestSparkLine(t *testing.T) {
cell.FgColor(cell.ColorBlue),
cell.BgColor(cell.ColorYellow),
))
testdraw.MustText(c, " ▁", image.Point{0, 1}, draw.TextCellOpts(
testdraw.MustText(c, "█", image.Point{8, 1}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
@ -411,10 +314,6 @@ func TestSparkLine(t *testing.T) {
testdraw.MustText(c, "▆▇█", image.Point{0, 0}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testcanvas.MustSetCell(c, image.Point{2, 0}, '█',
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
)
testcanvas.MustApply(c, ft)
return ft
@ -434,10 +333,6 @@ func TestSparkLine(t *testing.T) {
testdraw.MustText(c, "▄█", image.Point{0, 0}, draw.TextCellOpts(
cell.FgColor(DefaultColor),
))
testcanvas.MustSetCell(c, image.Point{1, 0}, '█',
cell.FgColor(DefaultColor),
cell.BgColor(DefaultColor),
)
testcanvas.MustApply(c, ft)
return ft

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/mum4k/termdash"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/draw"
"github.com/mum4k/termdash/terminal/termbox"
@ -44,14 +45,48 @@ func main() {
defer t.Close()
ctx, cancel := context.WithCancel(context.Background())
bc := sparkline.New()
go playSparkLine(ctx, bc, 1*time.Second)
green := sparkline.New(
sparkline.Label("Green SparkLine", cell.FgColor(cell.ColorBlue)),
)
go playSparkLine(ctx, green, 250*time.Millisecond)
red := sparkline.New(
sparkline.Label("Red SparkLine", cell.FgColor(cell.ColorBlue)),
sparkline.Color(cell.ColorRed),
)
go playSparkLine(ctx, red, 500*time.Millisecond)
yellow := sparkline.New(
sparkline.Label("Yellow SparkLine", cell.FgColor(cell.ColorGreen)),
sparkline.Color(cell.ColorYellow),
)
go playSparkLine(ctx, yellow, 1*time.Second)
c := container.New(
t,
container.Border(draw.LineStyleLight),
container.BorderTitle("PRESS Q TO QUIT"),
container.PlaceWidget(bc),
container.SplitVertical(
container.Left(
container.SplitHorizontal(
container.Top(),
container.Bottom(
container.Border(draw.LineStyleLight),
container.BorderTitle("SparkLine group"),
container.SplitHorizontal(
container.Top(
container.PlaceWidget(green),
),
container.Bottom(
container.PlaceWidget(red),
),
),
),
),
),
container.Right(
container.Border(draw.LineStyleLight),
container.PlaceWidget(yellow),
),
),
)
quitter := func(k *terminalapi.Keyboard) {

View File

@ -12,9 +12,11 @@ import "math"
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
// visibleMax determines the maximum visible data point given the canvas width.
func visibleMax(data []int, width int) int {
// Returns a slice that contains only visible data points and the maximum value
// among them.
func visibleMax(data []int, width int) ([]int, int) {
if width <= 0 || len(data) == 0 {
return 0
return nil, 0
}
if width < len(data) {
@ -27,7 +29,7 @@ func visibleMax(data []int, width int) int {
max = v
}
}
return max
return data, max
}
// blocks represents blocks that display one value on a SparkLine.

View File

@ -8,65 +8,77 @@ import (
func TestVisibleMax(t *testing.T) {
tests := []struct {
desc string
data []int
width int
want int
desc string
data []int
width int
wantData []int
wantMax int
}{
{
desc: "zero for no data",
width: 3,
want: 0,
desc: "zero for no data",
width: 3,
wantData: nil,
wantMax: 0,
},
{
desc: "zero for zero width",
data: []int{0, 1},
width: 0,
want: 0,
desc: "zero for zero width",
data: []int{0, 1},
width: 0,
wantData: nil,
wantMax: 0,
},
{
desc: "zero for negative width",
data: []int{0, 1},
width: -1,
want: 0,
desc: "zero for negative width",
data: []int{0, 1},
width: -1,
wantData: nil,
wantMax: 0,
},
{
desc: "all values are zero",
data: []int{0, 0, 0},
width: 3,
want: 0,
desc: "all values are zero",
data: []int{0, 0, 0},
width: 3,
wantData: []int{0, 0, 0},
wantMax: 0,
},
{
desc: "all values are visible",
data: []int{8, 0, 1},
width: 3,
want: 8,
desc: "all values are visible",
data: []int{8, 0, 1},
width: 3,
wantData: []int{8, 0, 1},
wantMax: 8,
},
{
desc: "width greater than number of values",
data: []int{8, 0, 1},
width: 10,
want: 8,
desc: "width greater than number of values",
data: []int{8, 0, 1},
width: 10,
wantData: []int{8, 0, 1},
wantMax: 8,
},
{
desc: "only some values are visible",
data: []int{8, 2, 1},
width: 2,
want: 2,
desc: "only some values are visible",
data: []int{8, 2, 1},
width: 2,
wantData: []int{2, 1},
wantMax: 2,
},
{
desc: "only one value is visible",
data: []int{8, 2, 1},
width: 1,
want: 1,
desc: "only one value is visible",
data: []int{8, 2, 1},
width: 1,
wantData: []int{1},
wantMax: 1,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got := visibleMax(tc.data, tc.width)
if got != tc.want {
t.Errorf("visibleMax => got %v, want %v", got, tc.want)
gotData, gotMax := visibleMax(tc.data, tc.width)
if diff := pretty.Compare(tc.wantData, gotData); diff != "" {
t.Errorf("visibleMax => unexpected visible data, diff (-want, +got):\n%s", diff)
}
if gotMax != tc.wantMax {
t.Errorf("visibleMax => gotMax %v, wantMax %v", gotMax, tc.wantMax)
}
})
}