mirror of
https://github.com/mum4k/termdash.git
synced 2025-05-01 22:17:51 +08:00
Test coverage for the initial functionality of linechart.
This commit is contained in:
parent
c2fd0aa46f
commit
1db0cfc7f1
@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/braille"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/terminal/faketerm"
|
||||
@ -46,3 +47,10 @@ func MustSetPixel(bc *braille.Canvas, p image.Point, opts ...cell.Option) {
|
||||
panic(fmt.Sprintf("braille.SetPixel => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// MustCopyTo copies the braille canvas onto the provided canvas or panics.
|
||||
func MustCopyTo(bc *braille.Canvas, dst *canvas.Canvas) {
|
||||
if err := bc.CopyTo(dst); err != nil {
|
||||
panic(fmt.Sprintf("bc.CopyTo => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"image"
|
||||
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/braille"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
)
|
||||
|
||||
@ -50,3 +51,10 @@ func MustHVLines(c *canvas.Canvas, lines []draw.HVLine, opts ...draw.HVLineOptio
|
||||
panic(fmt.Sprintf("draw.HVLines => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// MustBrailleLine draws the braille line or panics.
|
||||
func MustBrailleLine(bc *braille.Canvas, start, end image.Point, opts ...draw.BrailleLineOption) {
|
||||
if err := draw.BrailleLine(bc, start, end, opts...); err != nil {
|
||||
panic(fmt.Sprintf("draw.BrailleLine => unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,12 @@ type YDetails struct {
|
||||
// Width in character cells of the Y axis and its character labels.
|
||||
Width int
|
||||
|
||||
// Start is the point where the Y axis starts.
|
||||
// Both coordinates of Start are less than End.
|
||||
Start image.Point
|
||||
// End is the point where the Y axis ends.
|
||||
End image.Point
|
||||
|
||||
// Scale is the scale of the Y axis.
|
||||
Scale *YScale
|
||||
|
||||
@ -81,15 +87,17 @@ func (y *Y) RequiredWidth() int {
|
||||
}
|
||||
|
||||
// Details retrieves details about the Y axis required to draw it on a canvas
|
||||
// of the provided height. The cvsHeight should be the height of the area with
|
||||
// the line chart. The maxWidth indicates the maximum width available
|
||||
// for the Y axis and its labels. This is guaranteed to be at least what
|
||||
// RequiredWidth returned.
|
||||
func (y *Y) Details(cvsHeight int, maxWidth int) (*YDetails, error) {
|
||||
// of the provided area.
|
||||
func (y *Y) Details(cvsAr image.Rectangle) (*YDetails, error) {
|
||||
cvsWidth := cvsAr.Dx()
|
||||
cvsHeight := cvsAr.Dy()
|
||||
maxWidth := cvsWidth - 1 // Reserve one row for the line chart itself.
|
||||
if req := y.RequiredWidth(); maxWidth < req {
|
||||
return nil, fmt.Errorf("the received maxWidth %d is smaller than the reported required width %d", maxWidth, req)
|
||||
}
|
||||
scale, err := NewYScale(y.min.Value, y.max.Value, cvsHeight, nonZeroDecimals)
|
||||
|
||||
graphHeight := cvsHeight - 2 // One row for the X axis and one for its labels.
|
||||
scale, err := NewYScale(y.min.Value, y.max.Value, graphHeight, nonZeroDecimals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -119,6 +127,8 @@ func (y *Y) Details(cvsHeight int, maxWidth int) (*YDetails, error) {
|
||||
|
||||
return &YDetails{
|
||||
Width: width,
|
||||
Start: image.Point{width - 1, 0},
|
||||
End: image.Point{width - 1, graphHeight},
|
||||
Scale: scale,
|
||||
Labels: labels,
|
||||
}, nil
|
||||
@ -138,6 +148,12 @@ func widestLabel(labels []*Label) int {
|
||||
// XDetails contain information about the X axis that will be drawn onto the
|
||||
// canvas.
|
||||
type XDetails struct {
|
||||
// Start is the point where the X axis starts.
|
||||
// Both coordinates of Start are less than End.
|
||||
Start image.Point
|
||||
// End is the point where the X axis ends.
|
||||
End image.Point
|
||||
|
||||
// Scale is the scale of the X axis.
|
||||
Scale *XScale
|
||||
|
||||
@ -146,20 +162,31 @@ type XDetails struct {
|
||||
}
|
||||
|
||||
// NewXDetails retrieves details about the X axis required to draw it on a canvas
|
||||
// of the provided height. The axisStart is the zero point of the X axis on the
|
||||
// canvas and the axisWidth is its width in cells. The numPoints is the number
|
||||
// of points in the largest series that will be plotted.
|
||||
func NewXDetails(numPoints int, axisStart image.Point, axisWidth int) (*XDetails, error) {
|
||||
scale, err := NewXScale(numPoints, axisWidth, nonZeroDecimals)
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
|
||||
// The space between the start of the axis and the end of the canvas.
|
||||
graphWidth := cvsAr.Dx() - yStart.X - 1
|
||||
scale, err := NewXScale(numPoints, graphWidth, nonZeroDecimals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labels, err := xLabels(scale, axisStart)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &XDetails{
|
||||
Start: image.Point{yStart.X, cvsAr.Dy() - 2}, // One row for the labels.
|
||||
End: image.Point{yStart.X + graphWidth, cvsAr.Dy() - 2},
|
||||
Scale: scale,
|
||||
Labels: labels,
|
||||
}, nil
|
||||
|
@ -32,8 +32,7 @@ func TestY(t *testing.T) {
|
||||
minVal float64
|
||||
maxVal float64
|
||||
update *updateY
|
||||
cvsHeight int
|
||||
maxWidth int
|
||||
cvsAr image.Rectangle
|
||||
wantWidth int
|
||||
want *YDetails
|
||||
wantErr bool
|
||||
@ -42,17 +41,15 @@ func TestY(t *testing.T) {
|
||||
desc: "fails on canvas too small",
|
||||
minVal: 0,
|
||||
maxVal: 3,
|
||||
cvsHeight: 1,
|
||||
maxWidth: 2,
|
||||
cvsAr: image.Rect(0, 0, 3, 2),
|
||||
wantWidth: 2,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on maxWidth less than required width",
|
||||
desc: "fails on cvsWidth less than required width",
|
||||
minVal: 0,
|
||||
maxVal: 3,
|
||||
cvsHeight: 2,
|
||||
maxWidth: 1,
|
||||
cvsAr: image.Rect(0, 0, 2, 4),
|
||||
wantWidth: 2,
|
||||
wantErr: true,
|
||||
},
|
||||
@ -60,20 +57,20 @@ func TestY(t *testing.T) {
|
||||
desc: "fails when max is less than min",
|
||||
minVal: 0,
|
||||
maxVal: -1,
|
||||
cvsHeight: 2,
|
||||
maxWidth: 3,
|
||||
cvsAr: image.Rect(0, 0, 4, 4),
|
||||
wantWidth: 3,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "maxWidth equals required width",
|
||||
desc: "cvsWidth equals required width",
|
||||
minVal: 0,
|
||||
maxVal: 3,
|
||||
cvsHeight: 2,
|
||||
cvsAr: image.Rect(0, 0, 3, 4),
|
||||
wantWidth: 2,
|
||||
maxWidth: 2,
|
||||
want: &YDetails{
|
||||
Width: 2,
|
||||
Start: image.Point{1, 0},
|
||||
End: image.Point{1, 2},
|
||||
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
|
||||
Labels: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 1}},
|
||||
@ -82,14 +79,15 @@ func TestY(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "maxWidth just accommodates the longest label",
|
||||
desc: "cvsWidth just accommodates the longest label",
|
||||
minVal: 0,
|
||||
maxVal: 3,
|
||||
cvsHeight: 2,
|
||||
cvsAr: image.Rect(0, 0, 6, 4),
|
||||
wantWidth: 2,
|
||||
maxWidth: 5,
|
||||
want: &YDetails{
|
||||
Width: 5,
|
||||
Start: image.Point{4, 0},
|
||||
End: image.Point{4, 2},
|
||||
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
|
||||
Labels: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{3, 1}},
|
||||
@ -98,14 +96,15 @@ func TestY(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "maxWidth is more than we need",
|
||||
desc: "cvsWidth is more than we need",
|
||||
minVal: 0,
|
||||
maxVal: 3,
|
||||
cvsHeight: 2,
|
||||
cvsAr: image.Rect(0, 0, 7, 4),
|
||||
wantWidth: 2,
|
||||
maxWidth: 6,
|
||||
want: &YDetails{
|
||||
Width: 5,
|
||||
Start: image.Point{4, 0},
|
||||
End: image.Point{4, 2},
|
||||
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
|
||||
Labels: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{3, 1}},
|
||||
@ -127,7 +126,7 @@ func TestY(t *testing.T) {
|
||||
t.Errorf("RequiredWidth => got %v, want %v", gotWidth, tc.wantWidth)
|
||||
}
|
||||
|
||||
got, err := y.Details(tc.cvsHeight, tc.maxWidth)
|
||||
got, err := y.Details(tc.cvsAr)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("Details => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
@ -145,51 +144,63 @@ func TestNewXDetails(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
numPoints int
|
||||
axisStart image.Point
|
||||
axisWidth int
|
||||
yStart image.Point
|
||||
cvsWidth int
|
||||
cvsAr image.Rectangle
|
||||
want *XDetails
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "fails when numPoints is negative",
|
||||
numPoints: -1,
|
||||
axisStart: image.Point{0, 1},
|
||||
axisWidth: 1,
|
||||
yStart: image.Point{0, 0},
|
||||
cvsAr: image.Rect(0, 0, 2, 3),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails when axisWidth is too small",
|
||||
desc: "fails when cvsAr isn't wide enough",
|
||||
numPoints: 1,
|
||||
axisStart: image.Point{0, 1},
|
||||
axisWidth: 0,
|
||||
yStart: image.Point{0, 0},
|
||||
cvsAr: image.Rect(0, 0, 1, 3),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails when cvsAr isn't tall enough",
|
||||
numPoints: 1,
|
||||
yStart: image.Point{0, 0},
|
||||
cvsAr: image.Rect(0, 0, 3, 2),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "works with no data points",
|
||||
numPoints: 0,
|
||||
axisStart: image.Point{0, 1},
|
||||
axisWidth: 1,
|
||||
yStart: image.Point{0, 0},
|
||||
cvsAr: image.Rect(0, 0, 2, 3),
|
||||
want: &XDetails{
|
||||
Start: image.Point{0, 1},
|
||||
End: image.Point{1, 1},
|
||||
Scale: mustNewXScale(0, 1, nonZeroDecimals),
|
||||
Labels: []*Label{
|
||||
{
|
||||
Value: NewValue(0, nonZeroDecimals),
|
||||
Pos: image.Point{0, 2},
|
||||
Pos: image.Point{1, 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "axis doesn't start at point zero",
|
||||
desc: "accounts for non-zero yStart",
|
||||
numPoints: 0,
|
||||
axisStart: image.Point{11, 2},
|
||||
axisWidth: 1,
|
||||
yStart: image.Point{2, 0},
|
||||
cvsAr: image.Rect(0, 0, 4, 5),
|
||||
want: &XDetails{
|
||||
Start: image.Point{2, 3},
|
||||
End: image.Point{3, 3},
|
||||
Scale: mustNewXScale(0, 1, nonZeroDecimals),
|
||||
Labels: []*Label{
|
||||
{
|
||||
Value: NewValue(0, nonZeroDecimals),
|
||||
Pos: image.Point{11, 3},
|
||||
Pos: image.Point{3, 4},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -198,7 +209,7 @@ func TestNewXDetails(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
got, err := NewXDetails(tc.numPoints, tc.axisStart, tc.axisWidth)
|
||||
got, err := NewXDetails(tc.numPoints, tc.yStart, tc.cvsAr)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("NewXDetails => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
|
@ -106,22 +106,22 @@ type xSpace struct {
|
||||
// The xSpace instance contains points 0 <= x < max
|
||||
max int
|
||||
|
||||
// axisStart is the actual position of the X axis zero point on the canvas.
|
||||
axisStart image.Point
|
||||
// graphZero is the (0, 0) point on the graph.
|
||||
graphZero image.Point
|
||||
}
|
||||
|
||||
// newXSpace returns a new xSpace instance initialized for the provided width.
|
||||
func newXSpace(axisStart image.Point, graphWidth int) *xSpace {
|
||||
func newXSpace(graphZero image.Point, graphWidth int) *xSpace {
|
||||
return &xSpace{
|
||||
cur: 0,
|
||||
max: graphWidth,
|
||||
axisStart: axisStart,
|
||||
graphZero: graphZero,
|
||||
}
|
||||
}
|
||||
|
||||
// Implements fmt.Stringer.
|
||||
func (xs *xSpace) String() string {
|
||||
return fmt.Sprintf("xSpace(size:%d)-cur:%v-max:%v", xs.Remaining(), image.Point{xs.cur, xs.axisStart.Y}, image.Point{xs.max, xs.axisStart.Y})
|
||||
return fmt.Sprintf("xSpace(size:%d)-cur:%v-max:%v", xs.Remaining(), image.Point{xs.cur, xs.graphZero.Y}, image.Point{xs.max, xs.graphZero.Y})
|
||||
}
|
||||
|
||||
// Remaining returns the remaining size on the X axis.
|
||||
@ -132,14 +132,14 @@ func (xs *xSpace) Remaining() int {
|
||||
// Relative returns the relative coordinate within the space, these are zero
|
||||
// based.
|
||||
func (xs *xSpace) Relative() image.Point {
|
||||
return image.Point{xs.cur, xs.axisStart.Y + 1}
|
||||
return image.Point{xs.cur, xs.graphZero.Y + 1}
|
||||
}
|
||||
|
||||
// Absolute returns the absolute coordinate on the canvas where a label should
|
||||
// LabelPos returns the absolute coordinate on the canvas where a label should
|
||||
// be placed. The is the coordinate that represents the current relative
|
||||
// coordinate of the space.
|
||||
func (xs *xSpace) Absolute() image.Point {
|
||||
return image.Point{xs.cur + xs.axisStart.X, xs.axisStart.Y + 1}
|
||||
func (xs *xSpace) LabelPos() image.Point {
|
||||
return image.Point{xs.cur + xs.graphZero.X, xs.graphZero.Y + 2} // First down is the axis, second the label.
|
||||
}
|
||||
|
||||
// Sub subtracts the specified size from the beginning of the available
|
||||
@ -153,13 +153,12 @@ func (xs *xSpace) Sub(size int) error {
|
||||
}
|
||||
|
||||
// xLabels returns labels that should be placed under the X axis.
|
||||
// The axisStart is the point where the X axis starts (its zero value) and is
|
||||
// used to determine label placement.
|
||||
// The graphZero is the (0, 0) point of the graph area on the canvas.
|
||||
// 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, axisStart image.Point) ([]*Label, error) {
|
||||
space := newXSpace(axisStart, scale.GraphWidth)
|
||||
func xLabels(scale *XScale, graphZero image.Point) ([]*Label, error) {
|
||||
space := newXSpace(graphZero, scale.GraphWidth)
|
||||
const minSpacing = 3
|
||||
var res []*Label
|
||||
|
||||
@ -213,7 +212,7 @@ func colLabel(scale *XScale, space *xSpace) (*Label, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
abs := space.Absolute()
|
||||
abs := space.LabelPos()
|
||||
if err := space.Sub(labelLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ func TestXLabels(t *testing.T) {
|
||||
desc string
|
||||
numPoints int
|
||||
graphWidth int
|
||||
axisStart image.Point
|
||||
graphZero image.Point
|
||||
want []*Label
|
||||
wantErr bool
|
||||
}{
|
||||
@ -162,98 +162,98 @@ func TestXLabels(t *testing.T) {
|
||||
desc: "only one point",
|
||||
numPoints: 1,
|
||||
graphWidth: 1,
|
||||
axisStart: image.Point{0, 1},
|
||||
graphZero: image.Point{0, 1},
|
||||
want: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 2}},
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "two points, only one label fits",
|
||||
numPoints: 2,
|
||||
graphWidth: 1,
|
||||
axisStart: image.Point{0, 1},
|
||||
graphZero: image.Point{0, 1},
|
||||
want: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 2}},
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "two points, two labels fit exactly",
|
||||
numPoints: 2,
|
||||
graphWidth: 5,
|
||||
axisStart: image.Point{0, 1},
|
||||
graphZero: image.Point{0, 1},
|
||||
want: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 2}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{4, 2}},
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 3}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{4, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "labels are placed according to axisStart",
|
||||
desc: "labels are placed according to graphZero",
|
||||
numPoints: 2,
|
||||
graphWidth: 5,
|
||||
axisStart: image.Point{3, 5},
|
||||
graphZero: image.Point{3, 5},
|
||||
want: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{3, 6}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{7, 6}},
|
||||
{NewValue(0, nonZeroDecimals), image.Point{3, 7}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{7, 7}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "skip to next value exhausts the space completely",
|
||||
numPoints: 11,
|
||||
graphWidth: 4,
|
||||
axisStart: image.Point{0, 1},
|
||||
graphZero: image.Point{0, 1},
|
||||
want: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 2}},
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "second label doesn't fit due to its length",
|
||||
numPoints: 100,
|
||||
graphWidth: 5,
|
||||
axisStart: image.Point{0, 1},
|
||||
graphZero: image.Point{0, 1},
|
||||
want: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 2}},
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "two points, two labels, more space than minSpacing so end label adjusted",
|
||||
numPoints: 2,
|
||||
graphWidth: 6,
|
||||
axisStart: image.Point{0, 1},
|
||||
graphZero: image.Point{0, 1},
|
||||
want: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 2}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{5, 2}},
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 3}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{5, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "at most as many labels as there are points",
|
||||
numPoints: 2,
|
||||
graphWidth: 100,
|
||||
axisStart: image.Point{0, 1},
|
||||
graphZero: image.Point{0, 1},
|
||||
want: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 2}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{98, 2}},
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 3}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{98, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "some labels in the middle",
|
||||
numPoints: 4,
|
||||
graphWidth: 100,
|
||||
axisStart: image.Point{0, 1},
|
||||
graphZero: image.Point{0, 1},
|
||||
want: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 2}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{31, 2}},
|
||||
{NewValue(2, nonZeroDecimals), image.Point{62, 2}},
|
||||
{NewValue(3, nonZeroDecimals), image.Point{94, 2}},
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 3}},
|
||||
{NewValue(1, nonZeroDecimals), image.Point{31, 3}},
|
||||
{NewValue(2, nonZeroDecimals), image.Point{62, 3}},
|
||||
{NewValue(3, nonZeroDecimals), image.Point{94, 3}},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "more points than pixels",
|
||||
numPoints: 100,
|
||||
graphWidth: 6,
|
||||
axisStart: image.Point{0, 1},
|
||||
graphZero: image.Point{0, 1},
|
||||
want: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 2}},
|
||||
{NewValue(72, nonZeroDecimals), image.Point{4, 2}},
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 3}},
|
||||
{NewValue(72, nonZeroDecimals), image.Point{4, 3}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -265,7 +265,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.axisStart)
|
||||
got, err := xLabels(scale, tc.graphZero)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("xLabels => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
@ -73,9 +74,6 @@ type LineChart struct {
|
||||
// yAxis is the Y axis of the line chart.
|
||||
yAxis *axes.Y
|
||||
|
||||
// reqWidth is the last reported required with on a call to Options.
|
||||
reqWidth int
|
||||
|
||||
// opts are the provided options.
|
||||
opts *options
|
||||
}
|
||||
@ -113,27 +111,27 @@ func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
|
||||
// The Y axis and its labels cannot take the entire canvas width, reserve one column for the line chart.
|
||||
maxWidth := cvs.Area().Dx() - 1
|
||||
yd, err := lc.yAxis.Details(cvs.Area().Dy()-2, maxWidth)
|
||||
yd, err := lc.yAxis.Details(cvs.Area())
|
||||
if err != nil {
|
||||
return fmt.Errorf("lc.yAxis.Details => %v", err)
|
||||
}
|
||||
|
||||
yStart := image.Point{yd.Width - 1, 0}
|
||||
xStart := image.Point{yStart.X, cvs.Area().Max.Y - 2} // One for X labels.
|
||||
yEnd := image.Point{yStart.X, xStart.Y}
|
||||
|
||||
xWidth := cvs.Area().Dx() - yStart.X - 1
|
||||
xEnd := image.Point{xStart.X + xWidth - 1, xStart.Y}
|
||||
xd, err := axes.NewXDetails(lc.maxPoints(), xStart, xWidth)
|
||||
xd, err := axes.NewXDetails(lc.maxPoints(), yd.Start, cvs.Area())
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewXDetails => %v", err)
|
||||
}
|
||||
|
||||
if err := lc.drawAxes(cvs, xd, yd); err != nil {
|
||||
return err
|
||||
}
|
||||
return lc.drawSeries(cvs, xd, yd)
|
||||
}
|
||||
|
||||
// drawAxes draws the X,Y axes and their labels.
|
||||
func (lc *LineChart) drawAxes(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) error {
|
||||
lines := []draw.HVLine{
|
||||
{Start: yStart, End: yEnd},
|
||||
{Start: xStart, End: xEnd},
|
||||
{Start: yd.Start, End: yd.End},
|
||||
{Start: xd.Start, End: xd.End},
|
||||
}
|
||||
if err := draw.HVLines(cvs, lines); err != nil {
|
||||
return fmt.Errorf("failed to draw the axes: %v", err)
|
||||
@ -141,7 +139,7 @@ func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
|
||||
|
||||
for _, l := range yd.Labels {
|
||||
if err := draw.Text(cvs, l.Value.Text(), l.Pos,
|
||||
draw.TextMaxX(yStart.X),
|
||||
draw.TextMaxX(yd.Start.X),
|
||||
draw.TextOverrunMode(draw.OverrunModeThreeDot),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to draw the Y labels: %v", err)
|
||||
@ -153,9 +151,15 @@ func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
|
||||
return fmt.Errorf("failed to draw the X labels: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ba := image.Rect(yStart.X+1, yStart.Y, cvs.Area().Max.X, xEnd.Y)
|
||||
bc, err := braille.New(ba)
|
||||
// drawSeries draws the graph representing the stored series.
|
||||
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)
|
||||
}
|
||||
@ -185,6 +189,9 @@ func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
|
||||
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 {
|
||||
return fmt.Errorf("draw.BrailleLine => %v", err)
|
||||
}
|
||||
@ -213,13 +220,13 @@ func (lc *LineChart) Options() widgetapi.Options {
|
||||
defer lc.mu.Unlock()
|
||||
|
||||
// At the very least we need:
|
||||
// - n columns for the Y axis and its values as reported by it.
|
||||
// - 2 rows for the X axis and its values.
|
||||
lc.reqWidth = lc.yAxis.RequiredWidth() + 1
|
||||
const reqHeight = 3
|
||||
// - n cells width for the Y axis and its labels as reported by it.
|
||||
// - at least 1 cell width for the graph.
|
||||
reqWidth := lc.yAxis.RequiredWidth() + 1
|
||||
// - 2 cells height the X axis and its values and 2 for min and max labels on Y.
|
||||
const reqHeight = 4
|
||||
return widgetapi.Options{
|
||||
// - 1 row and column for the line chart.
|
||||
MinimumSize: image.Point{lc.reqWidth, reqHeight},
|
||||
MinimumSize: image.Point{reqWidth, reqHeight},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,12 +20,15 @@ import (
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/mum4k/termdash/canvas"
|
||||
"github.com/mum4k/termdash/canvas/braille/testbraille"
|
||||
"github.com/mum4k/termdash/canvas/testcanvas"
|
||||
"github.com/mum4k/termdash/draw"
|
||||
"github.com/mum4k/termdash/draw/testdraw"
|
||||
"github.com/mum4k/termdash/terminal/faketerm"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
)
|
||||
|
||||
func TestLineChartDraws(t *testing.T) {
|
||||
t.Skip() // Unimplemented.
|
||||
tests := []struct {
|
||||
desc string
|
||||
canvas image.Rectangle
|
||||
@ -33,11 +36,194 @@ func TestLineChartDraws(t *testing.T) {
|
||||
writes func(*LineChart) error
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
wantWriteErr bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "empty without series",
|
||||
canvas: image.Rect(0, 0, 1, 1),
|
||||
desc: "write 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: "draw fails when canvas not wide enough",
|
||||
canvas: image.Rect(0, 0, 2, 4),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "draw fails when canvas not tall enough",
|
||||
canvas: image.Rect(0, 0, 3, 3),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty without series",
|
||||
canvas: image.Rect(0, 0, 3, 4),
|
||||
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})
|
||||
testdraw.MustText(c, "0", image.Point{2, 3})
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "two Y and X labels",
|
||||
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{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})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple Y and X labels",
|
||||
canvas: image.Rect(0, 0, 20, 11),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("first", []float64{0, 50, 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{5, 0}, End: image.Point{5, 9}},
|
||||
{Start: image.Point{5, 9}, End: image.Point{19, 9}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "0", image.Point{4, 8})
|
||||
testdraw.MustText(c, "45.76", image.Point{0, 4})
|
||||
testdraw.MustText(c, "91.52", image.Point{0, 0})
|
||||
testdraw.MustText(c, "0", image.Point{6, 10})
|
||||
testdraw.MustText(c, "1", image.Point{12, 10})
|
||||
testdraw.MustText(c, "2", image.Point{19, 10})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(6, 0, 20, 9)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 35}, image.Point{13, 18})
|
||||
testdraw.MustBrailleLine(bc, image.Point{13, 18}, image.Point{27, 0})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Y labels are trimmed",
|
||||
canvas: image.Rect(0, 0, 5, 4),
|
||||
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{3, 0}, End: image.Point{3, 2}},
|
||||
{Start: image.Point{3, 2}, End: image.Point{4, 2}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "0", image.Point{2, 1})
|
||||
testdraw.MustText(c, "57…", image.Point{0, 0})
|
||||
testdraw.MustText(c, "0", image.Point{4, 3})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(4, 0, 5, 2)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 7}, image.Point{1, 0})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draw multiple series",
|
||||
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{100, 0})
|
||||
},
|
||||
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{27, 0})
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 0}, image.Point{13, 31})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
|
||||
// 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.
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@ -58,8 +244,14 @@ func TestLineChartDraws(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := widget.Draw(c); err != nil {
|
||||
t.Fatalf("Draw => unexpected error: %v", err)
|
||||
{
|
||||
err := widget.Draw(c)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Fatalf("Draw => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
got, err := faketerm.New(c.Size())
|
||||
@ -92,7 +284,7 @@ func TestOptions(t *testing.T) {
|
||||
{
|
||||
desc: "reserves space for axis without series",
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{3, 3},
|
||||
MinimumSize: image.Point{3, 4},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -101,7 +293,7 @@ func TestOptions(t *testing.T) {
|
||||
return lc.Series("series", []float64{0, 100})
|
||||
},
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{5, 3},
|
||||
MinimumSize: image.Point{5, 4},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -110,7 +302,7 @@ func TestOptions(t *testing.T) {
|
||||
return lc.Series("series", []float64{-100, 100})
|
||||
},
|
||||
want: widgetapi.Options{
|
||||
MinimumSize: image.Point{6, 3},
|
||||
MinimumSize: image.Point{6, 4},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user