From 7ef79393df940db3c6a9063339c08481f02fc4ca Mon Sep 17 00:00:00 2001 From: Jakub Sobon Date: Wed, 13 Feb 2019 23:16:05 -0500 Subject: [PATCH] xLabels now supports vertical labels. --- widgets/linechart/axes/axes.go | 30 +-------- widgets/linechart/axes/axes_test.go | 19 +++--- widgets/linechart/axes/label.go | 40 ++++++++++-- widgets/linechart/axes/label_test.go | 91 +++++++++++++++++++++++++--- widgets/linechart/linechart.go | 2 +- 5 files changed, 132 insertions(+), 50 deletions(-) diff --git a/widgets/linechart/axes/axes.go b/widgets/linechart/axes/axes.go index bb947a4..8a78f58 100644 --- a/widgets/linechart/axes/axes.go +++ b/widgets/linechart/axes/axes.go @@ -168,7 +168,7 @@ type XDetails struct { // plotted. // 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) { +func NewXDetails(numPoints int, yStart image.Point, cvsAr image.Rectangle, customLabels map[int]string, lo LabelOrientation) (*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) } @@ -183,7 +183,7 @@ func NewXDetails(numPoints int, yStart image.Point, cvsAr image.Rectangle, custo // 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, customLabels) + labels, err := xLabels(scale, graphZero, customLabels, lo) if err != nil { return nil, err } @@ -195,32 +195,6 @@ func NewXDetails(numPoints int, yStart image.Point, cvsAr image.Rectangle, custo }, nil } -// LabelOrientation represents the orientation of text labels. -type LabelOrientation int - -// String implements fmt.Stringer() -func (lo LabelOrientation) String() string { - if n, ok := labelOrientationNames[lo]; ok { - return n - } - return "LabelOrientationUnknown" -} - -// labelOrientationNames maps LabelOrientation values to human readable names. -var labelOrientationNames = map[LabelOrientation]string{ - LabelOrientationHorizontal: "LabelOrientationHorizontal", - LabelOrientationVertical: "LabelOrientationVertical", -} - -const ( - // LabelOrientationHorizontal is the default label orientation where text - // flows horizontally. - LabelOrientationHorizontal LabelOrientation = iota - - // LabelOrientationvertical is an orientation where text flows vertically. - LabelOrientationVertical -) - // RequiredHeight calculates the minimum height required in order to draw the X // axis and its labels. func RequiredHeight(numPoints int, customLabels map[int]string, lo LabelOrientation) int { diff --git a/widgets/linechart/axes/axes_test.go b/widgets/linechart/axes/axes_test.go index 41164be..27b2af7 100644 --- a/widgets/linechart/axes/axes_test.go +++ b/widgets/linechart/axes/axes_test.go @@ -179,14 +179,15 @@ func TestY(t *testing.T) { func TestNewXDetails(t *testing.T) { tests := []struct { - desc string - numPoints int - yStart image.Point - cvsWidth int - cvsAr image.Rectangle - customLabels map[int]string - want *XDetails - wantErr bool + desc string + numPoints int + yStart image.Point + cvsWidth int + cvsAr image.Rectangle + customLabels map[int]string + labelOrientation LabelOrientation + want *XDetails + wantErr bool }{ { desc: "fails when numPoints is negative", @@ -247,7 +248,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, tc.customLabels) + got, err := NewXDetails(tc.numPoints, tc.yStart, tc.cvsAr, tc.customLabels, tc.labelOrientation) 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 ce2a22b..fef779f 100644 --- a/widgets/linechart/axes/label.go +++ b/widgets/linechart/axes/label.go @@ -23,6 +23,32 @@ import ( "github.com/mum4k/termdash/align" ) +// LabelOrientation represents the orientation of text labels. +type LabelOrientation int + +// String implements fmt.Stringer() +func (lo LabelOrientation) String() string { + if n, ok := labelOrientationNames[lo]; ok { + return n + } + return "LabelOrientationUnknown" +} + +// labelOrientationNames maps LabelOrientation values to human readable names. +var labelOrientationNames = map[LabelOrientation]string{ + LabelOrientationHorizontal: "LabelOrientationHorizontal", + LabelOrientationVertical: "LabelOrientationVertical", +} + +const ( + // LabelOrientationHorizontal is the default label orientation where text + // flows horizontally. + LabelOrientationHorizontal LabelOrientation = iota + + // LabelOrientationvertical is an orientation where text flows vertically. + LabelOrientationVertical +) + // Label is one value label on an axis. type Label struct { // Value if the value to be displayed. @@ -162,14 +188,14 @@ func (xs *xSpace) Sub(size int) error { // fit under the width of the axis. // 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) { +func xLabels(scale *XScale, graphZero image.Point, customLabels map[int]string, lo LabelOrientation) ([]*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, customLabels) + label, err := colLabel(scale, space, customLabels, lo) if err != nil { return nil, err } @@ -205,7 +231,7 @@ func xLabels(scale *XScale, graphZero image.Point, customLabels map[int]string) // colLabel returns a label placed 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, customLabels map[int]string) (*Label, error) { +func colLabel(scale *XScale, space *xSpace, customLabels map[int]string, lo LabelOrientation) (*Label, error) { pos := space.Relative() label, err := scale.CellLabel(pos.X) if err != nil { @@ -216,7 +242,13 @@ func colLabel(scale *XScale, space *xSpace, customLabels map[int]string) (*Label label = NewTextValue(custom) } - labelLen := len(label.Text()) + var labelLen int + switch lo { + case LabelOrientationHorizontal: + labelLen = len(label.Text()) + case LabelOrientationVertical: + labelLen = 1 + } if labelLen > space.Remaining() { return nil, nil } diff --git a/widgets/linechart/axes/label_test.go b/widgets/linechart/axes/label_test.go index d1fc746..334985b 100644 --- a/widgets/linechart/axes/label_test.go +++ b/widgets/linechart/axes/label_test.go @@ -151,13 +151,14 @@ func TestYLabels(t *testing.T) { func TestXLabels(t *testing.T) { const nonZeroDecimals = 2 tests := []struct { - desc string - numPoints int - graphWidth int - graphZero image.Point - customLabels map[int]string - want []*Label - wantErr bool + desc string + numPoints int + graphWidth int + graphZero image.Point + customLabels map[int]string + labelOrientation LabelOrientation + want []*Label + wantErr bool }{ { desc: "only one point", @@ -168,6 +169,16 @@ func TestXLabels(t *testing.T) { {NewValue(0, nonZeroDecimals), image.Point{0, 3}}, }, }, + { + desc: "only one point, vertical", + numPoints: 1, + graphWidth: 1, + graphZero: image.Point{0, 1}, + labelOrientation: LabelOrientationVertical, + want: []*Label{ + {NewValue(0, nonZeroDecimals), image.Point{0, 3}}, + }, + }, { desc: "two points, only one label fits", numPoints: 2, @@ -187,6 +198,17 @@ func TestXLabels(t *testing.T) { {NewValue(1, nonZeroDecimals), image.Point{4, 3}}, }, }, + { + desc: "two points, two labels fit exactly, vertical", + numPoints: 2, + graphWidth: 5, + graphZero: image.Point{0, 1}, + labelOrientation: LabelOrientationVertical, + want: []*Label{ + {NewValue(0, nonZeroDecimals), image.Point{0, 3}}, + {NewValue(1, nonZeroDecimals), image.Point{4, 3}}, + }, + }, { desc: "labels are placed according to graphZero", numPoints: 2, @@ -321,6 +343,59 @@ func TestXLabels(t *testing.T) { {NewValue(72, nonZeroDecimals), image.Point{4, 3}}, }, }, + { + desc: "longer labels, only two fit in horizontal", + numPoints: 1000, + graphWidth: 10, + graphZero: image.Point{0, 1}, + want: []*Label{ + {NewValue(0, nonZeroDecimals), image.Point{0, 3}}, + {NewValue(421, nonZeroDecimals), image.Point{4, 3}}, + }, + }, + { + desc: "longer labels, multiple fit in vertical", + numPoints: 1000, + graphWidth: 10, + graphZero: image.Point{0, 1}, + labelOrientation: LabelOrientationVertical, + want: []*Label{ + {NewValue(0, nonZeroDecimals), image.Point{0, 3}}, + {NewValue(421, nonZeroDecimals), image.Point{4, 3}}, + {NewValue(841, nonZeroDecimals), image.Point{8, 3}}, + }, + }, + { + desc: "longer custom labels, only one fits in horizontal", + numPoints: 1000, + graphWidth: 10, + graphZero: image.Point{0, 1}, + customLabels: map[int]string{ + 0: "zero label", + 421: "this one is even longer", + 841: "this label just keeps on going", + }, + want: []*Label{ + {NewTextValue("zero label"), image.Point{0, 3}}, + }, + }, + { + desc: "longer custom labels, all fit in vertical", + numPoints: 1000, + graphWidth: 10, + graphZero: image.Point{0, 1}, + customLabels: map[int]string{ + 0: "zero label", + 421: "this one is even longer", + 841: "this label just keeps on going", + }, + labelOrientation: LabelOrientationVertical, + want: []*Label{ + {NewTextValue("zero label"), image.Point{0, 3}}, + {NewTextValue("this one is even longer"), image.Point{4, 3}}, + {NewTextValue("this label just keeps on going"), image.Point{8, 3}}, + }, + }, } for _, tc := range tests { @@ -330,7 +405,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, tc.customLabels) + got, err := xLabels(scale, tc.graphZero, tc.customLabels, tc.labelOrientation) if (err != nil) != tc.wantErr { t.Errorf("xLabels => unexpected error: %v, wantErr: %v", err, tc.wantErr) } diff --git a/widgets/linechart/linechart.go b/widgets/linechart/linechart.go index c800fa9..e258dbf 100644 --- a/widgets/linechart/linechart.go +++ b/widgets/linechart/linechart.go @@ -187,7 +187,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(), lc.xLabels) + xd, err := axes.NewXDetails(lc.maxPoints(), yd.Start, cvs.Area(), lc.xLabels, lc.opts.xLabelOrientation) if err != nil { return fmt.Errorf("NewXDetails => %v", err) }