cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / chart / types_test.go
types_test.go 6.0K
1package chart23import (4 "crypto/sha256"5 "fmt"6 "os"7 "path/filepath"8 "strings"9 "testing"1011 "src.rest/cat/012/internal/sheet"12)1314// imageCases are the images TestImageHashes pins: every type, and the15// options that change how the plot is drawn.16var imageCases = []struct {17 name string18 t sheet.ChartType19 d sheet.ChartData20 o sheet.ChartOptions21}{22 {"column", sheet.ChartColumn, budget, sheet.ChartOptions{}},23 {"column-negative", sheet.ChartColumn, mixed, sheet.ChartOptions{}},24 {"bar", sheet.ChartBar, budget, sheet.ChartOptions{}},25 {"line", sheet.ChartLine, budget, sheet.ChartOptions{}},26 {"pie", sheet.ChartPie, single, sheet.ChartOptions{}},27 {"area", sheet.ChartArea, budget, sheet.ChartOptions{}},28 {"area-stacked", sheet.ChartArea, budget, sheet.ChartOptions{Stack: sheet.StackNormal}},29 {"column-stacked", sheet.ChartColumn, signs, sheet.ChartOptions{Stack: sheet.StackNormal}},30 {"column-percent", sheet.ChartColumn, budget, sheet.ChartOptions{Stack: sheet.StackPercent}},31 {"bar-stacked", sheet.ChartBar, signs, sheet.ChartOptions{Stack: sheet.StackNormal}},32 {"bar-percent", sheet.ChartBar, budget, sheet.ChartOptions{Stack: sheet.StackPercent}},33 {"scatter", sheet.ChartScatter, xy, sheet.ChartOptions{}},34 {"scatter-trend", sheet.ChartScatter, xy, sheet.ChartOptions{Trend: true}},35 {"no-gridlines", sheet.ChartColumn, budget, sheet.ChartOptions{NoGrid: true}},36 {"axis-min-max", sheet.ChartLine, budget, sheet.ChartOptions{Min: 500, HasMin: true, Max: 1600, HasMax: true}},37 {"axis-log", sheet.ChartColumn, growth, sheet.ChartOptions{Log: true}},38 {"legend-right", sheet.ChartColumn, budget, sheet.ChartOptions{Legend: sheet.LegendRight}},39}4041// TestImageHashes pins the image of each case by a hash of its pixels,42// kept in testdata/images.txt (go test ./internal/chart -update rewrites43// it). A changed hash means the image changed: look at it before44// updating.45func TestImageHashes(t *testing.T) {46 var got strings.Builder47 for _, tc := range imageCases {48 img := Image(tc.t, tc.d, 40, 12, Options{CellW: 8, CellH: 16, Chart: tc.o}, testPalette)49 if img == nil {50 t.Errorf("%s: no image", tc.name)51 continue52 }53 sum := sha256.Sum256(img.Pix)54 fmt.Fprintf(&got, "%s %dx%d %x\n", tc.name, img.Bounds().Dx(), img.Bounds().Dy(), sum[:8])55 }56 path := filepath.Join("testdata", "images.txt")57 if *update {58 os.WriteFile(path, []byte(got.String()), 0o644)59 return60 }61 want, err := os.ReadFile(path)62 if err != nil {63 t.Fatalf("missing hashes (go test ./internal/chart -update): %v", err)64 }65 if got.String() != string(want) {66 t.Errorf("images changed:\n%s\nwant:\n%s", got.String(), want)67 }68}6970// TestNewTypesPixels checks a few pixels of the stacked, scatter and71// area images directly.72func TestNewTypesPixels(t *testing.T) {73 o := Options{CellW: 8, CellH: 16}74 // A stacked column: Rent at the bottom, Food above it.75 o.Chart = sheet.ChartOptions{Stack: sheet.StackNormal}76 p := newColumnPlan(budget, 44, 11, columnBars, o.Chart)77 img := Image(sheet.ChartColumn, budget, 44, 12, o, testPalette)78 x0, x1 := p.bar(0, 0)79 mid := ((x0+x1)/2 - p.plot.Min.X) * 880 bottom := img.Bounds().Dy()81 rentTop := int(float64(bottom) - p.sc.pos(1450)*16)82 if c := img.RGBAAt(mid, bottom-2); c != testPalette.Series[0] {83 t.Errorf("stack bottom %v, want Rent's color", c)84 }85 if c := img.RGBAAt(mid, rentTop-4); c != testPalette.Series[1] {86 t.Errorf("stack top %v, want Food's color", c)87 }88 if x0b, _ := p.bar(0, 1); x0b != x0 {89 t.Errorf("stacked series at %d and %d, want one column", x0, x0b)90 }91 // A scatter point is its series' color at its center.92 o.Chart = sheet.ChartOptions{}93 sp := newScatterPlan(xy, 40, 11, o.Chart)94 simg := Image(sheet.ChartScatter, xy, 40, 12, o, testPalette)95 cx := int(sp.xsc.at(1) * 8)96 cy := int(float64(simg.Bounds().Dy()) - sp.ysc.at(52)*16)97 if c := simg.RGBAAt(cx, cy); c != testPalette.Series[0] {98 t.Errorf("scatter point %v at %d,%d", c, cx, cy)99 }100 // The area under a line is filled, above it is clear.101 aimg := Image(sheet.ChartArea, single, 40, 10, o, testPalette)102 b := aimg.Bounds()103 if c := aimg.RGBAAt(2, b.Dy()-3); c.A == 0 {104 t.Errorf("under the area %v, want filled", c)105 }106 if c := aimg.RGBAAt(b.Dx()-2, 2); c.A != 0 {107 t.Errorf("above the area %v, want clear", c)108 }109}110111func TestLogAndFixedScales(t *testing.T) {112 s := newAxis(3, 9000, 10, 2, 6, sheet.Format{}, sheet.ChartOptions{Log: true})113 if s.tick(0) != 1 || s.tick(s.n) != 10000 || s.n != 4 {114 t.Errorf("log scale %v..%v in %d", s.tick(0), s.tick(s.n), s.n)115 }116 if got := s.label(1000); got != "1,000" {117 t.Errorf("log label %q", got)118 }119 if p := s.pos(100); p != float64(2*s.k) {120 t.Errorf("100 at %v cells", p)121 }122 f := newAxis(0, 1500, 9, 2, 6, sheet.Format{}, sheet.ChartOptions{Min: 200, HasMin: true})123 if f.lo != 200 || f.hi < 1500 {124 t.Errorf("fixed minimum: %v..%v", f.lo, f.hi)125 }126 f = newAxis(0, 1500, 8, 2, 6, sheet.Format{}, sheet.ChartOptions{Min: 0, HasMin: true, Max: 2000, HasMax: true})127 if f.lo != 0 || f.hi != 2000 || niceStep(f.step) != f.step {128 t.Errorf("fixed ends: %v..%v step %v", f.lo, f.hi, f.step)129 }130 // A value past a fixed end stops at the end of the axis.131 if at := f.at(5000); at != float64(f.cells()) {132 t.Errorf("clipped at %v", at)133 }134}135136// TestLegendPlacement checks the legend takes its room from the chart.137func TestLegendPlacement(t *testing.T) {138 names := []string{"Rent", "Food"}139 if lg, w, h := placeLegend(names, sheet.LegendRight, 44, 12); lg.pos != sheet.LegendRight || w != 44-6-1 || h != 12 || lg.x != 38 {140 t.Errorf("right: %+v in %dx%d", lg, w, h)141 }142 if lg, w, h := placeLegend(names, sheet.LegendBottom, 44, 12); lg.row != 11 || w != 44 || h != 11 {143 t.Errorf("bottom: %+v in %dx%d", lg, w, h)144 }145 if lg, w, h := placeLegend(names, sheet.LegendNone, 44, 12); lg.pos != sheet.LegendNone || w != 44 || h != 12 {146 t.Errorf("none: %+v in %dx%d", lg, w, h)147 }148 // One series, or too little room: no legend.149 if lg, _, _ := placeLegend(names[:1], sheet.LegendBottom, 44, 12); lg.pos != sheet.LegendNone {150 t.Errorf("one series: %+v", lg)151 }152 if lg, _, _ := placeLegend(names, sheet.LegendRight, 20, 12); lg.pos != sheet.LegendNone {153 t.Errorf("narrow: %+v", lg)154 }155}