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

xLabels now supports vertical labels.

This commit is contained in:
Jakub Sobon 2019-02-13 23:16:05 -05:00
parent 9f893eb482
commit 7ef79393df
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
5 changed files with 132 additions and 50 deletions

View File

@ -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 {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}