mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
Allow users to provide custom Y axis scale for the LineChart.
This commit is contained in:
parent
6969da653e
commit
6379b1d28e
@ -11,12 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- A function that draws text in vertically.
|
||||
- The LineChart widget can display X axis labels in vertical orientation.
|
||||
- The LineChart widget allows the user to specify a custom scale for the Y
|
||||
axis.
|
||||
|
||||
### Changed
|
||||
|
||||
- Termbox is now initialized in 256 color mode by default.
|
||||
- Generalized mouse button FSM for use in widgets that need to track mouse
|
||||
button clicks.
|
||||
- The constructor of the LineChart widget now also returns an error so that it
|
||||
can validate its options. This is a breaking change on the LineChart API.
|
||||
|
||||
## [0.6.1] - 12-Feb-2019
|
||||
|
||||
|
@ -77,6 +77,10 @@ func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container,
|
||||
),
|
||||
}
|
||||
|
||||
heartLC, err := newHeartbeat(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gaugeAndHeartbeat := []container.Option{
|
||||
container.SplitHorizontal(
|
||||
container.Top(
|
||||
@ -88,7 +92,7 @@ func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container,
|
||||
container.Bottom(
|
||||
container.Border(draw.LineStyleLight),
|
||||
container.BorderTitle("A LineChart"),
|
||||
container.PlaceWidget(newHeartbeat(ctx)),
|
||||
container.PlaceWidget(heartLC),
|
||||
),
|
||||
container.SplitPercent(20),
|
||||
),
|
||||
@ -107,6 +111,10 @@ func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sineLC, err := newSines(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rightSide := []container.Option{
|
||||
container.SplitHorizontal(
|
||||
container.Top(
|
||||
@ -127,7 +135,7 @@ func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container,
|
||||
container.Border(draw.LineStyleLight),
|
||||
container.BorderTitle("Multiple series"),
|
||||
container.BorderTitleAlignRight(),
|
||||
container.PlaceWidget(newSines(ctx)),
|
||||
container.PlaceWidget(sineLC),
|
||||
),
|
||||
container.SplitPercent(30),
|
||||
),
|
||||
@ -317,18 +325,21 @@ func newDonut(ctx context.Context) (*donut.Donut, error) {
|
||||
}
|
||||
|
||||
// newHeartbeat returns a line chart that displays a heartbeat-like progression.
|
||||
func newHeartbeat(ctx context.Context) *linechart.LineChart {
|
||||
func newHeartbeat(ctx context.Context) (*linechart.LineChart, error) {
|
||||
var inputs []float64
|
||||
for i := 0; i < 100; i++ {
|
||||
v := math.Pow(math.Sin(float64(i)), 63) * math.Sin(float64(i)+1.5) * 8
|
||||
inputs = append(inputs, v)
|
||||
}
|
||||
|
||||
lc := linechart.New(
|
||||
lc, err := linechart.New(
|
||||
linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
|
||||
linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
||||
linechart.XLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
step := 0
|
||||
go periodic(ctx, redrawInterval/3, func() error {
|
||||
step = (step + 1) % len(inputs)
|
||||
@ -339,7 +350,7 @@ func newHeartbeat(ctx context.Context) *linechart.LineChart {
|
||||
}),
|
||||
)
|
||||
})
|
||||
return lc
|
||||
return lc, nil
|
||||
}
|
||||
|
||||
// newBarChart returns a BarcChart that displays random values on multiple bars.
|
||||
@ -380,18 +391,21 @@ func newBarChart(ctx context.Context) *barchart.BarChart {
|
||||
}
|
||||
|
||||
// newSines returns a line chart that displays multiple sine series.
|
||||
func newSines(ctx context.Context) *linechart.LineChart {
|
||||
func newSines(ctx context.Context) (*linechart.LineChart, error) {
|
||||
var inputs []float64
|
||||
for i := 0; i < 200; i++ {
|
||||
v := math.Sin(float64(i) / 100 * math.Pi)
|
||||
inputs = append(inputs, v)
|
||||
}
|
||||
|
||||
lc := linechart.New(
|
||||
lc, err := linechart.New(
|
||||
linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
|
||||
linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
||||
linechart.XLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
step1 := 0
|
||||
go periodic(ctx, redrawInterval/3, func() error {
|
||||
step1 = (step1 + 1) % len(inputs)
|
||||
@ -404,7 +418,7 @@ func newSines(ctx context.Context) *linechart.LineChart {
|
||||
step2 := (step1 + 100) % len(inputs)
|
||||
return lc.Series("second", rotateFloats(inputs, step2), linechart.SeriesCellOpts(cell.FgColor(cell.ColorWhite)))
|
||||
})
|
||||
return lc
|
||||
return lc, nil
|
||||
}
|
||||
|
||||
// rotateFloats returns a new slice with inputs rotated by step.
|
||||
|
@ -90,13 +90,16 @@ type LineChart struct {
|
||||
}
|
||||
|
||||
// New returns a new line chart widget.
|
||||
func New(opts ...Option) *LineChart {
|
||||
func New(opts ...Option) (*LineChart, error) {
|
||||
opt := newOptions(opts...)
|
||||
if err := opt.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &LineChart{
|
||||
series: map[string]*seriesValues{},
|
||||
yAxis: axes.NewY(0, 0),
|
||||
opts: opt,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SeriesOption is used to provide options to Series.
|
||||
@ -136,6 +139,27 @@ func SeriesXLabels(labels map[int]string) SeriesOption {
|
||||
})
|
||||
}
|
||||
|
||||
// yMinMax determines the min and max values for the Y axis.
|
||||
func (lc *LineChart) yMinMax() (float64, float64) {
|
||||
var (
|
||||
minimums []float64
|
||||
maximums []float64
|
||||
)
|
||||
for _, sv := range lc.series {
|
||||
minimums = append(minimums, sv.min)
|
||||
maximums = append(maximums, sv.max)
|
||||
}
|
||||
|
||||
if lc.opts.yAxisCustomScale != nil {
|
||||
minimums = append(minimums, lc.opts.yAxisCustomScale.min)
|
||||
maximums = append(maximums, lc.opts.yAxisCustomScale.max)
|
||||
}
|
||||
|
||||
min, _ := numbers.MinMax(minimums)
|
||||
_, max := numbers.MinMax(maximums)
|
||||
return min, max
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -164,7 +188,7 @@ func (lc *LineChart) Series(label string, values []float64, opts ...SeriesOption
|
||||
}
|
||||
|
||||
lc.series[label] = series
|
||||
lc.yAxis = axes.NewY(series.min, series.max)
|
||||
lc.yAxis = axes.NewY(lc.yMinMax())
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ package linechart
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
@ -36,9 +37,42 @@ func TestLineChartDraws(t *testing.T) {
|
||||
opts []Option
|
||||
writes func(*LineChart) error
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
wantWriteErr bool
|
||||
wantErr bool
|
||||
wantWriteErr bool
|
||||
wantDrawErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails with custom scale where min is NaN",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
opts: []Option{
|
||||
YAxisCustomScale(math.NaN(), 1),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails with custom scale where max is NaN",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
opts: []Option{
|
||||
YAxisCustomScale(0, math.NaN()),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails with custom scale where min > max",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
opts: []Option{
|
||||
YAxisCustomScale(1, 0),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails with custom scale where min == max",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
opts: []Option{
|
||||
YAxisCustomScale(1, 1),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "series fails without name for the series",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
@ -190,6 +224,187 @@ func TestLineChartDraws(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "custom Y scale, zero based positive, values fit",
|
||||
opts: []Option{
|
||||
YAxisCustomScale(0, 200),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("first", []float64{0, 100})
|
||||
},
|
||||
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{6, 0}, End: image.Point{6, 8}},
|
||||
{Start: image.Point{6, 8}, End: image.Point{19, 8}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "0", image.Point{5, 7})
|
||||
testdraw.MustText(c, "103.36", image.Point{0, 3})
|
||||
testdraw.MustText(c, "0", image.Point{7, 9})
|
||||
testdraw.MustText(c, "1", image.Point{19, 9})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(7, 0, 20, 8)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{25, 16})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "custom Y scale, zero based negative, values fit",
|
||||
opts: []Option{
|
||||
YAxisCustomScale(-200, 0),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("first", []float64{0, -200})
|
||||
},
|
||||
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{6, 0}, End: image.Point{6, 8}},
|
||||
{Start: image.Point{6, 8}, End: image.Point{19, 8}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "-200", image.Point{2, 7})
|
||||
testdraw.MustText(c, "-96.64", image.Point{0, 3})
|
||||
testdraw.MustText(c, "0", image.Point{7, 9})
|
||||
testdraw.MustText(c, "1", image.Point{19, 9})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(7, 0, 20, 8)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{25, 31})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "custom Y scale, negative and positive, values fit",
|
||||
opts: []Option{
|
||||
YAxisCustomScale(-200, 200),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("first", []float64{0, 100})
|
||||
},
|
||||
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{4, 0}, End: image.Point{4, 8}},
|
||||
{Start: image.Point{4, 8}, End: image.Point{19, 8}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "-200", image.Point{0, 7})
|
||||
testdraw.MustText(c, "6.57", image.Point{0, 3})
|
||||
testdraw.MustText(c, "0", image.Point{5, 9})
|
||||
testdraw.MustText(c, "1", image.Point{19, 9})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(5, 0, 20, 8)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 16}, image.Point{29, 8})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "custom Y scale, negative only, values fit",
|
||||
opts: []Option{
|
||||
YAxisCustomScale(-200, -100),
|
||||
YAxisAdaptive(),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("first", []float64{-200, -100})
|
||||
},
|
||||
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{7, 0}, End: image.Point{7, 8}},
|
||||
{Start: image.Point{7, 8}, End: image.Point{19, 8}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "-200", image.Point{3, 7})
|
||||
testdraw.MustText(c, "-148.32", image.Point{0, 3})
|
||||
testdraw.MustText(c, "0", image.Point{8, 9})
|
||||
testdraw.MustText(c, "1", image.Point{19, 9})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(8, 0, 20, 8)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{23, 0})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "custom Y scale, negative and positive, values don't fit so adjusted",
|
||||
opts: []Option{
|
||||
YAxisCustomScale(-200, 200),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("first", []float64{-400, 400})
|
||||
},
|
||||
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, "-400", image.Point{1, 7})
|
||||
testdraw.MustText(c, "12.96", 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})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws anchored Y axis",
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
@ -544,6 +759,84 @@ func TestLineChartDraws(t *testing.T) {
|
||||
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
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draw multiple series, the second has smaller scale than the first",
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
if err := lc.Series("first", []float64{0, 50, 100}); err != nil {
|
||||
return err
|
||||
}
|
||||
return lc.Series("second", []float64{10, 20})
|
||||
},
|
||||
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{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{13, 16})
|
||||
testdraw.MustBrailleLine(bc, image.Point{13, 16}, image.Point{27, 0})
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 28}, image.Point{13, 25})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draw multiple series, the second has larger scale than the first",
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
if err := lc.Series("first", []float64{0, 50, 100}); err != nil {
|
||||
return err
|
||||
}
|
||||
return lc.Series("second", []float64{-10, 200})
|
||||
},
|
||||
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, "-10", image.Point{2, 7})
|
||||
testdraw.MustText(c, "98.48", 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, 30}, image.Point{13, 22})
|
||||
testdraw.MustBrailleLine(bc, image.Point{13, 22}, image.Point{27, 15})
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{13, 0})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
@ -557,7 +850,14 @@ func TestLineChartDraws(t *testing.T) {
|
||||
t.Fatalf("canvas.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
widget := New(tc.opts...)
|
||||
widget, err := New(tc.opts...)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("New => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.writes != nil {
|
||||
err := tc.writes(widget)
|
||||
if (err != nil) != tc.wantWriteErr {
|
||||
@ -570,8 +870,8 @@ func TestLineChartDraws(t *testing.T) {
|
||||
|
||||
{
|
||||
err := widget.Draw(c)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Fatalf("Draw => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
if (err != nil) != tc.wantDrawErr {
|
||||
t.Fatalf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
@ -658,7 +958,10 @@ func TestOptions(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
lc := New(tc.opts...)
|
||||
lc, err := New(tc.opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if tc.addSeries != nil {
|
||||
if err := tc.addSeries(lc); err != nil {
|
||||
|
@ -82,11 +82,14 @@ func main() {
|
||||
|
||||
const redrawInterval = 250 * time.Millisecond
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
lc := linechart.New(
|
||||
lc, err := linechart.New(
|
||||
linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
|
||||
linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)),
|
||||
linechart.XLabelCellOpts(cell.FgColor(cell.ColorCyan)),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go playLineChart(ctx, lc, redrawInterval/3)
|
||||
c, err := container.New(
|
||||
t,
|
||||
|
@ -15,6 +15,9 @@
|
||||
package linechart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/widgets/linechart/axes"
|
||||
)
|
||||
@ -34,6 +37,22 @@ type options struct {
|
||||
xLabelOrientation axes.LabelOrientation
|
||||
yLabelCellOpts []cell.Option
|
||||
yAxisMode axes.YScaleMode
|
||||
yAxisCustomScale *customScale
|
||||
}
|
||||
|
||||
// validate validates the provided options.
|
||||
func (o *options) validate() error {
|
||||
if o.yAxisCustomScale == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if math.IsNaN(o.yAxisCustomScale.min) || math.IsNaN(o.yAxisCustomScale.max) {
|
||||
return fmt.Errorf("both the min(%v) and the max(%v) provided as custom Y scale must be valid numbers", o.yAxisCustomScale.min, o.yAxisCustomScale.max)
|
||||
}
|
||||
if o.yAxisCustomScale.min >= o.yAxisCustomScale.max {
|
||||
return fmt.Errorf("the min(%v) must be less than the max(%v) provided as custom Y scale", o.yAxisCustomScale.min, o.yAxisCustomScale.max)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newOptions returns a new options instance.
|
||||
@ -103,3 +122,28 @@ func YAxisAdaptive() Option {
|
||||
opts.yAxisMode = axes.YScaleModeAdaptive
|
||||
})
|
||||
}
|
||||
|
||||
// customScale is the custom scale provided via the YAxisCustomScale option.
|
||||
type customScale struct {
|
||||
min, max float64
|
||||
}
|
||||
|
||||
// YAxisCustomScale when provided, the scale of the Y axis will be based on the
|
||||
// specified minimum and maximum value instead of determining those from the
|
||||
// LineChart series. Useful to visually stabilize the Y axis for LineChart
|
||||
// applications that continuously feed values.
|
||||
// The default behavior is to continuously determine the minimum and maximum
|
||||
// value from the series before drawing the LineChart.
|
||||
// Even when this option is provided, the LineChart would still rescale the Y
|
||||
// axis if a value is encountered that is outside of the range specified here,
|
||||
// i.e. smaller than the minimum or larger than the maximum.
|
||||
// Both the minimum and the maximum must be valid numbers and the minimum must
|
||||
// be smaller than the maximum.
|
||||
func YAxisCustomScale(min, max float64) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.yAxisCustomScale = &customScale{
|
||||
min: min,
|
||||
max: max,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user