mirror of https://github.com/mum4k/termdash.git
Self-review.
This commit is contained in:
parent
a9813c4c76
commit
9d02f43c6f
|
@ -17,7 +17,7 @@ import (
|
|||
// SparkLine draws a graph showing a series of values as vertical bars.
|
||||
//
|
||||
// Bars can have sub-cell height. The graphs scale adjusts dynamically based on
|
||||
// the largest displayed value or has a statically set maximum.
|
||||
// the largest visible value.
|
||||
//
|
||||
// Implements widgetapi.Widget. This object is thread-safe.
|
||||
type SparkLine struct {
|
||||
|
@ -61,40 +61,32 @@ func (sl *SparkLine) Draw(cvs *canvas.Canvas) error {
|
|||
blocks := toBlocks(v, max, ar.Dy())
|
||||
curY := ar.Max.Y - 1
|
||||
for i := 0; i < blocks.full; i++ {
|
||||
cells, err := cvs.SetCell(
|
||||
if _, err := cvs.SetCell(
|
||||
image.Point{curX, curY},
|
||||
sparks[len(sparks)-1],
|
||||
sparks[len(sparks)-1], // Last spark represents full cell.
|
||||
cell.FgColor(sl.opts.color),
|
||||
)
|
||||
if err != nil {
|
||||
); 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(
|
||||
if _, err := cvs.SetCell(
|
||||
image.Point{curX, curY},
|
||||
blocks.partSpark,
|
||||
cell.FgColor(sl.opts.color),
|
||||
)
|
||||
if err != nil {
|
||||
); 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 != "" {
|
||||
// Label is placed immediately above the SparkLine.
|
||||
lStart := image.Point{ar.Min.X, ar.Min.Y - 1}
|
||||
if err := draw.Text(cvs, sl.opts.label, lStart,
|
||||
draw.TextCellOpts(sl.opts.labelCellOpts...),
|
||||
|
@ -111,10 +103,15 @@ func (sl *SparkLine) Draw(cvs *canvas.Canvas) error {
|
|||
// integers.
|
||||
// The last added data point will be the one displayed all the way on the right
|
||||
// of the SparkLine.
|
||||
func (sl *SparkLine) Add(data ...int) error {
|
||||
// Provided options override values set when New() was called.
|
||||
func (sl *SparkLine) Add(data []int, opts ...Option) error {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.set(sl.opts)
|
||||
}
|
||||
|
||||
for i, d := range data {
|
||||
if d < 0 {
|
||||
return fmt.Errorf("data point[%d]: %v must be a positive integer", i, d)
|
||||
|
@ -124,11 +121,12 @@ func (sl *SparkLine) Add(data ...int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Clear removes all the data points in the sparkline, effectively returning to
|
||||
// Clear removes all the data points in the SparkLine, effectively returning to
|
||||
// an empty graph.
|
||||
func (sl *SparkLine) Clear() {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
sl.data = nil
|
||||
}
|
||||
|
||||
|
@ -145,8 +143,9 @@ func (*SparkLine) Mouse(m *terminalapi.Mouse) error {
|
|||
// 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
|
||||
|
||||
// Height is determined based on options (fixed height / label).
|
||||
var minY int
|
||||
if sl.opts.height > 0 {
|
||||
minY = maxY - sl.opts.height
|
||||
|
@ -165,10 +164,10 @@ func (sl *SparkLine) area(cvs *canvas.Canvas) image.Rectangle {
|
|||
)
|
||||
}
|
||||
|
||||
// minSize returns the minimum canvas size for the sparkline based on the options.
|
||||
// minSize returns the minimum canvas size for the SparkLine based on the options.
|
||||
func (sl *SparkLine) minSize() image.Point {
|
||||
// At least one data point.
|
||||
const minWidth = 1
|
||||
const minWidth = 1 // At least one data point.
|
||||
|
||||
var minHeight int
|
||||
if sl.opts.height > 0 {
|
||||
minHeight = sl.opts.height
|
||||
|
@ -190,7 +189,7 @@ func (sl *SparkLine) Options() widgetapi.Options {
|
|||
min := sl.minSize()
|
||||
var max image.Point
|
||||
if sl.opts.height > 0 {
|
||||
max = min
|
||||
max = min // Fix the height to the one specified.
|
||||
}
|
||||
|
||||
return widgetapi.Options{
|
||||
|
|
|
@ -39,7 +39,7 @@ func TestSparkLine(t *testing.T) {
|
|||
desc: "fails on negative data points",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(0, 3, -1, 2)
|
||||
return sl.Add([]int{0, 3, -1, 2})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -51,7 +51,7 @@ func TestSparkLine(t *testing.T) {
|
|||
desc: "single height sparkline",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(0, 1, 2, 3, 4, 5, 6, 7, 8)
|
||||
return sl.Add([]int{0, 1, 2, 3, 4, 5, 6, 7, 8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -65,13 +65,28 @@ func TestSparkLine(t *testing.T) {
|
|||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sparkline can be cleared",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
if err := sl.Add([]int{0, 1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
|
||||
return err
|
||||
}
|
||||
sl.Clear()
|
||||
return nil
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
return faketerm.MustNew(size)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets sparkline color",
|
||||
sparkLine: New(
|
||||
Color(cell.ColorMagenta),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(0, 1, 2, 3, 4, 5, 6, 7, 8)
|
||||
return sl.Add([]int{0, 1, 2, 3, 4, 5, 6, 7, 8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -85,11 +100,30 @@ func TestSparkLine(t *testing.T) {
|
|||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets sparkline color on a call to Add",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add([]int{0, 1, 2, 3, 4, 5, 6, 7, 8}, Color(cell.ColorMagenta))
|
||||
},
|
||||
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{1, 0}, draw.TextCellOpts(
|
||||
cell.FgColor(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(7, 8)
|
||||
return sl.Add([]int{7, 8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -110,7 +144,7 @@ func TestSparkLine(t *testing.T) {
|
|||
Label("Hello"),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(0, 1, 2, 3, 8, 3, 2, 1, 1)
|
||||
return sl.Add([]int{0, 1, 2, 3, 8, 3, 2, 1, 1})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 2),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -132,7 +166,7 @@ func TestSparkLine(t *testing.T) {
|
|||
Label("Hello world"),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(8)
|
||||
return sl.Add([]int{8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 2),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -152,7 +186,7 @@ func TestSparkLine(t *testing.T) {
|
|||
desc: "stretches up to the height of the container",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(0, 100, 50, 85)
|
||||
return sl.Add([]int{0, 100, 50, 85})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 4),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -188,7 +222,7 @@ func TestSparkLine(t *testing.T) {
|
|||
Label("zoo"),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(0, 90, 30, 85)
|
||||
return sl.Add([]int{0, 90, 30, 85})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 4),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -222,7 +256,7 @@ func TestSparkLine(t *testing.T) {
|
|||
Height(2),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(0, 100, 50, 85)
|
||||
return sl.Add([]int{0, 100, 50, 85})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 4),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -250,7 +284,7 @@ func TestSparkLine(t *testing.T) {
|
|||
Height(2),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(0, 100, 50, 0)
|
||||
return sl.Add([]int{0, 100, 50, 0})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 4),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -281,7 +315,7 @@ func TestSparkLine(t *testing.T) {
|
|||
),
|
||||
),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(0, 1)
|
||||
return sl.Add([]int{0, 1})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 9, 2),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -304,7 +338,7 @@ func TestSparkLine(t *testing.T) {
|
|||
desc: "displays only data points that fit the width",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(0, 1, 2, 3, 4, 5, 6, 7, 8)
|
||||
return sl.Add([]int{0, 1, 2, 3, 4, 5, 6, 7, 8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 3, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
@ -323,7 +357,7 @@ func TestSparkLine(t *testing.T) {
|
|||
desc: "data points not visible don't affect the determined max data point",
|
||||
sparkLine: New(),
|
||||
update: func(sl *SparkLine) error {
|
||||
return sl.Add(10, 4, 8)
|
||||
return sl.Add([]int{10, 4, 8})
|
||||
},
|
||||
canvas: image.Rect(0, 0, 2, 1),
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
package sparkline
|
||||
|
||||
import "math"
|
||||
|
||||
// sparks.go contains code that determines which characters should be used to
|
||||
// represent a value on the SparkLine.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// sparks are the characters used to draw the SparkLine.
|
||||
// Note that the last character representing fully populated cell isn't ever
|
||||
// used. If we need to fill the cell fully, we use a space character with background
|
||||
// color set. This ensures we have no gaps between cells.
|
||||
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||
|
||||
// visibleMax determines the maximum visible data point given the canvas width.
|
||||
|
@ -32,13 +34,14 @@ func visibleMax(data []int, width int) ([]int, int) {
|
|||
return data, max
|
||||
}
|
||||
|
||||
// blocks represents blocks that display one value on a SparkLine.
|
||||
// blocks represents the building blocks that display one value on a SparkLine.
|
||||
// I.e. one vertical bar.
|
||||
type blocks struct {
|
||||
// full is the number of fully populated blocks.
|
||||
full int
|
||||
|
||||
// partSpark is the spark character from sparks that should be used in the
|
||||
// topmost block. Equals to zero if no part blocks should be displayed.
|
||||
// topmost block. Equals to zero if no partial block should be displayed.
|
||||
partSpark rune
|
||||
}
|
||||
|
||||
|
@ -50,7 +53,7 @@ func toBlocks(value, max, vertCells int) blocks {
|
|||
return blocks{}
|
||||
}
|
||||
|
||||
// How many of the smallesr spark elements fit into a cell.
|
||||
// How many of the smallest spark elements fit into a cell.
|
||||
cellSparks := len(sparks)
|
||||
|
||||
// Scale is how much of the max does one smallest spark element represent,
|
||||
|
@ -72,7 +75,8 @@ func toBlocks(value, max, vertCells int) blocks {
|
|||
}
|
||||
|
||||
// round returns the nearest integer, rounding half away from zero.
|
||||
// Copied from Go 1.10, package math for backwards compatibility with go 1.8.
|
||||
// Copied from the math package of Go 1.10 for backwards compatibility with Go
|
||||
// 1.8 where the math.Round function doesn't exist yet.
|
||||
func round(x float64) float64 {
|
||||
t := math.Trunc(x)
|
||||
if math.Abs(x-t) >= 0.5 {
|
||||
|
@ -80,3 +84,14 @@ func round(x float64) float64 {
|
|||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// init ensures that all spark characters are half-width runes.
|
||||
// The SparkLine widget assumes that each value can be represented in a column
|
||||
// that has a width of one cell.
|
||||
func init() {
|
||||
for i, s := range sparks {
|
||||
if got := runewidth.RuneWidth(s); got > 1 {
|
||||
panic(fmt.Sprintf("all sparks must be half-width runes (width of one), spark[%d] has width %d", i, got))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue