Skip to content

Commit

Permalink
Merge 21083a9 into b3346a1
Browse files Browse the repository at this point in the history
  • Loading branch information
mum4k committed Jan 27, 2019
2 parents b3346a1 + 21083a9 commit 1ff3a6e
Show file tree
Hide file tree
Showing 9 changed files with 419 additions and 29 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions widgets/linechart/axes/axes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
Expand Down
45 changes: 41 additions & 4 deletions widgets/linechart/axes/axes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func TestY(t *testing.T) {
minVal float64
maxVal float64
update *updateY
mode YScaleMode
cvsAr image.Rectangle
wantWidth int
want *YDetails
Expand Down Expand Up @@ -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,
Expand All @@ -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}},
Expand All @@ -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}},
Expand All @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion widgets/linechart/axes/label_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
57 changes: 51 additions & 6 deletions widgets/linechart/axes/scale.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 1ff3a6e

Please sign in to comment.