1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-25 13:48:50 +08:00

Merge branch 'devel' into text-input

This commit is contained in:
Jakub Sobon 2019-04-18 23:57:35 -04:00
commit afe70553e5
8 changed files with 303 additions and 26 deletions

View File

@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
executed.
- The SegmentDisplay widget now has a method that returns the observed character
capacity the last time Draw was called.
- The grid.Builder API now allows users to specify options for intermediate
containers, i.e. containers that don't have widgets, but represent rows and
columns.
- Line chart widget now allows `math.NaN` values to represent "no value" (values
that will not be rendered) in the values slice.
#### Breaking API changes
@ -82,10 +87,10 @@ identifiers shouldn't be used externally.
- The draw.LineStyle enum was refactored into its own package
linestyle.LineStyle. Users will have to replace:
- draw.LineStyleNone -> linestyle.None
- draw.LineStyleLight -> linestyle.Light
- draw.LineStyleDouble -> linestyle.Double
- draw.LineStyleRound -> linestyle.Round
- draw.LineStyleNone -> linestyle.None
- draw.LineStyleLight -> linestyle.Light
- draw.LineStyleDouble -> linestyle.Double
- draw.LineStyleRound -> linestyle.Round
## [0.7.0] - 24-Feb-2019
@ -119,7 +124,6 @@ identifiers shouldn't be used externally.
- The Text widget now has a Write option that atomically replaces the entire
text content.
#### Improvements to the infrastructure
- A function that draws text vertically.
@ -248,7 +252,7 @@ identifiers shouldn't be used externally.
- The Gauge widget.
- The Text widget.
[Unreleased]: https://github.com/mum4k/termdash/compare/v0.8.0...devel
[unreleased]: https://github.com/mum4k/termdash/compare/v0.8.0...devel
[0.8.0]: https://github.com/mum4k/termdash/compare/v0.7.2...v0.8.0
[0.7.2]: https://github.com/mum4k/termdash/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/mum4k/termdash/compare/v0.7.0...v0.7.1

View File

@ -109,18 +109,19 @@ func build(elems []Element, parentHeightPerc, parentWidthPerc int) []container.O
switch e := elem.(type) {
case *row:
if len(elems) > 0 {
perc := innerPerc(e.heightPerc, parentHeightPerc)
childHeightPerc := parentHeightPerc - e.heightPerc
return []container.Option{
container.SplitHorizontal(
container.Top(build(e.subElem, 100, parentWidthPerc)...),
container.Top(append(e.cOpts, build(e.subElem, 100, parentWidthPerc)...)...),
container.Bottom(build(elems, childHeightPerc, parentWidthPerc)...),
container.SplitPercent(perc),
),
}
}
return build(e.subElem, 100, parentWidthPerc)
return append(e.cOpts, build(e.subElem, 100, parentWidthPerc)...)
case *col:
if len(elems) > 0 {
@ -128,13 +129,13 @@ func build(elems []Element, parentHeightPerc, parentWidthPerc int) []container.O
childWidthPerc := parentWidthPerc - e.widthPerc
return []container.Option{
container.SplitVertical(
container.Left(build(e.subElem, parentHeightPerc, 100)...),
container.Left(append(e.cOpts, build(e.subElem, parentHeightPerc, 100)...)...),
container.Right(build(elems, parentHeightPerc, childWidthPerc)...),
container.SplitPercent(perc),
),
}
}
return build(e.subElem, parentHeightPerc, 100)
return append(e.cOpts, build(e.subElem, parentHeightPerc, 100)...)
case *widget:
opts := e.cOpts
@ -186,6 +187,9 @@ type row struct {
// subElem are the sub Rows or Columns or a single widget.
subElem []Element
// cOpts are the options for the row's container.
cOpts []container.Option
}
// isElement implements Element.isElement.
@ -204,6 +208,9 @@ type col struct {
// subElem are the sub Rows or Columns or a single widget.
subElem []Element
// cOpts are the options for the column's container.
cOpts []container.Option
}
// isElement implements Element.isElement.
@ -244,6 +251,16 @@ func RowHeightPerc(heightPerc int, subElements ...Element) Element {
}
}
// RowHeightPercWithOpts is like RowHeightPerc, but also allows to apply
// additional options to the container that represents the row.
func RowHeightPercWithOpts(heightPerc int, cOpts []container.Option, subElements ...Element) Element {
return &row{
heightPerc: heightPerc,
subElem: subElements,
cOpts: cOpts,
}
}
// ColWidthPerc creates a column of the specified width.
// The width is supplied as width percentage of the parent element.
// The sum of all widths at the same level cannot be larger than 100%. If it
@ -257,6 +274,16 @@ func ColWidthPerc(widthPerc int, subElements ...Element) Element {
}
}
// ColWidthPercWithOpts is like ColWidthPerc, but also allows to apply
// additional options to the container that represents the column.
func ColWidthPercWithOpts(widthPerc int, cOpts []container.Option, subElements ...Element) Element {
return &col{
widthPerc: widthPerc,
subElem: subElements,
cOpts: cOpts,
}
}
// Widget adds a widget into the Row or Column.
// The options will be applied to the container that directly holds this
// widget.

View File

@ -389,6 +389,45 @@ func TestBuilder(t *testing.T) {
return ft
},
},
{
desc: "two equal rows with options",
termSize: image.Point{10, 10},
builder: func() *Builder {
b := New()
b.Add(RowHeightPercWithOpts(
50,
[]container.Option{
container.Border(linestyle.Double),
},
Widget(mirror()),
))
b.Add(RowHeightPercWithOpts(
50,
[]container.Option{
container.Border(linestyle.Double),
},
Widget(mirror()),
))
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
top, bot := mustHSplit(ft.Area(), 50)
topCvs := testcanvas.MustNew(top)
botCvs := testcanvas.MustNew(bot)
testdraw.MustBorder(topCvs, topCvs.Area(), draw.BorderLineStyle(linestyle.Double))
testdraw.MustBorder(botCvs, botCvs.Area(), draw.BorderLineStyle(linestyle.Double))
testcanvas.MustApply(topCvs, ft)
testcanvas.MustApply(botCvs, ft)
topWidget := testcanvas.MustNew(area.ExcludeBorder(top))
botWidget := testcanvas.MustNew(area.ExcludeBorder(bot))
fakewidget.MustDraw(ft, topWidget, &widgetapi.Meta{}, widgetapi.Options{})
fakewidget.MustDraw(ft, botWidget, &widgetapi.Meta{}, widgetapi.Options{})
return ft
},
},
{
desc: "two unequal rows",
termSize: image.Point{10, 10},
@ -406,6 +445,45 @@ func TestBuilder(t *testing.T) {
return ft
},
},
{
desc: "two equal columns with options",
termSize: image.Point{20, 10},
builder: func() *Builder {
b := New()
b.Add(ColWidthPercWithOpts(
50,
[]container.Option{
container.Border(linestyle.Double),
},
Widget(mirror()),
))
b.Add(ColWidthPercWithOpts(
50,
[]container.Option{
container.Border(linestyle.Double),
},
Widget(mirror()),
))
return b
}(),
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
left, right := mustVSplit(ft.Area(), 50)
leftCvs := testcanvas.MustNew(left)
rightCvs := testcanvas.MustNew(right)
testdraw.MustBorder(leftCvs, leftCvs.Area(), draw.BorderLineStyle(linestyle.Double))
testdraw.MustBorder(rightCvs, rightCvs.Area(), draw.BorderLineStyle(linestyle.Double))
testcanvas.MustApply(leftCvs, ft)
testcanvas.MustApply(rightCvs, ft)
leftWidget := testcanvas.MustNew(area.ExcludeBorder(left))
rightWidget := testcanvas.MustNew(area.ExcludeBorder(right))
fakewidget.MustDraw(ft, leftWidget, &widgetapi.Meta{}, widgetapi.Options{})
fakewidget.MustDraw(ft, rightWidget, &widgetapi.Meta{}, widgetapi.Options{})
return ft
},
},
{
desc: "two equal columns",
termSize: image.Point{20, 10},

View File

@ -109,14 +109,21 @@ func Round(x float64) float64 {
// MinMax returns the smallest and the largest value among the provided values.
// Returns (0, 0) if there are no values.
// Ignores NaN values. Allowing NaN values could lead to a corner case where all
// values can be NaN, in this case the function will return NaN as min and max.
func MinMax(values []float64) (min, max float64) {
if len(values) == 0 {
return 0, 0
}
min = math.MaxFloat64
max = -1 * math.MaxFloat64
allNaN := true
for _, v := range values {
if math.IsNaN(v) {
continue
}
allNaN = false
if v < min {
min = v
}
@ -124,6 +131,11 @@ func MinMax(values []float64) (min, max float64) {
max = v
}
}
if allNaN {
return math.NaN(), math.NaN()
}
return min, max
}

View File

@ -215,13 +215,28 @@ func TestMinMax(t *testing.T) {
wantMin: -11.3,
wantMax: 22.5,
},
{
desc: "min and max among negative, positive, zero and NaN values",
values: []float64{1.1, 0, 1.3, math.NaN(), -11.3, 22.5},
wantMin: -11.3,
wantMax: 22.5,
},
{
desc: "all NaN values",
values: []float64{math.NaN(), math.NaN(), math.NaN(), math.NaN()},
wantMin: math.NaN(),
wantMax: math.NaN(),
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
gotMin, gotMax := MinMax(tc.values)
if gotMin != tc.wantMin || gotMax != tc.wantMax {
t.Errorf("MinMax => (%v, %v), want (%v, %v)", gotMin, gotMax, tc.wantMin, tc.wantMax)
if diff := pretty.Compare(tc.wantMin, gotMin); diff != "" {
t.Errorf("MinMax => unexpected min, diff (-want, +got):\n %s", diff)
}
if diff := pretty.Compare(tc.wantMax, gotMax); diff != "" {
t.Errorf("MinMax => unexpected max, diff (-want, +got):\n %s", diff)
}
})
}

View File

@ -166,16 +166,18 @@ func gridLayout(w *widgets, lt layoutType) ([]container.Option, error) {
container.BorderTitle("A rolling text"),
),
),
grid.RowHeightPerc(50,
grid.Widget(w.spGreen,
container.Border(linestyle.Light),
container.BorderTitle("Green SparkLine"),
grid.ColWidthPerc(50,
grid.RowHeightPerc(50,
grid.Widget(w.spGreen,
container.Border(linestyle.Light),
container.BorderTitle("Green SparkLine"),
),
),
),
grid.RowHeightPerc(50,
grid.Widget(w.spRed,
container.Border(linestyle.Light),
container.BorderTitle("Red SparkLine"),
grid.RowHeightPerc(50,
grid.Widget(w.spRed,
container.Border(linestyle.Light),
container.BorderTitle("Red SparkLine"),
),
),
),
),

View File

@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"image"
"math"
"sort"
"sync"
@ -56,7 +57,7 @@ func newSeriesValues(values []float64) *seriesValues {
v := make([]float64, len(values))
copy(v, values)
min, max := numbers.MinMax(v)
min, max := minMax(v)
return &seriesValues{
values: v,
min: min,
@ -175,8 +176,9 @@ func (lc *LineChart) yMinMax() (float64, float64) {
maximums = append(maximums, lc.opts.yAxisCustomScale.max)
}
min, _ := numbers.MinMax(minimums)
_, max := numbers.MinMax(maximums)
min, _ := minMax(minimums)
_, max := minMax(maximums)
return min, max
}
@ -196,6 +198,8 @@ func (lc *LineChart) ValueCapacity() int {
// Series sets the values that should be displayed as the line chart with the
// provided label.
// The values that should not be displayed on the line chart should be represented
// as math.NaN values on the values slice.
// Subsequent calls with the same label replace any previously provided values.
func (lc *LineChart) Series(label string, values []float64, opts ...SeriesOption) error {
if label == "" {
@ -364,6 +368,7 @@ func (lc *LineChart) graphAr(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDe
// drawSeries draws the graph representing the stored series.
// Returns XDetails that might be adjusted to not start at zero value if some
// of the series didn't fit the graphs and XAxisUnscaled was provided.
// If the series has NaN values they will be ignored and not draw on the graph.
func (lc *LineChart) drawSeries(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) (*axes.XDetails, error) {
graphAr := lc.graphAr(cvs, xd, yd)
bc, err := braille.New(graphAr)
@ -406,7 +411,14 @@ func (lc *LineChart) drawSeries(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.
var prev float64
for i := 1; i < len(sv.values); i++ {
v := sv.values[i]
prev = sv.values[i-1]
// Skip the values that are missing.
if math.IsNaN(v) || math.IsNaN(prev) {
continue
}
if i < int(xdZoomed.Scale.Min.Value)+1 || i > int(xdZoomed.Scale.Max.Value) {
// Don't draw lines for values that aren't supposed to be visible.
// These are either values outside of the current zoom or
@ -429,7 +441,7 @@ func (lc *LineChart) drawSeries(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.
if err != nil {
return nil, fmt.Errorf("failure for series %v[%d] on scale %v, yd.Scale.ValueToPixel(%v) => %v", name, i-1, yd.Scale, prev, err)
}
v := sv.values[i]
endY, err := yd.Scale.ValueToPixel(v)
if err != nil {
return nil, fmt.Errorf("failure for series %v[%d] on scale %v, yd.Scale.ValueToPixel(%v) => %v", name, i, yd.Scale, v, err)
@ -519,3 +531,17 @@ func (lc *LineChart) maxXValue() int {
}
return maxLen - 1
}
// minMax is a wrapper around numbers.MinMax that controls
// the output if the values are NaN and sets defaults if it's
// the case.
func minMax(values []float64) (x, y float64) {
min, max := numbers.MinMax(values)
if math.IsNaN(min) {
min = 0
}
if math.IsNaN(max) {
max = 0
}
return min, max
}

View File

@ -1523,6 +1523,119 @@ func TestLineChartDraws(t *testing.T) {
testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{26, 0})
testbraille.MustCopyTo(bc, c)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "all NaN values",
canvas: image.Rect(0, 0, 20, 10),
writes: func(lc *LineChart) error {
return lc.Series("first", []float64{math.NaN(), math.NaN(), math.NaN(), math.NaN(), math.NaN()})
},
wantCapacity: 36,
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, 8}},
{Start: image.Point{1, 8}, End: image.Point{19, 8}},
}
testdraw.MustHVLines(c, lines)
// Value labels.
testdraw.MustText(c, "0", image.Point{0, 7})
testdraw.MustText(c, "0", image.Point{2, 9})
testdraw.MustText(c, "1", image.Point{6, 9})
testdraw.MustText(c, "2", image.Point{10, 9})
testdraw.MustText(c, "3", image.Point{14, 9})
testdraw.MustText(c, "4", image.Point{18, 9})
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "first and last NaN values",
canvas: image.Rect(0, 0, 28, 10),
writes: func(lc *LineChart) error {
return lc.Series("first", []float64{math.NaN(), math.NaN(), 100, 150, math.NaN()})
},
wantCapacity: 44,
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{27, 8}},
}
testdraw.MustHVLines(c, lines)
// Value labels.
testdraw.MustText(c, "0", image.Point{4, 7})
testdraw.MustText(c, "77.44", image.Point{0, 3})
testdraw.MustText(c, "0", image.Point{6, 9})
testdraw.MustText(c, "1", image.Point{11, 9})
testdraw.MustText(c, "2", image.Point{16, 9})
testdraw.MustText(c, "3", image.Point{22, 9})
testdraw.MustText(c, "4", image.Point{27, 9})
graphAr := image.Rect(6, 0, 25, 8)
bc := testbraille.MustNew(graphAr)
testdraw.MustBrailleLine(bc, image.Point{21, 10}, image.Point{32, 0})
testbraille.MustCopyTo(bc, c)
testcanvas.MustApply(c, ft)
return ft
},
},
{
desc: "more values than capacity, X rescales with NaN values ignored",
canvas: image.Rect(0, 0, 11, 10),
writes: func(lc *LineChart) error {
return lc.Series("first", []float64{0, 1, 2, 3, 4, 5, 6, 7, math.NaN(), math.NaN(), math.NaN(), math.NaN(), 12, 13, 14, 15, 16, 17, 18, 19})
},
wantCapacity: 12,
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{10, 8}},
}
testdraw.MustHVLines(c, lines)
// Value labels.
testdraw.MustText(c, "0", image.Point{3, 7})
testdraw.MustText(c, "9.92", image.Point{0, 3})
testdraw.MustText(c, "0", image.Point{5, 9})
testdraw.MustText(c, "14", image.Point{9, 9})
// Braille line.
graphAr := image.Rect(5, 0, 11, 8)
bc := testbraille.MustNew(graphAr)
testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{1, 29})
testdraw.MustBrailleLine(bc, image.Point{1, 29}, image.Point{1, 28})
testdraw.MustBrailleLine(bc, image.Point{1, 28}, image.Point{2, 26})
testdraw.MustBrailleLine(bc, image.Point{2, 26}, image.Point{2, 25})
testdraw.MustBrailleLine(bc, image.Point{2, 25}, image.Point{3, 23})
testdraw.MustBrailleLine(bc, image.Point{3, 23}, image.Point{3, 21})
testdraw.MustBrailleLine(bc, image.Point{3, 21}, image.Point{4, 20})
testdraw.MustBrailleLine(bc, image.Point{7, 12}, image.Point{8, 10})
testdraw.MustBrailleLine(bc, image.Point{8, 10}, image.Point{8, 8})
testdraw.MustBrailleLine(bc, image.Point{8, 8}, image.Point{9, 7})
testdraw.MustBrailleLine(bc, image.Point{9, 7}, image.Point{9, 5})
testdraw.MustBrailleLine(bc, image.Point{9, 5}, image.Point{10, 4})
testdraw.MustBrailleLine(bc, image.Point{10, 4}, image.Point{10, 2})
testdraw.MustBrailleLine(bc, image.Point{10, 2}, image.Point{11, 0})
testbraille.MustCopyTo(bc, c)
testcanvas.MustApply(c, ft)
return ft
},