mirror of https://github.com/mum4k/termdash.git
Code that determines which sparks runes to use.
This commit is contained in:
parent
27f6d76dc6
commit
630151301f
|
@ -0,0 +1,70 @@
|
|||
package sparkline
|
||||
|
||||
import "math"
|
||||
|
||||
// sparks.go contains code that determines which characters should be used to
|
||||
// represent a value on the SparkLine.
|
||||
|
||||
// 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.
|
||||
func visibleMax(data []int, width int) int {
|
||||
if width <= 0 || len(data) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if width < len(data) {
|
||||
data = data[len(data)-width:]
|
||||
}
|
||||
|
||||
var max int
|
||||
for _, v := range data {
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
// blocks represents blocks that display one value on a SparkLine.
|
||||
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.
|
||||
partSpark rune
|
||||
}
|
||||
|
||||
// toBlocks determines the number of full and partial vertical blocks required
|
||||
// to represent the provided value given the specified max visible value and
|
||||
// number of vertical cells available to the SparkLine.
|
||||
func toBlocks(value, max, vertCells int) blocks {
|
||||
if value <= 0 || max <= 0 || vertCells <= 0 {
|
||||
return blocks{}
|
||||
}
|
||||
|
||||
// How many of the smallesr spark elements fit into a cell.
|
||||
cellSparks := len(sparks)
|
||||
|
||||
// Scale is how much of the max does one smallest spark element represent,
|
||||
// given the vertical cells that will be used to represent the value.
|
||||
scale := float64(cellSparks) * float64(vertCells) / float64(max)
|
||||
|
||||
// How many smallest spark elements are needed to represent the value.
|
||||
elements := int(math.Round(float64(value) * scale))
|
||||
|
||||
b := blocks{
|
||||
full: elements / cellSparks,
|
||||
}
|
||||
|
||||
part := elements % cellSparks
|
||||
if part > 0 {
|
||||
b.partSpark = sparks[part-1]
|
||||
}
|
||||
return b
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
package sparkline
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
)
|
||||
|
||||
func TestVisibleMax(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
data []int
|
||||
width int
|
||||
want int
|
||||
}{
|
||||
{
|
||||
desc: "zero for no data",
|
||||
width: 3,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
desc: "zero for zero width",
|
||||
data: []int{0, 1},
|
||||
width: 0,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
desc: "zero for negative width",
|
||||
data: []int{0, 1},
|
||||
width: -1,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
desc: "all values are zero",
|
||||
data: []int{0, 0, 0},
|
||||
width: 3,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
desc: "all values are visible",
|
||||
data: []int{8, 0, 1},
|
||||
width: 3,
|
||||
want: 8,
|
||||
},
|
||||
{
|
||||
desc: "width greater than number of values",
|
||||
data: []int{8, 0, 1},
|
||||
width: 10,
|
||||
want: 8,
|
||||
},
|
||||
{
|
||||
desc: "only some values are visible",
|
||||
data: []int{8, 2, 1},
|
||||
width: 2,
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
desc: "only one value is visible",
|
||||
data: []int{8, 2, 1},
|
||||
width: 1,
|
||||
want: 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToBlocks(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
value int
|
||||
max int
|
||||
vertCells int
|
||||
want blocks
|
||||
}{
|
||||
{
|
||||
desc: "zero value has no blocks",
|
||||
value: 0,
|
||||
max: 10,
|
||||
vertCells: 2,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "negative value has no blocks",
|
||||
value: -1,
|
||||
max: 10,
|
||||
vertCells: 2,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "zero max has no blocks",
|
||||
value: 10,
|
||||
max: 0,
|
||||
vertCells: 2,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "negative max has no blocks",
|
||||
value: 10,
|
||||
max: -1,
|
||||
vertCells: 2,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "zero vertCells has no blocks",
|
||||
value: 10,
|
||||
max: 10,
|
||||
vertCells: 0,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "negative vertCells has no blocks",
|
||||
value: 10,
|
||||
max: 10,
|
||||
vertCells: -1,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "single line, zero value",
|
||||
value: 0,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 1/8",
|
||||
value: 1,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[0]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 2/8",
|
||||
value: 2,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[1]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 3/8",
|
||||
value: 3,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[2]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 4/8",
|
||||
value: 4,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[3]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 5/8",
|
||||
value: 5,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[4]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 6/8",
|
||||
value: 6,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[5]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 7/8",
|
||||
value: 7,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 0, partSpark: sparks[6]},
|
||||
},
|
||||
{
|
||||
desc: "single line, value is 8/8",
|
||||
value: 8,
|
||||
max: 8,
|
||||
vertCells: 1,
|
||||
want: blocks{full: 1, partSpark: 0},
|
||||
},
|
||||
{
|
||||
desc: "multi line, zero value",
|
||||
value: 0,
|
||||
max: 24,
|
||||
vertCells: 3,
|
||||
want: blocks{},
|
||||
},
|
||||
{
|
||||
desc: "multi line, lowest block is partial",
|
||||
value: 2,
|
||||
max: 24,
|
||||
vertCells: 3,
|
||||
want: blocks{full: 0, partSpark: sparks[1]},
|
||||
},
|
||||
{
|
||||
desc: "multi line, two full blocks, no partial block",
|
||||
value: 16,
|
||||
max: 24,
|
||||
vertCells: 3,
|
||||
want: blocks{full: 2, partSpark: 0},
|
||||
},
|
||||
{
|
||||
desc: "multi line, topmost block is partial",
|
||||
value: 20,
|
||||
max: 24,
|
||||
vertCells: 3,
|
||||
want: blocks{full: 2, partSpark: sparks[3]},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got := toBlocks(tc.value, tc.max, tc.vertCells)
|
||||
if diff := pretty.Compare(tc.want, got); diff != "" {
|
||||
t.Errorf("toBlocks => unexpected diff (-want, +got):\n%s", diff)
|
||||
if got.full != tc.want.full {
|
||||
t.Errorf("toBlocks => unexpected diff, blocks.full got %d, want %d", got.full, tc.want.full)
|
||||
}
|
||||
if got.partSpark != tc.want.partSpark {
|
||||
t.Errorf("toBlocks => unexpected diff, blocks.partSpark got '%c' (sparks[%d])), want '%c' (sparks[%d])",
|
||||
got.partSpark, findRune(got.partSpark, sparks), tc.want.partSpark, findRune(tc.want.partSpark, sparks))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// findRune finds the rune in the slice and returns its index.
|
||||
// Returns -1 if the rune isn't in the slice.
|
||||
func findRune(target rune, runes []rune) int {
|
||||
for i, r := range runes {
|
||||
if r == target {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
Loading…
Reference in New Issue