mirror of https://github.com/mum4k/termdash.git
parent
ab013dd021
commit
68fb7606d9
|
@ -31,6 +31,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
not just received them.
|
not just received them.
|
||||||
- Container focus tracker now correctly tracks focus changes in enlarged areas,
|
- Container focus tracker now correctly tracks focus changes in enlarged areas,
|
||||||
i.e. when the terminal size increased.
|
i.e. when the terminal size increased.
|
||||||
|
- The BarChart, LineChart and SegmentDisplay widgets now protect against
|
||||||
|
external mutation of the values passed into them by copying the data they
|
||||||
|
receive.
|
||||||
|
|
||||||
#### Breaking API changes
|
#### Breaking API changes
|
||||||
|
|
||||||
|
|
|
@ -264,14 +264,17 @@ func (bc *BarChart) Values(values []int, max int, opts ...Option) error {
|
||||||
bc.mu.Lock()
|
bc.mu.Lock()
|
||||||
defer bc.mu.Unlock()
|
defer bc.mu.Unlock()
|
||||||
|
|
||||||
if err := validateValues(values, max); err != nil {
|
// Copy to avoid external modifications. See #174.
|
||||||
|
v := make([]int, len(values))
|
||||||
|
copy(v, values)
|
||||||
|
if err := validateValues(v, max); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt.set(bc.opts)
|
opt.set(bc.opts)
|
||||||
}
|
}
|
||||||
bc.values = values
|
bc.values = v
|
||||||
bc.max = max
|
bc.max = max
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -579,6 +579,61 @@ func TestBarChart(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantCapacity: 3,
|
wantCapacity: 3,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "regression for #174, protects against external data mutation",
|
||||||
|
opts: []Option{
|
||||||
|
Char('o'),
|
||||||
|
Labels([]string{
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
update: func(bc *BarChart) error {
|
||||||
|
values := []int{1, 2, 5, 10}
|
||||||
|
if err := bc.Values(values, 10); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values[0] = 100
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 7, 11),
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
c := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
testdraw.MustRectangle(c, image.Rect(0, 9, 1, 10),
|
||||||
|
draw.RectChar('o'),
|
||||||
|
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
|
||||||
|
)
|
||||||
|
testdraw.MustRectangle(c, image.Rect(2, 8, 3, 10),
|
||||||
|
draw.RectChar('o'),
|
||||||
|
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
|
||||||
|
)
|
||||||
|
testdraw.MustRectangle(c, image.Rect(4, 5, 5, 10),
|
||||||
|
draw.RectChar('o'),
|
||||||
|
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
|
||||||
|
)
|
||||||
|
testdraw.MustRectangle(c, image.Rect(6, 0, 7, 10),
|
||||||
|
draw.RectChar('o'),
|
||||||
|
draw.RectCellOpts(cell.BgColor(DefaultBarColor)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Labels.
|
||||||
|
testdraw.MustText(c, "1", image.Point{0, 10}, draw.TextCellOpts(
|
||||||
|
cell.FgColor(DefaultLabelColor),
|
||||||
|
))
|
||||||
|
testdraw.MustText(c, "2", image.Point{2, 10}, draw.TextCellOpts(
|
||||||
|
cell.FgColor(DefaultLabelColor),
|
||||||
|
))
|
||||||
|
testdraw.MustText(c, "3", image.Point{4, 10}, draw.TextCellOpts(
|
||||||
|
cell.FgColor(DefaultLabelColor),
|
||||||
|
))
|
||||||
|
testcanvas.MustApply(c, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCapacity: 4,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
|
|
@ -143,7 +143,9 @@ func LabelColors(colors []cell.Color) Option {
|
||||||
// label.
|
// label.
|
||||||
func Labels(labels []string) Option {
|
func Labels(labels []string) Option {
|
||||||
return option(func(opts *options) {
|
return option(func(opts *options) {
|
||||||
opts.labels = labels
|
// Copy to avoid external modifications. See #174.
|
||||||
|
opts.labels = make([]string, len(labels))
|
||||||
|
copy(opts.labels, labels)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,11 @@ type YScale struct {
|
||||||
brailleHeight int
|
brailleHeight int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String implements fmt.Stringer.
|
||||||
|
func (ys *YScale) String() string {
|
||||||
|
return fmt.Sprintf("YScale{Min:%v, Max:%v, Step:%v, GraphHeight:%v}", ys.Min, ys.Max, ys.Step, ys.GraphHeight)
|
||||||
|
}
|
||||||
|
|
||||||
// NewYScale calculates the scale of the Y axis, given the boundary values and
|
// NewYScale calculates the scale of the Y axis, given the boundary values and
|
||||||
// the height of the graph. The nonZeroDecimals dictates rounding of the
|
// the height of the graph. The nonZeroDecimals dictates rounding of the
|
||||||
// calculated scale, see NewValue for details.
|
// calculated scale, see NewValue for details.
|
||||||
|
|
|
@ -52,9 +52,13 @@ type seriesValues struct {
|
||||||
|
|
||||||
// newSeriesValues returns a new seriesValues instance.
|
// newSeriesValues returns a new seriesValues instance.
|
||||||
func newSeriesValues(values []float64) *seriesValues {
|
func newSeriesValues(values []float64) *seriesValues {
|
||||||
min, max := numbers.MinMax(values)
|
// Copy to avoid external modifications. See #174.
|
||||||
|
v := make([]float64, len(values))
|
||||||
|
copy(v, values)
|
||||||
|
|
||||||
|
min, max := numbers.MinMax(v)
|
||||||
return &seriesValues{
|
return &seriesValues{
|
||||||
values: values,
|
values: v,
|
||||||
min: min,
|
min: min,
|
||||||
max: max,
|
max: max,
|
||||||
}
|
}
|
||||||
|
@ -146,7 +150,12 @@ func SeriesCellOpts(co ...cell.Option) SeriesOption {
|
||||||
func SeriesXLabels(labels map[int]string) SeriesOption {
|
func SeriesXLabels(labels map[int]string) SeriesOption {
|
||||||
return seriesOption(func(opts *seriesValues) {
|
return seriesOption(func(opts *seriesValues) {
|
||||||
opts.xLabelsSet = true
|
opts.xLabelsSet = true
|
||||||
opts.xLabels = labels
|
|
||||||
|
// Copy to avoid external modifications. See #174.
|
||||||
|
opts.xLabels = make(map[int]string, len(labels))
|
||||||
|
for pos, label := range labels {
|
||||||
|
opts.xLabels[pos] = label
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,21 +418,21 @@ func (lc *LineChart) drawSeries(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.
|
||||||
|
|
||||||
startX, err := xdZoomed.Scale.ValueToPixel(i - 1)
|
startX, err := xdZoomed.Scale.ValueToPixel(i - 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failure for series %v[%d], xdZoomed.Scale.ValueToPixel => %v", name, i-1, err)
|
return nil, fmt.Errorf("failure for series %v[%d] on scale %v, xdZoomed.Scale.ValueToPixel(%v) => %v", name, i-1, xdZoomed.Scale, i-1, err)
|
||||||
}
|
}
|
||||||
endX, err := xdZoomed.Scale.ValueToPixel(i)
|
endX, err := xdZoomed.Scale.ValueToPixel(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failure for series %v[%d], xdZoomed.Scale.ValueToPixel => %v", name, i, err)
|
return nil, fmt.Errorf("failure for series %v[%d] on scale %v, xdZoomed.Scale.ValueToPixel(%v) => %v", name, i, xdZoomed.Scale, i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
startY, err := yd.Scale.ValueToPixel(prev)
|
startY, err := yd.Scale.ValueToPixel(prev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failure for series %v[%d], yd.Scale.ValueToPixel => %v", name, i-1, err)
|
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]
|
v := sv.values[i]
|
||||||
endY, err := yd.Scale.ValueToPixel(v)
|
endY, err := yd.Scale.ValueToPixel(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failure for series %v[%d], yd.Scale.ValueToPixel => %v", name, i, err)
|
return nil, fmt.Errorf("failure for series %v[%d] on scale %v, yd.Scale.ValueToPixel(%v) => %v", name, i, yd.Scale, v, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := draw.BrailleLine(bc,
|
if err := draw.BrailleLine(bc,
|
||||||
|
|
|
@ -1479,6 +1479,53 @@ func TestLineChartDraws(t *testing.T) {
|
||||||
return ft
|
return ft
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "regression for #174, protects against external data mutation",
|
||||||
|
canvas: image.Rect(0, 0, 20, 10),
|
||||||
|
writes: func(lc *LineChart) error {
|
||||||
|
values := []float64{0, 100}
|
||||||
|
labels := map[int]string{
|
||||||
|
0: "start",
|
||||||
|
1: "end",
|
||||||
|
}
|
||||||
|
if err := lc.Series("first", values, SeriesXLabels(labels)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the values after they were passed in.
|
||||||
|
// Increase above the previous maximum to run out of the Y axis.
|
||||||
|
// The linechart should not use the original values.
|
||||||
|
values[1] = 1000
|
||||||
|
labels[0] = "bad"
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
wantCapacity: 28,
|
||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
|
|
@ -731,6 +731,40 @@ func TestSegmentDisplay(t *testing.T) {
|
||||||
mustDrawChar(cvs, tc.char, tc.area)
|
mustDrawChar(cvs, tc.char, tc.area)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testcanvas.MustApply(cvs, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "regression for #174, protects against external data mutation",
|
||||||
|
opts: []Option{
|
||||||
|
GapPercent(0),
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, sixteen.MinCols*3, sixteen.MinRows),
|
||||||
|
update: func(sd *SegmentDisplay) error {
|
||||||
|
chunks := []*TextChunk{NewChunk("123")}
|
||||||
|
if err := sd.Write(chunks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Mutates and adds unsupported characters.
|
||||||
|
chunks[0] = NewChunk("\r\t")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
cvs := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
char rune
|
||||||
|
area image.Rectangle
|
||||||
|
}{
|
||||||
|
{'1', image.Rect(0, 0, sixteen.MinCols, sixteen.MinRows)},
|
||||||
|
{'2', image.Rect(sixteen.MinCols, 0, sixteen.MinCols*2, sixteen.MinRows)},
|
||||||
|
{'3', image.Rect(sixteen.MinCols*2, 0, sixteen.MinCols*3, sixteen.MinRows)},
|
||||||
|
} {
|
||||||
|
mustDrawChar(cvs, tc.char, tc.area)
|
||||||
|
}
|
||||||
|
|
||||||
testcanvas.MustApply(cvs, ft)
|
testcanvas.MustApply(cvs, ft)
|
||||||
return ft
|
return ft
|
||||||
},
|
},
|
||||||
|
|
|
@ -426,6 +426,30 @@ func TestSparkLine(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantCapacity: 2,
|
wantCapacity: 2,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "regression for #174, protects against external data mutation",
|
||||||
|
update: func(sl *SparkLine) error {
|
||||||
|
values := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
|
||||||
|
if err := sl.Add(values); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Mutation should have no effect.
|
||||||
|
values[0] = 8
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
canvas: image.Rect(0, 0, 9, 1),
|
||||||
|
want: func(size image.Point) *faketerm.Terminal {
|
||||||
|
ft := faketerm.MustNew(size)
|
||||||
|
c := testcanvas.MustNew(ft.Area())
|
||||||
|
|
||||||
|
testdraw.MustText(c, "▁▂▃▄▅▆▇█", image.Point{1, 0}, draw.TextCellOpts(
|
||||||
|
cell.FgColor(DefaultColor),
|
||||||
|
))
|
||||||
|
testcanvas.MustApply(c, ft)
|
||||||
|
return ft
|
||||||
|
},
|
||||||
|
wantCapacity: 9,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
|
Loading…
Reference in New Issue