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:
parent
9f893eb482
commit
7ef79393df
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user