From bc911a3cd636d133e7b9833f550f2f596f3de4f0 Mon Sep 17 00:00:00 2001 From: Jakub Sobon Date: Sun, 13 Jan 2019 01:38:39 -0500 Subject: [PATCH] More test coverage for linechart. --- widgets/linechart/axes/axes.go | 6 +- widgets/linechart/axes/axes_test.go | 17 +- widgets/linechart/axes/label.go | 26 ++- widgets/linechart/axes/label_test.go | 59 +++++- widgets/linechart/axes/value.go | 15 ++ widgets/linechart/axes/value_test.go | 9 + widgets/linechart/linechart.go | 93 ++++++++- widgets/linechart/linechart_test.go | 190 +++++++++++++++++- .../linechart/linechartdemo/linechartdemo.go | 20 +- widgets/linechart/options.go | 24 ++- 10 files changed, 411 insertions(+), 48 deletions(-) diff --git a/widgets/linechart/axes/axes.go b/widgets/linechart/axes/axes.go index 4ccf2b0..38b1b39 100644 --- a/widgets/linechart/axes/axes.go +++ b/widgets/linechart/axes/axes.go @@ -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 } diff --git a/widgets/linechart/axes/axes_test.go b/widgets/linechart/axes/axes_test.go index 0b75c4e..fe75854 100644 --- a/widgets/linechart/axes/axes_test.go +++ b/widgets/linechart/axes/axes_test.go @@ -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) } diff --git a/widgets/linechart/axes/label.go b/widgets/linechart/axes/label.go index 2692baa..6e7ba50 100644 --- a/widgets/linechart/axes/label.go +++ b/widgets/linechart/axes/label.go @@ -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 } diff --git a/widgets/linechart/axes/label_test.go b/widgets/linechart/axes/label_test.go index d5aa62f..175b9b8 100644 --- a/widgets/linechart/axes/label_test.go +++ b/widgets/linechart/axes/label_test.go @@ -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) } diff --git a/widgets/linechart/axes/value.go b/widgets/linechart/axes/value.go index fde70a2..a0f73a8 100644 --- a/widgets/linechart/axes/value.go +++ b/widgets/linechart/axes/value.go @@ -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) } diff --git a/widgets/linechart/axes/value_test.go b/widgets/linechart/axes/value_test.go index 6aecf9f..25318c3 100644 --- a/widgets/linechart/axes/value_test.go +++ b/widgets/linechart/axes/value_test.go @@ -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) + } +} diff --git a/widgets/linechart/linechart.go b/widgets/linechart/linechart.go index c7c670a..316acc4 100644 --- a/widgets/linechart/linechart.go +++ b/widgets/linechart/linechart.go @@ -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 diff --git a/widgets/linechart/linechart_test.go b/widgets/linechart/linechart_test.go index 5d1f155..f2e9d46 100644 --- a/widgets/linechart/linechart_test.go +++ b/widgets/linechart/linechart_test.go @@ -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 { diff --git a/widgets/linechart/linechartdemo/linechartdemo.go b/widgets/linechart/linechartdemo/linechartdemo.go index 56ec1ac..e1bbc4c 100644 --- a/widgets/linechart/linechartdemo/linechartdemo.go +++ b/widgets/linechart/linechartdemo/linechartdemo.go @@ -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, diff --git a/widgets/linechart/options.go b/widgets/linechart/options.go index 12eb61b..19d7870 100644 --- a/widgets/linechart/options.go +++ b/widgets/linechart/options.go @@ -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 }) }