mirror of https://github.com/mum4k/termdash.git
More test coverage for linechart.
This commit is contained in:
parent
1db0cfc7f1
commit
bc911a3cd6
|
@ -165,7 +165,9 @@ type XDetails struct {
|
|||
// of the provided area. The yStart is the point where the Y axis starts.
|
||||
// The numPoints is the number of points in the largest series that will be
|
||||
// plotted.
|
||||
func NewXDetails(numPoints int, yStart image.Point, cvsAr image.Rectangle) (*XDetails, error) {
|
||||
// customLabels are the desired labels for the X axis, these are preferred if
|
||||
// provided.
|
||||
func NewXDetails(numPoints int, yStart image.Point, cvsAr image.Rectangle, customLabels map[int]string) (*XDetails, error) {
|
||||
if min := 3; cvsAr.Dy() < min {
|
||||
return nil, fmt.Errorf("the canvas isn't tall enough to accommodate the X axis, its labels and the line chart, got height %d, minimum is %d", cvsAr.Dy(), min)
|
||||
}
|
||||
|
@ -180,7 +182,7 @@ func NewXDetails(numPoints int, yStart image.Point, cvsAr image.Rectangle) (*XDe
|
|||
// One point horizontally for the Y axis.
|
||||
// Two points vertically, one for the X axis and one for its labels.
|
||||
graphZero := image.Point{yStart.X + 1, cvsAr.Dy() - 3}
|
||||
labels, err := xLabels(scale, graphZero)
|
||||
labels, err := xLabels(scale, graphZero, customLabels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -142,13 +142,14 @@ func TestY(t *testing.T) {
|
|||
|
||||
func TestNewXDetails(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
numPoints int
|
||||
yStart image.Point
|
||||
cvsWidth int
|
||||
cvsAr image.Rectangle
|
||||
want *XDetails
|
||||
wantErr bool
|
||||
desc string
|
||||
numPoints int
|
||||
yStart image.Point
|
||||
cvsWidth int
|
||||
cvsAr image.Rectangle
|
||||
customLabels map[int]string
|
||||
want *XDetails
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails when numPoints is negative",
|
||||
|
@ -209,7 +210,7 @@ func TestNewXDetails(t *testing.T) {
|
|||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got, err := NewXDetails(tc.numPoints, tc.yStart, tc.cvsAr)
|
||||
got, err := NewXDetails(tc.numPoints, tc.yStart, tc.cvsAr, tc.customLabels)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("NewXDetails => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
|
|
|
@ -157,14 +157,16 @@ func (xs *xSpace) Sub(size int) error {
|
|||
// Labels are returned in an increasing value order.
|
||||
// Returned labels shouldn't be trimmed, their count is adjusted so that they
|
||||
// fit under the width of the axis.
|
||||
func xLabels(scale *XScale, graphZero image.Point) ([]*Label, error) {
|
||||
// The customLabels map value positions in the series to the desired custom
|
||||
// label. These are preferred if present.
|
||||
func xLabels(scale *XScale, graphZero image.Point, customLabels map[int]string) ([]*Label, error) {
|
||||
space := newXSpace(graphZero, scale.GraphWidth)
|
||||
const minSpacing = 3
|
||||
var res []*Label
|
||||
|
||||
next := 0
|
||||
for haveLabels := 0; haveLabels <= int(scale.Max.Value); haveLabels = len(res) {
|
||||
label, err := colLabel(scale, space)
|
||||
label, err := colLabel(scale, space, next, customLabels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -200,14 +202,20 @@ func xLabels(scale *XScale, graphZero image.Point) ([]*Label, error) {
|
|||
// colLabel returns a label placed either at the beginning of the space.
|
||||
// The space is adjusted according to how much space was taken by the label.
|
||||
// Returns nil, nil if the label doesn't fit in the space.
|
||||
func colLabel(scale *XScale, space *xSpace) (*Label, error) {
|
||||
pos := space.Relative()
|
||||
v, err := scale.CellLabel(pos.X)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to determine label value for column %d: %v", pos.X, err)
|
||||
func colLabel(scale *XScale, space *xSpace, labelNum int, customLabels map[int]string) (*Label, error) {
|
||||
var val *Value
|
||||
if custom, ok := customLabels[labelNum]; ok {
|
||||
val = NewTextValue(custom)
|
||||
} else {
|
||||
pos := space.Relative()
|
||||
v, err := scale.CellLabel(pos.X)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to determine label value for column %d: %v", pos.X, err)
|
||||
}
|
||||
val = v
|
||||
}
|
||||
|
||||
labelLen := len(v.Text())
|
||||
labelLen := len(val.Text())
|
||||
if labelLen > space.Remaining() {
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -218,7 +226,7 @@ func colLabel(scale *XScale, space *xSpace) (*Label, error) {
|
|||
}
|
||||
|
||||
return &Label{
|
||||
Value: v,
|
||||
Value: val,
|
||||
Pos: abs,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -151,12 +151,13 @@ func TestYLabels(t *testing.T) {
|
|||
func TestXLabels(t *testing.T) {
|
||||
const nonZeroDecimals = 2
|
||||
tests := []struct {
|
||||
desc string
|
||||
numPoints int
|
||||
graphWidth int
|
||||
graphZero image.Point
|
||||
want []*Label
|
||||
wantErr bool
|
||||
desc string
|
||||
numPoints int
|
||||
graphWidth int
|
||||
graphZero image.Point
|
||||
customLabels map[int]string
|
||||
want []*Label
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "only one point",
|
||||
|
@ -246,6 +247,50 @@ func TestXLabels(t *testing.T) {
|
|||
{NewValue(3, nonZeroDecimals), image.Point{94, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "custom labels provided",
|
||||
numPoints: 4,
|
||||
graphWidth: 100,
|
||||
graphZero: image.Point{0, 1},
|
||||
customLabels: map[int]string{
|
||||
0: "a",
|
||||
1: "b",
|
||||
2: "c",
|
||||
3: "d",
|
||||
},
|
||||
want: []*Label{
|
||||
{NewTextValue("a"), image.Point{0, 3}},
|
||||
{NewTextValue("b"), image.Point{31, 3}},
|
||||
{NewTextValue("c"), image.Point{62, 3}},
|
||||
{NewTextValue("d"), image.Point{94, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "only some custom labels provided",
|
||||
numPoints: 4,
|
||||
graphWidth: 100,
|
||||
graphZero: image.Point{0, 1},
|
||||
customLabels: map[int]string{
|
||||
0: "a",
|
||||
3: "d",
|
||||
},
|
||||
want: []*Label{
|
||||
{NewTextValue("a"), image.Point{0, 3}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{31, 3}},
|
||||
{NewValue(2, nonZeroDecimals), image.Point{62, 3}},
|
||||
{NewTextValue("d"), image.Point{94, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "not displayed if custom labels don't fit",
|
||||
numPoints: 2,
|
||||
graphWidth: 6,
|
||||
graphZero: image.Point{0, 1},
|
||||
customLabels: map[int]string{
|
||||
0: "a very very long custom label",
|
||||
},
|
||||
want: []*Label{},
|
||||
},
|
||||
{
|
||||
desc: "more points than pixels",
|
||||
numPoints: 100,
|
||||
|
@ -265,7 +310,7 @@ func TestXLabels(t *testing.T) {
|
|||
t.Fatalf("NewXScale => unexpected error: %v", err)
|
||||
}
|
||||
t.Logf("scale step: %v", scale.Step.Rounded)
|
||||
got, err := xLabels(scale, tc.graphZero)
|
||||
got, err := xLabels(scale, tc.graphZero, tc.customLabels)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("xLabels => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
|
|
|
@ -36,6 +36,9 @@ type Value struct {
|
|||
// NonZeroDecimals indicates the rounding precision used, it is provided on
|
||||
// a call to newValue.
|
||||
NonZeroDecimals int
|
||||
|
||||
// text value if this value was constructed using NewTextValue.
|
||||
text string
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
|
@ -55,8 +58,20 @@ func NewValue(v float64, nonZeroDecimals int) *Value {
|
|||
}
|
||||
}
|
||||
|
||||
// NewTextValue constructs a value out of the provided text.
|
||||
func NewTextValue(text string) *Value {
|
||||
return &Value{
|
||||
Value: math.NaN(),
|
||||
Rounded: math.NaN(),
|
||||
text: text,
|
||||
}
|
||||
}
|
||||
|
||||
// Text returns textual representation of the value.
|
||||
func (v *Value) Text() string {
|
||||
if v.text != "" {
|
||||
return v.text
|
||||
}
|
||||
if math.Ceil(v.Rounded) == v.Rounded {
|
||||
return fmt.Sprintf("%.0f", v.Rounded)
|
||||
}
|
||||
|
|
|
@ -115,3 +115,12 @@ func TestText(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTextValue(t *testing.T) {
|
||||
const want = "foo"
|
||||
v := NewTextValue(want)
|
||||
got := v.Text()
|
||||
if got != want {
|
||||
t.Errorf("v.Text => got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,12 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/braille"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
"github.com/mum4k/termdash/numbers"
|
||||
"github.com/mum4k/termdash/terminalapi"
|
||||
|
@ -39,6 +40,12 @@ type seriesValues struct {
|
|||
min float64
|
||||
// max is the largest value, zero if values is empty.
|
||||
max float64
|
||||
|
||||
seriesCellOpts []cell.Option
|
||||
// The custom labels provided on a call to Series and a bool indicating if
|
||||
// the labels were provided. This allows resetting them to nil.
|
||||
xLabelsSet bool
|
||||
xLabels map[int]string
|
||||
}
|
||||
|
||||
// newSeriesValues returns a new seriesValues instance.
|
||||
|
@ -76,6 +83,9 @@ type LineChart struct {
|
|||
|
||||
// opts are the provided options.
|
||||
opts *options
|
||||
|
||||
// xLabels that were provided on a call to Series.
|
||||
xLabels map[int]string
|
||||
}
|
||||
|
||||
// New returns a new line chart widget.
|
||||
|
@ -88,10 +98,47 @@ func New(opts ...Option) *LineChart {
|
|||
}
|
||||
}
|
||||
|
||||
// SeriesOption is used to provide options to Series.
|
||||
type SeriesOption interface {
|
||||
// set sets the provided option.
|
||||
set(*seriesValues)
|
||||
}
|
||||
|
||||
// seriesOption implements SeriesOption.
|
||||
type seriesOption func(*seriesValues)
|
||||
|
||||
// set implements SeriesOption.set.
|
||||
func (so seriesOption) set(sv *seriesValues) {
|
||||
so(sv)
|
||||
}
|
||||
|
||||
// SeriesCellOpts sets the cell options for this series.
|
||||
// Note that the braille canvas has resolution of 2x4 pixels per cell, but each
|
||||
// cell can only have one set of cell options set. Meaning that where series
|
||||
// share a cell, the last drawn series sets the cell options. Series are drawn
|
||||
// in alphabetical order based on their name.
|
||||
func SeriesCellOpts(co ...cell.Option) SeriesOption {
|
||||
return seriesOption(func(opts *seriesValues) {
|
||||
opts.seriesCellOpts = co
|
||||
})
|
||||
}
|
||||
|
||||
// SeriesXLabels is used to provide custom labels for the X axis.
|
||||
// The argument maps the positions in the provided series to the desired label.
|
||||
// The labels are only used if they fit under the axis.
|
||||
// Custom labels are property of the line chart, since there is only one X axis,
|
||||
// providing multiple custom labels overwrites the previous value.
|
||||
func SeriesXLabels(labels map[int]string) SeriesOption {
|
||||
return seriesOption(func(opts *seriesValues) {
|
||||
opts.xLabelsSet = true
|
||||
opts.xLabels = labels
|
||||
})
|
||||
}
|
||||
|
||||
// Series sets the values that should be displayed as the line chart with the
|
||||
// provided label.
|
||||
// Subsequent calls with the same label replace any previously provided values.
|
||||
func (lc *LineChart) Series(label string, values []float64) error {
|
||||
func (lc *LineChart) Series(label string, values []float64, opts ...SeriesOption) error {
|
||||
if label == "" {
|
||||
return errors.New("the label cannot be empty")
|
||||
}
|
||||
|
@ -100,6 +147,21 @@ func (lc *LineChart) Series(label string, values []float64) error {
|
|||
defer lc.mu.Unlock()
|
||||
|
||||
series := newSeriesValues(values)
|
||||
for _, opt := range opts {
|
||||
opt.set(series)
|
||||
}
|
||||
if series.xLabelsSet {
|
||||
for i, t := range series.xLabels {
|
||||
if i < 0 {
|
||||
return fmt.Errorf("invalid key %d -> %q provided in SeriesXLabels, keys must be positive", i, t)
|
||||
}
|
||||
if t == "" {
|
||||
return fmt.Errorf("invalid label %d -> %q provided in SeriesXLabels, values cannot be empty", i, t)
|
||||
}
|
||||
}
|
||||
lc.xLabels = series.xLabels
|
||||
}
|
||||
|
||||
lc.series[label] = series
|
||||
lc.yAxis = axes.NewY(series.min, series.max)
|
||||
return nil
|
||||
|
@ -116,7 +178,7 @@ func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
|
|||
return fmt.Errorf("lc.yAxis.Details => %v", err)
|
||||
}
|
||||
|
||||
xd, err := axes.NewXDetails(lc.maxPoints(), yd.Start, cvs.Area())
|
||||
xd, err := axes.NewXDetails(lc.maxPoints(), yd.Start, cvs.Area(), lc.xLabels)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewXDetails => %v", err)
|
||||
}
|
||||
|
@ -133,7 +195,7 @@ func (lc *LineChart) drawAxes(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YD
|
|||
{Start: yd.Start, End: yd.End},
|
||||
{Start: xd.Start, End: xd.End},
|
||||
}
|
||||
if err := draw.HVLines(cvs, lines); err != nil {
|
||||
if err := draw.HVLines(cvs, lines, draw.HVLineCellOpts(lc.opts.axesCellOpts...)); err != nil {
|
||||
return fmt.Errorf("failed to draw the axes: %v", err)
|
||||
}
|
||||
|
||||
|
@ -141,13 +203,14 @@ func (lc *LineChart) drawAxes(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YD
|
|||
if err := draw.Text(cvs, l.Value.Text(), l.Pos,
|
||||
draw.TextMaxX(yd.Start.X),
|
||||
draw.TextOverrunMode(draw.OverrunModeThreeDot),
|
||||
draw.TextCellOpts(lc.opts.yLabelCellOpts...),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to draw the Y labels: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range xd.Labels {
|
||||
if err := draw.Text(cvs, l.Value.Text(), l.Pos); err != nil {
|
||||
if err := draw.Text(cvs, l.Value.Text(), l.Pos, draw.TextCellOpts(lc.opts.xLabelCellOpts...)); err != nil {
|
||||
return fmt.Errorf("failed to draw the X labels: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -158,12 +221,19 @@ func (lc *LineChart) drawAxes(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YD
|
|||
func (lc *LineChart) drawSeries(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) error {
|
||||
// The area available to the graph.
|
||||
graphAr := image.Rect(yd.Start.X+1, yd.Start.Y, cvs.Area().Max.X, xd.End.Y)
|
||||
log.Printf("graphAr:%v", graphAr)
|
||||
bc, err := braille.New(graphAr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("braille.New => %v", err)
|
||||
}
|
||||
for name, sv := range lc.series {
|
||||
|
||||
var names []string
|
||||
for name := range lc.series {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
sv := lc.series[name]
|
||||
if len(sv.values) <= 1 {
|
||||
continue
|
||||
}
|
||||
|
@ -189,10 +259,11 @@ func (lc *LineChart) drawSeries(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.
|
|||
return fmt.Errorf("failure for series %v[%d], yd.Scale.ValueToPixel => %v", name, i, err)
|
||||
}
|
||||
|
||||
start := image.Point{startX, startY}
|
||||
end := image.Point{endX, endY}
|
||||
log.Printf("start:%v, end:%v", start, end)
|
||||
if err := draw.BrailleLine(bc, image.Point{startX, startY}, image.Point{endX, endY}); err != nil {
|
||||
if err := draw.BrailleLine(bc,
|
||||
image.Point{startX, startY},
|
||||
image.Point{endX, endY},
|
||||
draw.BrailleLineCellOpts(sv.seriesCellOpts...),
|
||||
); err != nil {
|
||||
return fmt.Errorf("draw.BrailleLine => %v", err)
|
||||
}
|
||||
prev = v
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/braille/testbraille"
|
||||
"github.com/mum4k/termdash/canvas/testcanvas"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
"github.com/mum4k/termdash/draw/testdraw"
|
||||
"github.com/mum4k/termdash/terminal/faketerm"
|
||||
|
@ -39,13 +40,29 @@ func TestLineChartDraws(t *testing.T) {
|
|||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "write fails without name for the series",
|
||||
desc: "series fails without name for the series",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("", nil)
|
||||
},
|
||||
wantWriteErr: true,
|
||||
},
|
||||
{
|
||||
desc: "series fails when custom label has negative key",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("series", nil, SeriesXLabels(map[int]string{-1: "text"}))
|
||||
},
|
||||
wantWriteErr: true,
|
||||
},
|
||||
{
|
||||
desc: "series fails when custom label has empty value",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("series", nil, SeriesXLabels(map[int]string{1: ""}))
|
||||
},
|
||||
wantWriteErr: true,
|
||||
},
|
||||
{
|
||||
desc: "draw fails when canvas not wide enough",
|
||||
canvas: image.Rect(0, 0, 2, 4),
|
||||
|
@ -78,6 +95,66 @@ func TestLineChartDraws(t *testing.T) {
|
|||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets axes cell options",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
opts: []Option{
|
||||
AxesCellOpts(
|
||||
cell.BgColor(cell.ColorRed),
|
||||
cell.FgColor(cell.ColorGreen),
|
||||
),
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
// Y and X axis.
|
||||
lines := []draw.HVLine{
|
||||
{Start: image.Point{1, 0}, End: image.Point{1, 2}},
|
||||
{Start: image.Point{1, 2}, End: image.Point{2, 2}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines, draw.HVLineCellOpts(cell.BgColor(cell.ColorRed), cell.FgColor(cell.ColorGreen)))
|
||||
|
||||
// Zero value labels.
|
||||
testdraw.MustText(c, "0", image.Point{0, 1})
|
||||
testdraw.MustText(c, "0", image.Point{2, 3})
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets label cell options",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
opts: []Option{
|
||||
XLabelCellOpts(
|
||||
cell.BgColor(cell.ColorYellow),
|
||||
cell.FgColor(cell.ColorBlue),
|
||||
),
|
||||
YLabelCellOpts(
|
||||
cell.BgColor(cell.ColorRed),
|
||||
cell.FgColor(cell.ColorGreen),
|
||||
),
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
// Y and X axis.
|
||||
lines := []draw.HVLine{
|
||||
{Start: image.Point{1, 0}, End: image.Point{1, 2}},
|
||||
{Start: image.Point{1, 2}, End: image.Point{2, 2}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Zero value labels.
|
||||
testdraw.MustText(c, "0", image.Point{0, 1}, draw.TextCellOpts(cell.BgColor(cell.ColorRed), cell.FgColor(cell.ColorGreen)))
|
||||
testdraw.MustText(c, "0", image.Point{2, 3}, draw.TextCellOpts(cell.BgColor(cell.ColorYellow), cell.FgColor(cell.ColorBlue)))
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "two Y and X labels",
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
|
@ -111,6 +188,74 @@ func TestLineChartDraws(t *testing.T) {
|
|||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "custom X labels",
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("first", []float64{0, 100}, SeriesXLabels(map[int]string{
|
||||
0: "start",
|
||||
1: "end",
|
||||
}))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
// Y and X axis.
|
||||
lines := []draw.HVLine{
|
||||
{Start: image.Point{5, 0}, End: image.Point{5, 8}},
|
||||
{Start: image.Point{5, 8}, End: image.Point{19, 8}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "0", image.Point{4, 7})
|
||||
testdraw.MustText(c, "51.68", image.Point{0, 3})
|
||||
testdraw.MustText(c, "start", image.Point{6, 9})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(6, 0, 20, 8)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{26, 0})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets series cell options",
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("first", []float64{0, 100}, SeriesCellOpts(cell.BgColor(cell.ColorRed), cell.FgColor(cell.ColorGreen)))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
// Y and X axis.
|
||||
lines := []draw.HVLine{
|
||||
{Start: image.Point{5, 0}, End: image.Point{5, 8}},
|
||||
{Start: image.Point{5, 8}, End: image.Point{19, 8}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "0", image.Point{4, 7})
|
||||
testdraw.MustText(c, "51.68", image.Point{0, 3})
|
||||
testdraw.MustText(c, "0", image.Point{6, 9})
|
||||
testdraw.MustText(c, "1", image.Point{19, 9})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(6, 0, 20, 8)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{26, 0}, draw.BrailleLineCellOpts(cell.BgColor(cell.ColorRed), cell.FgColor(cell.ColorGreen)))
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple Y and X labels",
|
||||
canvas: image.Rect(0, 0, 20, 11),
|
||||
|
@ -217,13 +362,44 @@ func TestLineChartDraws(t *testing.T) {
|
|||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draw multiple series with different cell options, last series wins where they cross",
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
if err := lc.Series("first", []float64{0, 50, 100}, SeriesCellOpts(cell.FgColor(cell.ColorRed))); err != nil {
|
||||
return err
|
||||
}
|
||||
return lc.Series("second", []float64{100, 0}, SeriesCellOpts(cell.FgColor(cell.ColorBlue)))
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
c := testcanvas.MustNew(ft.Area())
|
||||
|
||||
// Sets axis colors.
|
||||
// Sets label colors on Y axis.
|
||||
// Sets label colors on X axis.
|
||||
// Sets series color.
|
||||
// Multiple series, same color.
|
||||
// Multiple series, different color.
|
||||
// Y and X axis.
|
||||
lines := []draw.HVLine{
|
||||
{Start: image.Point{5, 0}, End: image.Point{5, 8}},
|
||||
{Start: image.Point{5, 8}, End: image.Point{19, 8}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "0", image.Point{4, 7})
|
||||
testdraw.MustText(c, "51.68", image.Point{0, 3})
|
||||
testdraw.MustText(c, "0", image.Point{6, 9})
|
||||
testdraw.MustText(c, "1", image.Point{12, 9})
|
||||
testdraw.MustText(c, "2", image.Point{19, 9})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(6, 0, 20, 8)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{27, 0}, draw.BrailleLineCellOpts(cell.FgColor(cell.ColorRed)))
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{13, 31}, draw.BrailleLineCellOpts(cell.FgColor(cell.ColorBlue)))
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/mum4k/termdash"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/container"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
"github.com/mum4k/termdash/terminal/termbox"
|
||||
|
@ -51,7 +52,18 @@ func playLineChart(ctx context.Context, lc *linechart.LineChart, delay time.Dura
|
|||
case <-ticker.C:
|
||||
i = (i + 1) % len(inputs)
|
||||
rotated := append(inputs[i:], inputs[:i]...)
|
||||
if err := lc.Series("sine", rotated); err != nil {
|
||||
if err := lc.Series("first", rotated,
|
||||
linechart.SeriesCellOpts(cell.FgColor(cell.ColorBlue)),
|
||||
linechart.SeriesXLabels(map[int]string{
|
||||
0: "zero",
|
||||
}),
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
i2 := (i + 100) % len(inputs)
|
||||
rotated2 := append(inputs[i2:], inputs[:i2]...)
|
||||
if err := lc.Series("second", rotated2, linechart.SeriesCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
@ -70,7 +82,11 @@ func main() {
|
|||
|
||||
const redrawInterval = 25 * time.Millisecond
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
lc := linechart.New()
|
||||
lc := linechart.New(
|
||||
linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
|
||||
linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
||||
linechart.XLabelCellOpts(cell.FgColor(cell.ColorCyan)),
|
||||
)
|
||||
go playLineChart(ctx, lc, redrawInterval/3)
|
||||
c := container.New(
|
||||
t,
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
package linechart
|
||||
|
||||
import "github.com/mum4k/termdash/cell"
|
||||
|
||||
// options.go contains configurable options for LineChart.
|
||||
|
||||
// Option is used to provide options to New().
|
||||
|
@ -24,6 +26,9 @@ type Option interface {
|
|||
|
||||
// options stores the provided options.
|
||||
type options struct {
|
||||
axesCellOpts []cell.Option
|
||||
xLabelCellOpts []cell.Option
|
||||
yLabelCellOpts []cell.Option
|
||||
}
|
||||
|
||||
// newOptions returns a new options instance.
|
||||
|
@ -43,8 +48,23 @@ func (o option) set(opts *options) {
|
|||
o(opts)
|
||||
}
|
||||
|
||||
// Foo ...
|
||||
func Foo() Option {
|
||||
// AxesCellOpts set the cell options for the X and Y axes.
|
||||
func AxesCellOpts(co ...cell.Option) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.axesCellOpts = co
|
||||
})
|
||||
}
|
||||
|
||||
// XLabelCellOpts set the cell options for the labels on the X axis.
|
||||
func XLabelCellOpts(co ...cell.Option) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.xLabelCellOpts = co
|
||||
})
|
||||
}
|
||||
|
||||
// YLabelCellOpts set the cell options for the labels on the Y axis.
|
||||
func YLabelCellOpts(co ...cell.Option) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.yLabelCellOpts = co
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue