Self-review.

This commit is contained in:
Jakub Sobon 2018-06-22 16:27:23 -04:00
parent a9813c4c76
commit 9d02f43c6f
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
3 changed files with 92 additions and 44 deletions

View File

@ -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{

View File

@ -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 {

View File

@ -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))
}
}
}