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

Merge branch 'devel' into sparkline

This commit is contained in:
Jakub Sobon 2018-06-24 23:53:09 -04:00
commit 38c288b077
6 changed files with 1262 additions and 0 deletions

View File

@ -0,0 +1,299 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package barchart implements a widget that draws multiple bars displaying
// values and their relative ratios.
package barchart
import (
"errors"
"fmt"
"image"
"sync"
"github.com/mum4k/termdash/align"
"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"
)
// BarChart displays multiple bars showing relative ratios of values.
//
// Each bar can have a text label under it explaining the meaning of the value
// and can display the value itself inside the bar.
//
// Implements widgetapi.Widget. This object is thread-safe.
type BarChart struct {
// values are the values provided on a call to Values(). These are the
// individual bars that will be drawn.
values []int
// max is the maximum value of a bar. A bar having this value takes all the
// vertical space.
max int
// mu protects the BarChart.
mu sync.Mutex
// opts are the provided options.
opts *options
}
// New returns a new BarChart.
func New(opts ...Option) *BarChart {
opt := newOptions()
for _, o := range opts {
o.set(opt)
}
return &BarChart{
opts: opt,
}
}
// Draw draws the BarChart widget onto the canvas.
// Implements widgetapi.Widget.Draw.
func (bc *BarChart) Draw(cvs *canvas.Canvas) error {
bc.mu.Lock()
defer bc.mu.Unlock()
for i, v := range bc.values {
r, err := bc.barRect(cvs, i, v)
if err != nil {
return err
}
if r.Dy() > 0 { // Value might be so small so that the rectangle is zero.
if err := draw.Rectangle(cvs, r,
draw.RectCellOpts(cell.BgColor(bc.barColor(i))),
draw.RectChar(bc.opts.barChar),
); err != nil {
return err
}
}
if bc.opts.showValues {
if err := bc.drawText(cvs, i, fmt.Sprint(bc.values[i]), bc.valColor(i), insideBar); err != nil {
return err
}
}
l, c := bc.label(i)
if l != "" {
if err := bc.drawText(cvs, i, l, c, underBar); err != nil {
return err
}
}
}
return nil
}
// textLoc represents the location of the drawn text.
type textLoc int
const (
insideBar textLoc = iota
underBar
)
// drawText draws the provided text inside or under the i-th bar.
func (bc *BarChart) drawText(cvs *canvas.Canvas, i int, text string, color cell.Color, loc textLoc) error {
// Rectangle representing area in which the text will be aligned.
var barCol image.Rectangle
r, err := bc.barRect(cvs, i, bc.max)
if err != nil {
return err
}
switch loc {
case insideBar:
// Align the text within the bar itself.
barCol = r
case underBar:
// Align the text within the entire column where the bar is, this
// includes the space for any label under the bar.
barCol = image.Rect(r.Min.X, cvs.Area().Min.Y, r.Max.X, cvs.Area().Max.Y)
}
start, err := align.Text(barCol, text, align.HorizontalCenter, align.VerticalBottom)
if err != nil {
return err
}
return draw.Text(cvs, text, start,
draw.TextCellOpts(cell.FgColor(color)),
draw.TextMaxX(barCol.Max.X),
draw.TextOverrunMode(draw.OverrunModeThreeDot),
)
}
// barWidth determines the width of a single bar based on options and the canvas.
func (bc *BarChart) barWidth(cvs *canvas.Canvas) int {
if len(bc.values) == 0 {
return 0 // No width when we have no values.
}
if bc.opts.barWidth >= 1 {
// Prefer width set via the options if it is positive.
return bc.opts.barWidth
}
gaps := len(bc.values) - 1
gapW := gaps * bc.opts.barGap
rem := cvs.Area().Dx() - gapW
return rem / len(bc.values)
}
// barHeight determines the height of the i-th bar based on the value it is displaying.
func (bc *BarChart) barHeight(cvs *canvas.Canvas, i, value int) int {
available := cvs.Area().Dy()
if len(bc.opts.labels) > 0 {
// One line for the bar labels.
available--
}
ratio := float32(value) / float32(bc.max)
return int(float32(available) * ratio)
}
// barRect returns a rectangle that represents the i-th bar on the canvas that
// displays the specified value.
func (bc *BarChart) barRect(cvs *canvas.Canvas, i, value int) (image.Rectangle, error) {
bw := bc.barWidth(cvs)
minX := bw * i
if i > 0 {
minX += bc.opts.barGap * i
}
maxX := minX + bw
bh := bc.barHeight(cvs, i, value)
maxY := cvs.Area().Max.Y
if len(bc.opts.labels) > 0 {
// One line for the bar labels.
maxY--
}
minY := maxY - bh
return image.Rect(minX, minY, maxX, maxY), nil
}
// barColor safely determines the color for the i-th bar.
// Colors are optional and don't have to be specified for all the bars.
func (bc *BarChart) barColor(i int) cell.Color {
if len(bc.opts.barColors) > i {
return bc.opts.barColors[i]
}
return DefaultBarColor
}
// valColor safely determines the color for the i-th value.
// Colors are optional and don't have to be specified for all the values.
func (bc *BarChart) valColor(i int) cell.Color {
if len(bc.opts.valueColors) > i {
return bc.opts.valueColors[i]
}
return DefaultValueColor
}
// label safely determines the label and its color for the i-th bar.
// Labels are optional and don't have to be specified for all the bars.
func (bc *BarChart) label(i int) (string, cell.Color) {
var label string
if len(bc.opts.labels) > i {
label = bc.opts.labels[i]
}
if len(bc.opts.labelColors) > i {
return label, bc.opts.labelColors[i]
}
return label, DefaultLabelColor
}
// Values sets the values to be displayed by the BarChart.
// Each value ends up in its own bar. The values must not be negative and must
// be less or equal the maximum value. A bar displaying the maximum value is a
// full bar, taking all available vertical space.
// Provided options override values set when New() was called.
func (bc *BarChart) Values(values []int, max int, opts ...Option) error {
bc.mu.Lock()
defer bc.mu.Unlock()
if err := validateValues(values, max); err != nil {
return err
}
for _, opt := range opts {
opt.set(bc.opts)
}
bc.values = values
bc.max = max
return nil
}
// Keyboard input isn't supported on the BarChart widget.
func (*BarChart) Keyboard(k *terminalapi.Keyboard) error {
return errors.New("the BarChart widget doesn't support keyboard events")
}
// Mouse input isn't supported on the BarChart widget.
func (*BarChart) Mouse(m *terminalapi.Mouse) error {
return errors.New("the BarChart widget doesn't support mouse events")
}
// Options implements widgetapi.Widget.Options.
func (bc *BarChart) Options() widgetapi.Options {
bc.mu.Lock()
defer bc.mu.Unlock()
return widgetapi.Options{
MinimumSize: bc.minSize(),
WantKeyboard: false,
WantMouse: false,
}
}
// minSize determines the minimum required size of the canvas.
func (bc *BarChart) minSize() image.Point {
bars := len(bc.values)
if bars == 0 {
return image.Point{1, 1}
}
minHeight := 1 // At least one character vertically to display the bar.
if len(bc.opts.labels) > 0 {
minHeight++ // One line for the labels.
}
var minBarWidth int
if bc.opts.barWidth < 1 {
minBarWidth = 1 // At least one char for the bar itself.
} else {
minBarWidth = bc.opts.barWidth
}
minWidth := bars*minBarWidth + (bars-1)*bc.opts.barGap
return image.Point{minWidth, minHeight}
}
// validateValues validates the provided values and maximum.
func validateValues(values []int, max int) error {
if max < 1 {
return fmt.Errorf("invalid maximum value %d, must be at least 1", max)
}
for i, v := range values {
if v < 0 || v > max {
return fmt.Errorf("invalid values[%d]: %d, each value must be 0 <= value <= max", i, v)
}
}
return nil
}

View File

@ -0,0 +1,676 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package barchart
import (
"image"
"testing"
"github.com/kylelemons/godebug/pretty"
"github.com/mum4k/termdash/canvas"
"github.com/mum4k/termdash/canvas/testcanvas"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/draw"
"github.com/mum4k/termdash/draw/testdraw"
"github.com/mum4k/termdash/terminal/faketerm"
"github.com/mum4k/termdash/widgetapi"
)
func TestGauge(t *testing.T) {
tests := []struct {
desc string
bc *BarChart
update func(*BarChart) error // update gets called before drawing of the widget.
canvas image.Rectangle
want func(size image.Point) *faketerm.Terminal
wantUpdateErr bool // whether to expect an error on a call to the update function
wantDrawErr bool
}{
{
desc: "draws empty for no values",
bc: New(
Char('o'),
),
update: func(bc *BarChart) error {
return nil
},
canvas: image.Rect(0, 0, 3, 10),
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
},
{
desc: "fails for zero max",
bc: New(
Char('o'),
),
update: func(bc *BarChart) error {
return bc.Values([]int{0, 2, 5, 10}, 0)
},
canvas: image.Rect(0, 0, 3, 10),
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantUpdateErr: true,
},
{
desc: "fails for negative max",
bc: New(
Char('o'),
),
update: func(bc *BarChart) error {
return bc.Values([]int{0, 2, 5, 10}, -1)
},
canvas: image.Rect(0, 0, 3, 10),
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantUpdateErr: true,
},
{
desc: "fails when negative value",
bc: New(
Char('o'),
),
update: func(bc *BarChart) error {
return bc.Values([]int{0, -2, 5, 10}, 10)
},
canvas: image.Rect(0, 0, 3, 10),
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantUpdateErr: true,
},
{
desc: "fails for value larger than max",
bc: New(
Char('o'),
),
update: func(bc *BarChart) error {
return bc.Values([]int{0, 2, 5, 11}, 10)
},
canvas: image.Rect(0, 0, 3, 10),
want: func(size image.Point) *faketerm.Terminal {
return faketerm.MustNew(size)
},
wantUpdateErr: true,
},
{
desc: "displays bars",
bc: New(
Char('o'),
),
update: func(bc *BarChart) error {
return bc.Values([]int{0, 2, 5, 10}, 10)
},
canvas: image.Rect(0, 0, 7, 10),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustRectangle(c, image.Rect(2, 8, 3, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(4, 5, 5, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(6, 0, 7, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "displays bars with labels",
bc: New(
Char('o'),
Labels([]string{
"1",
"2",
"3",
}),
),
update: func(bc *BarChart) error {
return bc.Values([]int{1, 2, 5, 10}, 10)
},
canvas: image.Rect(0, 0, 7, 11),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustRectangle(c, image.Rect(0, 9, 1, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(2, 8, 3, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(4, 5, 5, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(6, 0, 7, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
// Labels.
testdraw.MustText(c, "1", image.Point{0, 10}, draw.TextCellOpts(
cell.FgColor(DefaultLabelColor),
))
testdraw.MustText(c, "2", image.Point{2, 10}, draw.TextCellOpts(
cell.FgColor(DefaultLabelColor),
))
testdraw.MustText(c, "3", image.Point{4, 10}, draw.TextCellOpts(
cell.FgColor(DefaultLabelColor),
))
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "trims too long labels",
bc: New(
Char('o'),
Labels([]string{
"1",
"22",
"3",
}),
),
update: func(bc *BarChart) error {
return bc.Values([]int{1, 2, 5, 10}, 10)
},
canvas: image.Rect(0, 0, 7, 11),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustRectangle(c, image.Rect(0, 9, 1, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(2, 8, 3, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(4, 5, 5, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(6, 0, 7, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
// Labels.
testdraw.MustText(c, "1", image.Point{0, 10}, draw.TextCellOpts(
cell.FgColor(DefaultLabelColor),
))
testdraw.MustText(c, "…", image.Point{2, 10}, draw.TextCellOpts(
cell.FgColor(DefaultLabelColor),
))
testdraw.MustText(c, "3", image.Point{4, 10}, draw.TextCellOpts(
cell.FgColor(DefaultLabelColor),
))
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "displays bars with labels and values",
bc: New(
Char('o'),
Labels([]string{
"1",
"2",
"3",
}),
ShowValues(),
),
update: func(bc *BarChart) error {
return bc.Values([]int{1, 2, 5, 10}, 10)
},
canvas: image.Rect(0, 0, 7, 11),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustRectangle(c, image.Rect(0, 9, 1, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(2, 8, 3, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(4, 5, 5, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(6, 0, 7, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
// Labels.
testdraw.MustText(c, "1", image.Point{0, 10}, draw.TextCellOpts(
cell.FgColor(DefaultLabelColor),
))
testdraw.MustText(c, "2", image.Point{2, 10}, draw.TextCellOpts(
cell.FgColor(DefaultLabelColor),
))
testdraw.MustText(c, "3", image.Point{4, 10}, draw.TextCellOpts(
cell.FgColor(DefaultLabelColor),
))
// Values.
testdraw.MustText(c, "1", image.Point{0, 9}, draw.TextCellOpts(
cell.FgColor(DefaultValueColor),
cell.BgColor(DefaultBarColor),
))
testdraw.MustText(c, "2", image.Point{2, 9}, draw.TextCellOpts(
cell.FgColor(DefaultValueColor),
cell.BgColor(DefaultBarColor),
))
testdraw.MustText(c, "5", image.Point{4, 9}, draw.TextCellOpts(
cell.FgColor(DefaultValueColor),
cell.BgColor(DefaultBarColor),
))
testdraw.MustText(c, "…", image.Point{6, 9}, draw.TextCellOpts(
cell.FgColor(DefaultValueColor),
cell.BgColor(DefaultBarColor),
))
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "bars take as much width as available",
bc: New(
Char('o'),
),
update: func(bc *BarChart) error {
return bc.Values([]int{1, 2}, 10)
},
canvas: image.Rect(0, 0, 5, 10),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustRectangle(c, image.Rect(0, 9, 2, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(3, 8, 5, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "respects set bar width",
bc: New(
Char('o'),
BarWidth(1),
),
update: func(bc *BarChart) error {
return bc.Values([]int{1, 2}, 10)
},
canvas: image.Rect(0, 0, 5, 10),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustRectangle(c, image.Rect(0, 9, 1, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(2, 8, 3, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "options can be set on a call to Values",
bc: New(),
update: func(bc *BarChart) error {
return bc.Values([]int{1, 2}, 10, Char('o'), BarWidth(1))
},
canvas: image.Rect(0, 0, 5, 10),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustRectangle(c, image.Rect(0, 9, 1, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(2, 8, 3, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "respects set bar gap",
bc: New(
Char('o'),
BarGap(2),
),
update: func(bc *BarChart) error {
return bc.Values([]int{1, 2}, 10)
},
canvas: image.Rect(0, 0, 5, 10),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustRectangle(c, image.Rect(0, 9, 1, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(3, 8, 4, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "respects both width and gap",
bc: New(
Char('o'),
BarGap(2),
BarWidth(2),
),
update: func(bc *BarChart) error {
return bc.Values([]int{5, 3}, 10)
},
canvas: image.Rect(0, 0, 6, 10),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustRectangle(c, image.Rect(0, 5, 2, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustRectangle(c, image.Rect(4, 7, 6, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "respects bar and label colors",
bc: New(
Char('o'),
BarColors([]cell.Color{
cell.ColorBlue,
cell.ColorYellow,
}),
LabelColors([]cell.Color{
cell.ColorCyan,
cell.ColorMagenta,
}),
Labels([]string{
"1",
"2",
}),
),
update: func(bc *BarChart) error {
return bc.Values([]int{1, 2, 3}, 10)
},
canvas: image.Rect(0, 0, 5, 11),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustRectangle(c, image.Rect(0, 9, 1, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(cell.ColorBlue)),
)
testdraw.MustText(c, "1", image.Point{0, 10}, draw.TextCellOpts(
cell.FgColor(cell.ColorCyan),
))
testdraw.MustRectangle(c, image.Rect(2, 8, 3, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(cell.ColorYellow)),
)
testdraw.MustText(c, "2", image.Point{2, 10}, draw.TextCellOpts(
cell.FgColor(cell.ColorMagenta),
))
testdraw.MustRectangle(c, image.Rect(4, 7, 5, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "respects value colors",
bc: New(
Char('o'),
ValueColors([]cell.Color{
cell.ColorBlue,
cell.ColorBlack,
}),
ShowValues(),
),
update: func(bc *BarChart) error {
return bc.Values([]int{0, 2, 3}, 10)
},
canvas: image.Rect(0, 0, 5, 10),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
c := testcanvas.MustNew(ft.Area())
testdraw.MustText(c, "0", image.Point{0, 9}, draw.TextCellOpts(
cell.FgColor(cell.ColorBlue),
))
testdraw.MustRectangle(c, image.Rect(2, 8, 3, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustText(c, "2", image.Point{2, 9}, draw.TextCellOpts(
cell.FgColor(cell.ColorBlack),
cell.BgColor(DefaultBarColor),
))
testdraw.MustRectangle(c, image.Rect(4, 7, 5, 10),
draw.RectChar('o'),
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
)
testdraw.MustText(c, "3", image.Point{4, 9}, draw.TextCellOpts(
cell.FgColor(DefaultValueColor),
cell.BgColor(DefaultBarColor),
))
testcanvas.MustApply(c, ft)
return ft
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
c, err := canvas.New(tc.canvas)
if err != nil {
t.Fatalf("canvas.New => unexpected error: %v", err)
}
err = tc.update(tc.bc)
if (err != nil) != tc.wantUpdateErr {
t.Errorf("update => unexpected error: %v, wantUpdateErr: %v", err, tc.wantUpdateErr)
}
if err != nil {
return
}
err = tc.bc.Draw(c)
if (err != nil) != tc.wantDrawErr {
t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
}
if err != nil {
return
}
got, err := faketerm.New(c.Size())
if err != nil {
t.Fatalf("faketerm.New => unexpected error: %v", err)
}
if err := c.Apply(got); err != nil {
t.Fatalf("Apply => unexpected error: %v", err)
}
if diff := faketerm.Diff(tc.want(c.Size()), got); diff != "" {
t.Errorf("Draw => %v", diff)
}
})
}
}
func TestOptions(t *testing.T) {
tests := []struct {
desc string
create func() (*BarChart, error)
want widgetapi.Options
}{
{
desc: "minimum size for no bars",
create: func() (*BarChart, error) {
return New(), nil
},
want: widgetapi.Options{
MinimumSize: image.Point{1, 1},
WantKeyboard: false,
WantMouse: false,
},
},
{
desc: "minimum size for no bars, but have labels",
create: func() (*BarChart, error) {
return New(
Labels([]string{"foo"}),
), nil
},
want: widgetapi.Options{
MinimumSize: image.Point{1, 1},
WantKeyboard: false,
WantMouse: false,
},
},
{
desc: "minimum size for one bar, default width, gap and no labels",
create: func() (*BarChart, error) {
bc := New()
if err := bc.Values([]int{1}, 3); err != nil {
return nil, err
}
return bc, nil
},
want: widgetapi.Options{
MinimumSize: image.Point{1, 1},
WantKeyboard: false,
WantMouse: false,
},
},
{
desc: "minimum size for two bars, default width, gap and no labels",
create: func() (*BarChart, error) {
bc := New()
if err := bc.Values([]int{1, 2}, 3); err != nil {
return nil, err
}
return bc, nil
},
want: widgetapi.Options{
MinimumSize: image.Point{3, 1},
WantKeyboard: false,
WantMouse: false,
},
},
{
desc: "minimum size for two bars, custom width, gap and no labels",
create: func() (*BarChart, error) {
bc := New(
BarWidth(3),
BarGap(2),
)
if err := bc.Values([]int{1, 2}, 3); err != nil {
return nil, err
}
return bc, nil
},
want: widgetapi.Options{
MinimumSize: image.Point{8, 1},
WantKeyboard: false,
WantMouse: false,
},
},
{
desc: "minimum size for two bars, custom width, gap and labels",
create: func() (*BarChart, error) {
bc := New(
BarWidth(3),
BarGap(2),
)
if err := bc.Values([]int{1, 2}, 3, Labels([]string{"foo", "bar"})); err != nil {
return nil, err
}
return bc, nil
},
want: widgetapi.Options{
MinimumSize: image.Point{8, 2},
WantKeyboard: false,
WantMouse: false,
},
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
bc, err := tc.create()
if err != nil {
t.Fatalf("create => unexpected error: %v", err)
}
got := bc.Options()
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}

View File

@ -0,0 +1,112 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Binary barchartdemo displays a couple of BarChart widgets.
// Exist when 'q' is pressed.
package main
import (
"context"
"math/rand"
"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"
"github.com/mum4k/termdash/terminalapi"
"github.com/mum4k/termdash/widgets/barchart"
)
// playBarChart continuously changes the displayed values on the bar chart once every delay.
// Exits when the context expires.
func playBarChart(ctx context.Context, bc *barchart.BarChart, delay time.Duration) {
const (
bars = 6
max = 100
)
values := make([]int, 6)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
for i := range values {
values[i] = int(rand.Int31n(max + 1))
}
if err := bc.Values(values, max); err != nil {
panic(err)
}
case <-ctx.Done():
return
}
}
}
func main() {
t, err := termbox.New()
if err != nil {
panic(err)
}
defer t.Close()
ctx, cancel := context.WithCancel(context.Background())
bc := barchart.New(
barchart.BarColors([]cell.Color{
cell.ColorBlue,
cell.ColorRed,
cell.ColorYellow,
cell.ColorBlue,
cell.ColorGreen,
cell.ColorRed,
}),
barchart.ValueColors([]cell.Color{
cell.ColorRed,
cell.ColorYellow,
cell.ColorBlue,
cell.ColorGreen,
cell.ColorRed,
cell.ColorBlue,
}),
barchart.ShowValues(),
barchart.Labels([]string{
"CPU1",
"",
"CPU3",
}),
)
go playBarChart(ctx, bc, 1*time.Second)
c := container.New(
t,
container.Border(draw.LineStyleLight),
container.BorderTitle("PRESS Q TO QUIT"),
container.PlaceWidget(bc),
)
quitter := func(k *terminalapi.Keyboard) {
if k.Key == 'q' || k.Key == 'Q' {
cancel()
}
}
if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter)); err != nil {
panic(err)
}
}

147
widgets/barchart/options.go Normal file
View File

@ -0,0 +1,147 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package barchart
// options.go contains configurable options for BarChart.
import (
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/draw"
)
// Option is used to provide options.
type Option interface {
// set sets the provided option.
set(*options)
}
// option implements Option.
type option func(*options)
// set implements Option.set.
func (o option) set(opts *options) {
o(opts)
}
// options holds the provided options.
type options struct {
barChar rune
barWidth int
barGap int
showValues bool
barColors []cell.Color
labelColors []cell.Color
valueColors []cell.Color
labels []string
}
// newOptions returns options with the default values set.
func newOptions() *options {
return &options{
barChar: DefaultChar,
barGap: DefaultBarGap,
}
}
// DefaultChar is the default value for the Char option.
const DefaultChar = draw.DefaultRectChar
// Char sets the rune that is used when drawing the rectangle representing the
// bars.
func Char(ch rune) Option {
return option(func(opts *options) {
opts.barChar = ch
})
}
// BarWidth sets the width of the bars. If not set, the bars use all the space
// available to the widget.
func BarWidth(width int) Option {
return option(func(opts *options) {
opts.barWidth = width
})
}
// DefaultBarGap is the default value for the BarGap option.
const DefaultBarGap = 1
// BarGap sets the width of the space between the bars.
// Defaults to DefaultBarGap.
func BarGap(width int) Option {
return option(func(opts *options) {
opts.barGap = width
})
}
// ShowValues tells the bar chart to display the actual values inside each of the bars.
func ShowValues() Option {
return option(func(opts *options) {
opts.showValues = true
})
}
// DefaultBarColor is the default color of a bar, unless specified otherwise
// via the BarColors option.
const DefaultBarColor = cell.ColorRed
// BarColors sets the colors of each of the bars.
// Bars are created on a call to Values(), each value ends up in its own Bar.
// The first supplied color applies to the bar displaying the first value.
// Any bars that don't have a color specified use the DefaultBarColor.
func BarColors(colors []cell.Color) Option {
return option(func(opts *options) {
opts.barColors = colors
})
}
// DefaultLabelColor is the default color of a bar label, unless specified
// otherwise via the LabelColors option.
const DefaultLabelColor = cell.ColorGreen
// LabelColors sets the colors of each of the labels under the bars.
// Bars are created on a call to Values(), each value ends up in its own Bar.
// The first supplied color applies to the label of the bar displaying the
// first value. Any labels that don't have a color specified use the
// DefaultLabelColor.
func LabelColors(colors []cell.Color) Option {
return option(func(opts *options) {
opts.labelColors = colors
})
}
// Labels sets the labels displayed under each bar,
// Bars are created on a call to Values(), each value ends up in its own Bar.
// The first supplied label applies to the bar displaying the first value.
// If not specified, the corresponding bar (or all the bars) don't have a
// label.
func Labels(labels []string) Option {
return option(func(opts *options) {
opts.labels = labels
})
}
// DefaultValueColor is the default color of a bar value, unless specified
// otherwise via the ValueColors option.
const DefaultValueColor = cell.ColorYellow
// ValueColors sets the colors of each of the values in the bars. Bars are
// created on a call to Values(), each value ends up in its own Bar. The first
// supplied color applies to the bar displaying the first value. Any values
// that don't have a color specified use the DefaultValueColor.
func ValueColors(colors []cell.Color) Option {
return option(func(opts *options) {
opts.valueColors = colors
})
}

View File

@ -1,3 +1,17 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package text
import (

View File

@ -1,3 +1,17 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package text
import (