From 9f893eb482e7ba16b45a14453ff2eb61e35d03e6 Mon Sep 17 00:00:00 2001 From: Jakub Sobon Date: Wed, 13 Feb 2019 22:53:19 -0500 Subject: [PATCH] Function to calculate required height. And options to set label orientation. --- widgets/linechart/axes/axes.go | 67 ++++++++++++++++++++++++----- widgets/linechart/axes/axes_test.go | 50 +++++++++++++++++++++ widgets/linechart/options.go | 25 +++++++++-- 3 files changed, 128 insertions(+), 14 deletions(-) diff --git a/widgets/linechart/axes/axes.go b/widgets/linechart/axes/axes.go index ddeceeb..bb947a4 100644 --- a/widgets/linechart/axes/axes.go +++ b/widgets/linechart/axes/axes.go @@ -26,8 +26,8 @@ const ( // rounded up to. nonZeroDecimals = 2 - // yAxisWidth is width of the Y axis. - yAxisWidth = 1 + // axisWidth is width of an axis. + axisWidth = 1 ) // YDetails contain information about the Y axis that will be drawn onto the @@ -75,15 +75,16 @@ func (y *Y) Update(minVal, maxVal float64) { y.min, y.max = NewValue(minVal, nonZeroDecimals), NewValue(maxVal, nonZeroDecimals) } -// RequiredWidth calculates the minimum width required in order to draw the Y axis. +// RequiredWidth calculates the minimum width required in order to draw the Y +// axis and its labels. func (y *Y) RequiredWidth() int { // This is an estimation only, it is possible that more labels in the // middle will be generated and might be wider than this. Such cases are // handled on the call to Details when the size of canvas is known. - return widestLabel([]*Label{ + return longestLabel([]*Label{ {Value: y.min}, {Value: y.max}, - }) + yAxisWidth + }) + axisWidth } // Details retrieves details about the Y axis required to draw it on a canvas @@ -103,7 +104,7 @@ func (y *Y) Details(cvsAr image.Rectangle, mode YScaleMode) (*YDetails, error) { } // See how the labels would look like on the entire maxWidth. - maxLabelWidth := maxWidth - yAxisWidth + maxLabelWidth := maxWidth - axisWidth labels, err := yLabels(scale, maxLabelWidth) if err != nil { return nil, err @@ -112,7 +113,7 @@ func (y *Y) Details(cvsAr image.Rectangle, mode YScaleMode) (*YDetails, error) { var width int // Determine the largest label, which might be less than maxWidth. // Such case would allow us to save more space for the line chart itself. - widest := widestLabel(labels) + widest := longestLabel(labels) if widest < maxLabelWidth { // Save the space and recalculate the labels, since they need to be realigned. l, err := yLabels(scale, widest) @@ -120,7 +121,7 @@ func (y *Y) Details(cvsAr image.Rectangle, mode YScaleMode) (*YDetails, error) { return nil, err } labels = l - width = widest + yAxisWidth // One for the axis itself. + width = widest + axisWidth // One for the axis itself. } else { width = maxWidth } @@ -134,8 +135,8 @@ func (y *Y) Details(cvsAr image.Rectangle, mode YScaleMode) (*YDetails, error) { }, nil } -// widestLabel returns the width of the widest label. -func widestLabel(labels []*Label) int { +// longestLabel returns the width of the widest label. +func longestLabel(labels []*Label) int { var widest int for _, label := range labels { if l := len(label.Value.Text()); l > widest { @@ -193,3 +194,49 @@ func NewXDetails(numPoints int, yStart image.Point, cvsAr image.Rectangle, custo Labels: labels, }, 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 { + if lo == LabelOrientationHorizontal { + // One row for the X axis and one row for its labels flowing + // horizontally. + return axisWidth + 1 + } + + labels := []*Label{ + {Value: NewValue(float64(numPoints), nonZeroDecimals)}, + } + for _, cl := range customLabels { + labels = append(labels, &Label{ + Value: NewTextValue(cl), + }) + } + return longestLabel(labels) + axisWidth +} diff --git a/widgets/linechart/axes/axes_test.go b/widgets/linechart/axes/axes_test.go index e79b47c..41164be 100644 --- a/widgets/linechart/axes/axes_test.go +++ b/widgets/linechart/axes/axes_test.go @@ -261,3 +261,53 @@ func TestNewXDetails(t *testing.T) { }) } } + +func TestRequiredHeight(t *testing.T) { + tests := []struct { + desc string + numPoints int + customLabels map[int]string + labelOrientation LabelOrientation + want int + }{ + { + desc: "horizontal orientation", + want: 2, + }, + { + desc: "vertical orientation, no custom labels, need single row for max label", + numPoints: 9, + labelOrientation: LabelOrientationVertical, + want: 2, + }, + { + desc: "vertical orientation, no custom labels, need multiple row for max label", + numPoints: 100, + labelOrientation: LabelOrientationVertical, + want: 4, + }, + { + desc: "vertical orientation, custom labels but all shorter than max label", + numPoints: 100, + customLabels: map[int]string{1: "a", 2: "b"}, + labelOrientation: LabelOrientationVertical, + want: 4, + }, + { + desc: "vertical orientation, custom labels and some longer than max label", + numPoints: 100, + customLabels: map[int]string{1: "a", 2: "bbbbb"}, + labelOrientation: LabelOrientationVertical, + want: 6, + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + got := RequiredHeight(tc.numPoints, tc.customLabels, tc.labelOrientation) + if got != tc.want { + t.Errorf("RequiredHeight => %d, want %d", got, tc.want) + } + }) + } +} diff --git a/widgets/linechart/options.go b/widgets/linechart/options.go index 326cb80..5e77f68 100644 --- a/widgets/linechart/options.go +++ b/widgets/linechart/options.go @@ -29,10 +29,11 @@ type Option interface { // options stores the provided options. type options struct { - axesCellOpts []cell.Option - xLabelCellOpts []cell.Option - yLabelCellOpts []cell.Option - yAxisMode axes.YScaleMode + axesCellOpts []cell.Option + xLabelCellOpts []cell.Option + xLabelOrientation axes.LabelOrientation + yLabelCellOpts []cell.Option + yAxisMode axes.YScaleMode } // newOptions returns a new options instance. @@ -66,6 +67,22 @@ func XLabelCellOpts(co ...cell.Option) Option { }) } +// XLabelsVertical makes the labels under the X axis flow vertically. +// Defaults to labels that flow horizontally. +func XLabelsVertical() Option { + return option(func(opts *options) { + opts.xLabelOrientation = axes.LabelOrientationVertical + }) +} + +// XLabelsHorizontal makes the labels under the X axis flow horizontally. +// This is the default option. +func XLabelsHorizontal() Option { + return option(func(opts *options) { + opts.xLabelOrientation = axes.LabelOrientationHorizontal + }) +} + // YLabelCellOpts set the cell options for the labels on the Y axis. func YLabelCellOpts(co ...cell.Option) Option { return option(func(opts *options) {