Bars, lines, areas, pies, donuts, radar, histogram, scatter, bubbles, heatmaps, gauge, bullet, funnel, waterfall, treemap, candles, box plots, steps, rings, legend, axis, crosshair, polar area, pyramid, spectrum bars and win-loss strips, each drawn dot by dot. Ticks follow 1-2-5 steps and print as precisely as their step needs; a label row uses micro letters or the pixel font as a whole. Descriptions carry the data in numbers. Bad data panics.
153 lines
4.8 KiB
Go
153 lines
4.8 KiB
Go
package chart_test
|
||
|
||
import (
|
||
"image"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/chart"
|
||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||
)
|
||
|
||
var hp = []chart.Series{{Name: "HP", Values: []float64{10, 30, 20, 40}, Slot: pixel.Good}, {Name: "MP", Values: []float64{5, 5, 15, 10}, Slot: pixel.Info}}
|
||
|
||
func TestLineChartJoinsAndMarksPoints(t *testing.T) {
|
||
tt := view(400, 200, newClock(), func(c *ui.Context) {
|
||
chart.LineChart(c, "Stats", days, hp, chart.LineChartOptions{})
|
||
})
|
||
img := tt.Image()
|
||
good, info := count(t, img, 0, 0, 160, 80, pixel.Good), count(t, img, 0, 0, 160, 80, pixel.Info)
|
||
if good < 100 || info < 100 {
|
||
t.Fatalf("line dots: HP %d, MP %d", good, info)
|
||
}
|
||
// One-dot lines leave whole 3×3 blocks only at the markers.
|
||
if hb, mb := blocks(t, img, pixel.Good), blocks(t, img, pixel.Info); hb != 4 || mb != 4 {
|
||
t.Fatalf("markers: HP %d, MP %d", hb, mb)
|
||
}
|
||
keeps(t, img)
|
||
panics(t, "a series too short", func() {
|
||
view(100, 100, newClock(), func(c *ui.Context) {
|
||
chart.LineChart(c, "x", days, []chart.Series{{Name: "a", Values: []float64{1}}}, chart.LineChartOptions{})
|
||
})
|
||
})
|
||
}
|
||
|
||
func TestAreaChartDithersBelowTheLine(t *testing.T) {
|
||
tt := view(400, 200, newClock(), func(c *ui.Context) {
|
||
chart.AreaChart(c, "Gold", days, []float64{20, 20, 20, 20}, chart.AreaChartOptions{Slot: pixel.Warn})
|
||
})
|
||
img := tt.Image()
|
||
// Under a flat line, the area mixes Warn and ground.
|
||
lit, ground := count(t, img, 60, 50, 40, 10, pixel.Warn), count(t, img, 60, 50, 40, 10, pixel.Back)
|
||
if lit == 0 || ground == 0 {
|
||
t.Fatalf("area under the line: %d Warn, %d ground", lit, ground)
|
||
}
|
||
keeps(t, img)
|
||
}
|
||
|
||
func TestAreaChartFillsBelowZeroToo(t *testing.T) {
|
||
tt := view(400, 200, newClock(), func(c *ui.Context) {
|
||
chart.AreaChart(c, "Debt", days, []float64{-20, -20, -20, -20}, chart.AreaChartOptions{Slot: pixel.Bad})
|
||
})
|
||
img := tt.Image()
|
||
// Zero tops the plot; the area hangs below it.
|
||
lit, ground := count(t, img, 60, 10, 40, 10, pixel.Bad), count(t, img, 60, 10, 40, 10, pixel.Back)
|
||
if lit == 0 || ground == 0 {
|
||
t.Fatalf("area above the line: %d Bad, %d ground", lit, ground)
|
||
}
|
||
}
|
||
|
||
func TestStepChartHoldsThenSteps(t *testing.T) {
|
||
tt := view(400, 200, newClock(), func(c *ui.Context) {
|
||
chart.StepChart(c, "Level", days, []float64{1, 1, 3, 3}, chart.StepChartOptions{Slot: pixel.Magic})
|
||
})
|
||
img := tt.Image()
|
||
// A flat run of a whole share holds one row.
|
||
rows := map[int]bool{}
|
||
for x := 20; x < 40; x++ {
|
||
for y := range 80 {
|
||
if at(t, img, x, y) == pixel.Magic {
|
||
rows[y] = true
|
||
}
|
||
}
|
||
}
|
||
if len(rows) != 1 {
|
||
t.Fatalf("the first hold spans rows %v", rows)
|
||
}
|
||
}
|
||
|
||
func TestSparklineMarksItsEnd(t *testing.T) {
|
||
tt := view(200, 60, newClock(), func(c *ui.Context) {
|
||
chart.Sparkline(c, "Ping", []float64{3, 9, 4, 12}, chart.SparklineOptions{Slot: pixel.Info})
|
||
})
|
||
img := tt.Image()
|
||
if count(t, img, 0, 0, 48, 16, pixel.Info) < 30 || count(t, img, 44, 0, 4, 16, pixel.Accent) == 0 {
|
||
t.Fatal("no line, or no mark at its end")
|
||
}
|
||
panics(t, "one value", func() {
|
||
view(100, 60, newClock(), func(c *ui.Context) { chart.Sparkline(c, "x", []float64{1}, chart.SparklineOptions{}) })
|
||
})
|
||
}
|
||
|
||
func TestCrosshairCrossesOnHover(t *testing.T) {
|
||
tt := view(400, 200, newClock(), func(c *ui.Context) {
|
||
chart.Crosshair(c, "Stats", days, hp, chart.CrosshairOptions{})
|
||
})
|
||
if n := count(t, tt.Image(), 0, 0, 160, 80, pixel.Lit); n != 0 {
|
||
t.Fatal("a readout shows before hover")
|
||
}
|
||
tt.Move(2*90, 2*30)
|
||
img := tt.Image()
|
||
if count(t, img, 0, 0, 160, 80, pixel.Lit) == 0 || count(t, img, 0, 30, 160, 1, pixel.Ink) < 20 {
|
||
t.Fatal("hover drew no crosshair or readout")
|
||
}
|
||
keeps(t, img)
|
||
}
|
||
|
||
func TestSpectrumPeaksFallAndRest(t *testing.T) {
|
||
clk := newClock()
|
||
levels := []float64{1, 0.5}
|
||
tt := view(200, 100, clk, func(c *ui.Context) {
|
||
chart.SpectrumBars(c, "EQ", levels, chart.SpectrumBarsOptions{})
|
||
})
|
||
img := tt.Image()
|
||
if lit := count(t, img, 0, 0, 5, 30, pixel.Shade); lit != 0 {
|
||
t.Fatalf("a full bar shows %d dark dots", lit)
|
||
}
|
||
// Five idle seconds do not count toward the fall.
|
||
clk.t = clk.t.Add(5 * time.Second)
|
||
levels[0] = 0
|
||
tt.Frame()
|
||
if top, _ := column(t, tt.Image(), 2, 30, pixel.Ink); top != 0 {
|
||
t.Fatalf("at the drop the peak sits at row %d", top)
|
||
}
|
||
clk.t = clk.t.Add(300 * time.Millisecond)
|
||
tt.Frame()
|
||
// After 0.3 s the peak has fallen three blocks.
|
||
top, _ := column(t, tt.Image(), 2, 30, pixel.Ink)
|
||
if top != 9 {
|
||
t.Fatalf("the peak sits at row %d", top)
|
||
}
|
||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||
tt.Frame()
|
||
if top, _ := column(t, tt.Image(), 2, 30, pixel.Ink); top != -1 {
|
||
t.Fatalf("reduced, the peak still floats at %d", top)
|
||
}
|
||
}
|
||
|
||
// blocks counts 3×3 squares of s on a 160×80 chart.
|
||
func blocks(t *testing.T, img *image.RGBA, s pixel.Slot) int {
|
||
t.Helper()
|
||
n := 0
|
||
for y := range 78 {
|
||
for x := range 158 {
|
||
if count(t, img, x, y, 3, 3, s) == 9 {
|
||
n++
|
||
}
|
||
}
|
||
}
|
||
return n
|
||
}
|