mirror of https://github.com/mum4k/termdash.git
Merge pull request #96 from mum4k/y-axis-anchor-option
Adding a LineChart option that switches the Y axis from anchored to adaptive.
This commit is contained in:
commit
6f33bdde28
|
@ -14,8 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Fixed
|
||||
|
||||
- The LineChart now correctly displays positive and negative series that don't
|
||||
contain zero value.
|
||||
- The LineChart now has an option to change the behavior of the Y axis from
|
||||
zero anchored to adaptive.
|
||||
- Lint errors reported on the Go report card.
|
||||
|
||||
## [0.5.0] - 21-Jan-2019
|
||||
|
|
|
@ -88,7 +88,7 @@ func (y *Y) RequiredWidth() int {
|
|||
|
||||
// Details retrieves details about the Y axis required to draw it on a canvas
|
||||
// of the provided area.
|
||||
func (y *Y) Details(cvsAr image.Rectangle) (*YDetails, error) {
|
||||
func (y *Y) Details(cvsAr image.Rectangle, mode YScaleMode) (*YDetails, error) {
|
||||
cvsWidth := cvsAr.Dx()
|
||||
cvsHeight := cvsAr.Dy()
|
||||
maxWidth := cvsWidth - 1 // Reserve one row for the line chart itself.
|
||||
|
@ -97,7 +97,7 @@ func (y *Y) Details(cvsAr image.Rectangle) (*YDetails, error) {
|
|||
}
|
||||
|
||||
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)
|
||||
scale, err := NewYScale(y.min.Value, y.max.Value, graphHeight, nonZeroDecimals, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ func TestY(t *testing.T) {
|
|||
minVal float64
|
||||
maxVal float64
|
||||
update *updateY
|
||||
mode YScaleMode
|
||||
cvsAr image.Rectangle
|
||||
wantWidth int
|
||||
want *YDetails
|
||||
|
@ -71,13 +72,49 @@ func TestY(t *testing.T) {
|
|||
Width: 2,
|
||||
Start: image.Point{1, 0},
|
||||
End: image.Point{1, 2},
|
||||
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
|
||||
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored),
|
||||
Labels: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 1}},
|
||||
{NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "success for anchored scale",
|
||||
minVal: 1,
|
||||
maxVal: 3,
|
||||
mode: YScaleModeAnchored,
|
||||
cvsAr: image.Rect(0, 0, 3, 4),
|
||||
wantWidth: 2,
|
||||
want: &YDetails{
|
||||
Width: 2,
|
||||
Start: image.Point{1, 0},
|
||||
End: image.Point{1, 2},
|
||||
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored),
|
||||
Labels: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{0, 1}},
|
||||
{NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "success for adaptive scale",
|
||||
minVal: 1,
|
||||
maxVal: 6,
|
||||
mode: YScaleModeAdaptive,
|
||||
cvsAr: image.Rect(0, 0, 3, 4),
|
||||
wantWidth: 2,
|
||||
want: &YDetails{
|
||||
Width: 2,
|
||||
Start: image.Point{1, 0},
|
||||
End: image.Point{1, 2},
|
||||
Scale: mustNewYScale(1, 6, 2, nonZeroDecimals, YScaleModeAdaptive),
|
||||
Labels: []*Label{
|
||||
{NewValue(1, nonZeroDecimals), image.Point{0, 1}},
|
||||
{NewValue(3.88, nonZeroDecimals), image.Point{0, 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "cvsWidth just accommodates the longest label",
|
||||
minVal: 0,
|
||||
|
@ -88,7 +125,7 @@ func TestY(t *testing.T) {
|
|||
Width: 5,
|
||||
Start: image.Point{4, 0},
|
||||
End: image.Point{4, 2},
|
||||
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
|
||||
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored),
|
||||
Labels: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{3, 1}},
|
||||
{NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
|
||||
|
@ -105,7 +142,7 @@ func TestY(t *testing.T) {
|
|||
Width: 5,
|
||||
Start: image.Point{4, 0},
|
||||
End: image.Point{4, 2},
|
||||
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals),
|
||||
Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored),
|
||||
Labels: []*Label{
|
||||
{NewValue(0, nonZeroDecimals), image.Point{3, 1}},
|
||||
{NewValue(1.72, nonZeroDecimals), image.Point{0, 0}},
|
||||
|
@ -126,7 +163,7 @@ func TestY(t *testing.T) {
|
|||
t.Errorf("RequiredWidth => got %v, want %v", gotWidth, tc.wantWidth)
|
||||
}
|
||||
|
||||
got, err := y.Details(tc.cvsAr)
|
||||
got, err := y.Details(tc.cvsAr, tc.mode)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("Details => unexpected error: %v, wantErr: %v", err, tc.wantErr)
|
||||
}
|
||||
|
|
|
@ -129,7 +129,7 @@ func TestYLabels(t *testing.T) {
|
|||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
scale, err := NewYScale(tc.min, tc.max, tc.graphHeight, nonZeroDecimals)
|
||||
scale, err := NewYScale(tc.min, tc.max, tc.graphHeight, nonZeroDecimals, YScaleModeAnchored)
|
||||
if err != nil {
|
||||
t.Fatalf("NewYScale => unexpected error: %v", err)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,35 @@ import (
|
|||
"github.com/mum4k/termdash/numbers"
|
||||
)
|
||||
|
||||
// YScaleMode determines whether the Y scale is anchored to the zero value.
|
||||
type YScaleMode int
|
||||
|
||||
// String implements fmt.Stringer()
|
||||
func (ysm YScaleMode) String() string {
|
||||
if n, ok := yScaleModeNames[ysm]; ok {
|
||||
return n
|
||||
}
|
||||
return "YScaleModeUnknown"
|
||||
}
|
||||
|
||||
// yScaleModeNames maps YScaleMode values to human readable names.
|
||||
var yScaleModeNames = map[YScaleMode]string{
|
||||
YScaleModeAnchored: "YScaleModeAnchored",
|
||||
YScaleModeAdaptive: "YScaleModeAdaptive",
|
||||
}
|
||||
|
||||
const (
|
||||
// YScaleModeAnchored is a mode in which the Y scale always starts at value
|
||||
// zero regardless of the min and max on the series.
|
||||
YScaleModeAnchored YScaleMode = iota
|
||||
|
||||
// YScaleModeAdaptive is a mode where the Y scale adapts its base value
|
||||
// according to the min and max on the series.
|
||||
// I.e. it starts at min for all-positive series and at max for
|
||||
// all-negative series.
|
||||
YScaleModeAdaptive
|
||||
)
|
||||
|
||||
// YScale is the scale of the Y axis.
|
||||
type YScale struct {
|
||||
// Min is the minimum value on the axis.
|
||||
|
@ -44,7 +73,7 @@ type YScale struct {
|
|||
// calculated scale, see NewValue for details.
|
||||
// Max must be greater or equal to min. The graphHeight must be a positive
|
||||
// number.
|
||||
func NewYScale(min, max float64, graphHeight, nonZeroDecimals int) (*YScale, error) {
|
||||
func NewYScale(min, max float64, graphHeight, nonZeroDecimals int, mode YScaleMode) (*YScale, error) {
|
||||
if max < min {
|
||||
return nil, fmt.Errorf("max(%v) cannot be less than min(%v)", max, min)
|
||||
}
|
||||
|
@ -55,11 +84,27 @@ func NewYScale(min, max float64, graphHeight, nonZeroDecimals int) (*YScale, err
|
|||
brailleHeight := graphHeight * braille.RowMult
|
||||
usablePixels := brailleHeight - 1 // One pixel reserved for value zero.
|
||||
|
||||
if min > 0 && min == max { // If all the data points are equal, make the scale zero based so we can draw something.
|
||||
min = 0
|
||||
}
|
||||
if max < 0 && min == max { // If all the data points are equal, make the scale zero based so we can draw something.
|
||||
max = 0
|
||||
switch mode {
|
||||
case YScaleModeAnchored:
|
||||
// Anchor the axis at the zero value.
|
||||
if min > 0 {
|
||||
min = 0
|
||||
}
|
||||
if max < 0 {
|
||||
max = 0
|
||||
}
|
||||
|
||||
case YScaleModeAdaptive:
|
||||
// Even in this mode, we still anchor the axis at the zero if all the
|
||||
// data points are equal, so we can still draw something.
|
||||
if min > 0 && min == max {
|
||||
min = 0
|
||||
}
|
||||
if max < 0 && min == max {
|
||||
max = 0
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported mode: %v(%d)", mode, mode)
|
||||
}
|
||||
diff := max - min
|
||||
step := NewValue(diff/float64(usablePixels), nonZeroDecimals)
|
||||
|
|
|
@ -22,8 +22,8 @@ import (
|
|||
)
|
||||
|
||||
// mustNewYScale returns a new YScale or panics.
|
||||
func mustNewYScale(min, max float64, graphHeight, nonZeroDecimals int) *YScale {
|
||||
s, err := NewYScale(min, max, graphHeight, nonZeroDecimals)
|
||||
func mustNewYScale(min, max float64, graphHeight, nonZeroDecimals int, mode YScaleMode) *YScale {
|
||||
s, err := NewYScale(min, max, graphHeight, nonZeroDecimals, mode)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ func TestYScale(t *testing.T) {
|
|||
max float64
|
||||
graphHeight int
|
||||
nonZeroDecimals int
|
||||
mode YScaleMode
|
||||
pixelToValueTests []pixelToValueTest
|
||||
valueToPixelTests []valueToPixelTest
|
||||
cellLabelTests []cellLabelTest
|
||||
|
@ -141,6 +142,15 @@ func TestYScale(t *testing.T) {
|
|||
{4, nil, true},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "fails on an unsupported Y scale mode",
|
||||
min: 0,
|
||||
max: 0,
|
||||
graphHeight: 1,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleMode(-1),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "works without data points",
|
||||
min: 0,
|
||||
|
@ -158,11 +168,12 @@ func TestYScale(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
desc: "min and max are non-zero positive and equal, scale is zero based",
|
||||
desc: "min and max are non-zero positive and equal, scale is anchored",
|
||||
min: 6,
|
||||
max: 6,
|
||||
graphHeight: 1,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAnchored,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{3, 0, false},
|
||||
{2, 2, false},
|
||||
|
@ -183,11 +194,38 @@ func TestYScale(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
desc: "min and max are non-zero negative and equal, scale is zero based",
|
||||
desc: "min and max are non-zero positive and equal, scale is adaptive",
|
||||
min: 6,
|
||||
max: 6,
|
||||
graphHeight: 1,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAdaptive,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{3, 0, false},
|
||||
{2, 2, false},
|
||||
{1, 4, false},
|
||||
{0, 6, false},
|
||||
},
|
||||
valueToPixelTests: []valueToPixelTest{
|
||||
{0, 3, false},
|
||||
{0.5, 3, false},
|
||||
{1, 2, false},
|
||||
{1.5, 2, false},
|
||||
{2, 2, false},
|
||||
{4, 1, false},
|
||||
{6, 0, false},
|
||||
},
|
||||
cellLabelTests: []cellLabelTest{
|
||||
{0, NewValue(0, 2), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "min and max are non-zero negative and equal, scale is anchored",
|
||||
min: -6,
|
||||
max: -6,
|
||||
graphHeight: 1,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAnchored,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{3, -6, false},
|
||||
{2, -4, false},
|
||||
|
@ -208,11 +246,64 @@ func TestYScale(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
desc: "min is non-zero positive, not equal to max, scale is min based",
|
||||
desc: "min and max are non-zero negative and equal, scale is adaptive",
|
||||
min: -6,
|
||||
max: -6,
|
||||
graphHeight: 1,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAdaptive,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{3, -6, false},
|
||||
{2, -4, false},
|
||||
{1, -2, false},
|
||||
{0, 0, false},
|
||||
},
|
||||
valueToPixelTests: []valueToPixelTest{
|
||||
{0, 0, false},
|
||||
{0.5, 0, false},
|
||||
{-1, 0, false},
|
||||
{-1.5, 1, false},
|
||||
{-2, 1, false},
|
||||
{-4, 2, false},
|
||||
{-6, 3, false},
|
||||
},
|
||||
cellLabelTests: []cellLabelTest{
|
||||
{0, NewValue(-6, 2), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "min is non-zero positive, not equal to max, scale is anchored",
|
||||
min: 1,
|
||||
max: 7,
|
||||
graphHeight: 1,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAnchored,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{3, 0, false},
|
||||
{2, 2.34, false},
|
||||
{1, 4.68, false},
|
||||
{0, 7, false},
|
||||
},
|
||||
valueToPixelTests: []valueToPixelTest{
|
||||
{0, 3, false},
|
||||
{0.5, 3, false},
|
||||
{1, 3, false},
|
||||
{1.5, 2, false},
|
||||
{2, 2, false},
|
||||
{4, 1, false},
|
||||
{6, 0, false},
|
||||
},
|
||||
cellLabelTests: []cellLabelTest{
|
||||
{0, NewValue(0, 2), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "min is non-zero positive, not equal to max, scale is adaptive",
|
||||
min: 1,
|
||||
max: 7,
|
||||
graphHeight: 1,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAdaptive,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{3, 1, false},
|
||||
{2, 3, false},
|
||||
|
@ -232,7 +323,6 @@ func TestYScale(t *testing.T) {
|
|||
{0, NewValue(1, 2), false},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
desc: "integer scale",
|
||||
min: 0,
|
||||
|
@ -335,11 +425,36 @@ func TestYScale(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
desc: "negative integer scale, max is also negative",
|
||||
desc: "negative integer scale, max is also negative, scale is anchored",
|
||||
min: -6,
|
||||
max: -1,
|
||||
graphHeight: 1,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAnchored,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{3, -6, false},
|
||||
{2, -4, false},
|
||||
{1, -2, false},
|
||||
{0, 0, false},
|
||||
},
|
||||
valueToPixelTests: []valueToPixelTest{
|
||||
{-6, 3, false},
|
||||
{-4, 2, false},
|
||||
{-2, 1, false},
|
||||
{-1, 0, false},
|
||||
{0, 0, false},
|
||||
},
|
||||
cellLabelTests: []cellLabelTest{
|
||||
{0, NewValue(-6, 2), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "negative integer scale, max is also negative, scale is adaptive",
|
||||
min: -7,
|
||||
max: -1,
|
||||
graphHeight: 1,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAdaptive,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{3, -7, false},
|
||||
{2, -5, false},
|
||||
|
@ -357,7 +472,7 @@ func TestYScale(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
desc: "zero based float scale",
|
||||
desc: "anchored based float scale",
|
||||
min: 0,
|
||||
max: 0.3,
|
||||
graphHeight: 1,
|
||||
|
@ -392,11 +507,40 @@ func TestYScale(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
desc: "regression for #92, positive values only",
|
||||
desc: "regression for #92, positive values only, scale is anchored",
|
||||
min: 1600,
|
||||
max: 1900,
|
||||
graphHeight: 4,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAnchored,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{15, 0, false},
|
||||
{14, 126.67, false},
|
||||
{2, 1646.71, false},
|
||||
{1, 1773.38, false},
|
||||
{0, 1900, false},
|
||||
},
|
||||
valueToPixelTests: []valueToPixelTest{
|
||||
{0, 15, false},
|
||||
{126, 14, false},
|
||||
{1600, 2, false},
|
||||
{1800, 1, false},
|
||||
{1900, 0, false},
|
||||
},
|
||||
cellLabelTests: []cellLabelTest{
|
||||
{3, NewValue(0, 2), false},
|
||||
{2, NewValue(506.68, 2), false},
|
||||
{1, NewValue(1013.36, 2), false},
|
||||
{0, NewValue(1520.04, 2), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "regression for #92, positive values only, scale is adaptive",
|
||||
min: 1600,
|
||||
max: 1900,
|
||||
graphHeight: 4,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAdaptive,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{15, 1600, false},
|
||||
{14, 1620, false},
|
||||
|
@ -443,11 +587,37 @@ func TestYScale(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
desc: "regression for #92, negative values only",
|
||||
desc: "regression for #92, negative values only, scale is anchored",
|
||||
min: -1900,
|
||||
max: -1600,
|
||||
graphHeight: 4,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAnchored,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{15, -1900, false},
|
||||
{14, -1773.33, false},
|
||||
{5, -633.3, false},
|
||||
{0, 0, false},
|
||||
},
|
||||
valueToPixelTests: []valueToPixelTest{
|
||||
{-1900, 15, false},
|
||||
{-1800, 14, false},
|
||||
{-633.3, 5, false},
|
||||
{0, 0, false},
|
||||
},
|
||||
cellLabelTests: []cellLabelTest{
|
||||
{3, NewValue(-1900, 2), false},
|
||||
{2, NewValue(-1393.32, 2), false},
|
||||
{1, NewValue(-886.64, 2), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "regression for #92, negative values only, scale is adaptiove",
|
||||
min: -1900,
|
||||
max: -1600,
|
||||
graphHeight: 4,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAdaptive,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{15, -1900, false},
|
||||
{14, -1880, false},
|
||||
|
@ -494,11 +664,62 @@ func TestYScale(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
desc: "regression for #92, negative and positive values",
|
||||
desc: "regression for #92, negative and positive values, scale is adaptive",
|
||||
min: -100,
|
||||
max: 200,
|
||||
graphHeight: 4,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAdaptive,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{15, -100, false},
|
||||
{14, -80, false},
|
||||
{13, -60, false},
|
||||
{12, -40, false},
|
||||
{11, -20, false},
|
||||
{10, 0, false},
|
||||
{9, 20, false},
|
||||
{8, 40, false},
|
||||
{7, 60, false},
|
||||
{6, 80, false},
|
||||
{5, 100, false},
|
||||
{4, 120, false},
|
||||
{3, 140, false},
|
||||
{2, 160, false},
|
||||
{1, 180, false},
|
||||
{0, 200, false},
|
||||
},
|
||||
valueToPixelTests: []valueToPixelTest{
|
||||
{-100, 15, false},
|
||||
{-80, 14, false},
|
||||
{-60, 13, false},
|
||||
{-40, 12, false},
|
||||
{-20, 11, false},
|
||||
{0, 10, false},
|
||||
{20, 9, false},
|
||||
{40, 8, false},
|
||||
{60, 7, false},
|
||||
{80, 6, false},
|
||||
{100, 5, false},
|
||||
{120, 4, false},
|
||||
{140, 3, false},
|
||||
{160, 2, false},
|
||||
{180, 1, false},
|
||||
{200, 0, false},
|
||||
},
|
||||
cellLabelTests: []cellLabelTest{
|
||||
{3, NewValue(-100, 2), false},
|
||||
{2, NewValue(-20, 2), false},
|
||||
{1, NewValue(60, 2), false},
|
||||
{0, NewValue(140, 2), false},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "regression for #92, negative and positive values, scale is anchored",
|
||||
min: -100,
|
||||
max: 200,
|
||||
graphHeight: 4,
|
||||
nonZeroDecimals: 2,
|
||||
mode: YScaleModeAnchored,
|
||||
pixelToValueTests: []pixelToValueTest{
|
||||
{15, -100, false},
|
||||
{14, -80, false},
|
||||
|
@ -545,7 +766,7 @@ func TestYScale(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, test := range tests {
|
||||
scale, err := NewYScale(test.min, test.max, test.graphHeight, test.nonZeroDecimals)
|
||||
scale, err := NewYScale(test.min, test.max, test.graphHeight, test.nonZeroDecimals, test.mode)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Errorf("NewYScale => unexpected error: %v, wantErr: %v", err, test.wantErr)
|
||||
}
|
||||
|
|
|
@ -173,7 +173,7 @@ func (lc *LineChart) Draw(cvs *canvas.Canvas) error {
|
|||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
|
||||
yd, err := lc.yAxis.Details(cvs.Area())
|
||||
yd, err := lc.yAxis.Details(cvs.Area(), lc.opts.yAxisMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lc.yAxis.Details => %v", err)
|
||||
}
|
||||
|
|
|
@ -188,6 +188,75 @@ func TestLineChartDraws(t *testing.T) {
|
|||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws anchored Y axis",
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("first", []float64{1600, 1900})
|
||||
},
|
||||
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{6, 0}, End: image.Point{6, 8}},
|
||||
{Start: image.Point{6, 8}, End: image.Point{19, 8}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "0", image.Point{5, 7})
|
||||
testdraw.MustText(c, "980.80", image.Point{0, 3})
|
||||
testdraw.MustText(c, "0", image.Point{7, 9})
|
||||
testdraw.MustText(c, "1", image.Point{19, 9})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(7, 0, 20, 8)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 5}, image.Point{25, 0})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "draws adaptive Y axis",
|
||||
opts: []Option{
|
||||
YAxisAdaptive(),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
writes: func(lc *LineChart) error {
|
||||
return lc.Series("first", []float64{1600, 1900})
|
||||
},
|
||||
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{7, 0}, End: image.Point{7, 8}},
|
||||
{Start: image.Point{7, 8}, End: image.Point{19, 8}},
|
||||
}
|
||||
testdraw.MustHVLines(c, lines)
|
||||
|
||||
// Value labels.
|
||||
testdraw.MustText(c, "1600", image.Point{3, 7})
|
||||
testdraw.MustText(c, "1754.88", image.Point{0, 3})
|
||||
testdraw.MustText(c, "0", image.Point{8, 9})
|
||||
testdraw.MustText(c, "1", image.Point{19, 9})
|
||||
|
||||
// Braille line.
|
||||
graphAr := image.Rect(8, 0, 20, 8)
|
||||
bc := testbraille.MustNew(graphAr)
|
||||
testdraw.MustBrailleLine(bc, image.Point{0, 31}, image.Point{23, 0})
|
||||
testbraille.MustCopyTo(bc, c)
|
||||
|
||||
testcanvas.MustApply(c, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "custom X labels",
|
||||
canvas: image.Rect(0, 0, 20, 10),
|
||||
|
|
|
@ -14,7 +14,10 @@
|
|||
|
||||
package linechart
|
||||
|
||||
import "github.com/mum4k/termdash/cell"
|
||||
import (
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/widgets/linechart/axes"
|
||||
)
|
||||
|
||||
// options.go contains configurable options for LineChart.
|
||||
|
||||
|
@ -29,6 +32,7 @@ type options struct {
|
|||
axesCellOpts []cell.Option
|
||||
xLabelCellOpts []cell.Option
|
||||
yLabelCellOpts []cell.Option
|
||||
yAxisMode axes.YScaleMode
|
||||
}
|
||||
|
||||
// newOptions returns a new options instance.
|
||||
|
@ -68,3 +72,17 @@ func YLabelCellOpts(co ...cell.Option) Option {
|
|||
opts.yLabelCellOpts = co
|
||||
})
|
||||
}
|
||||
|
||||
// YAxisAdaptive makes the Y axis adapt its base value depending on the
|
||||
// provided series.
|
||||
// Without this option, the Y axis always starts at the zero value regardless of
|
||||
// values available in the series.
|
||||
// When this option is specified and the series don't contain value zero, the Y
|
||||
// axis will be adapted to the minimum value for all-positive series or the
|
||||
// maximum value for all-negative series. The Y axis still starts at the zero
|
||||
// value if the series contain both positive and negative values.
|
||||
func YAxisAdaptive() Option {
|
||||
return option(func(opts *options) {
|
||||
opts.yAxisMode = axes.YScaleModeAdaptive
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue