2019-01-07 00:16:48 -05:00
|
|
|
// Copyright 2019 Google Inc.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2019-01-07 00:15:31 -05:00
|
|
|
package axes
|
|
|
|
|
|
|
|
// scale.go calculates the scale of the Y axis.
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
"github.com/mum4k/termdash/canvas/braille"
|
2019-01-07 20:53:42 -05:00
|
|
|
"github.com/mum4k/termdash/numbers"
|
2019-01-07 00:15:31 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
// YScale is the scale of the Y axis.
|
|
|
|
type YScale struct {
|
|
|
|
// Min is the minimum value on the axis.
|
|
|
|
Min *Value
|
|
|
|
// Max is the maximum value on the axis.
|
|
|
|
Max *Value
|
|
|
|
// Step is the step in the value between pixels.
|
|
|
|
Step *Value
|
|
|
|
|
2019-01-07 22:55:28 -05:00
|
|
|
// CvsHeight is the height of the canvas the scale was calculated for.
|
|
|
|
CvsHeight int
|
|
|
|
// brailleHeight is the height of the braille canvas based on the CvsHeight.
|
2019-01-07 00:15:31 -05:00
|
|
|
brailleHeight int
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewYScale calculates the scale of the Y axis, given the boundary values and
|
|
|
|
// the height of the canvas. The nonZeroDecimals dictates rounding of the
|
2019-01-08 23:36:21 -05:00
|
|
|
// calculated scale, see NewValue for details.
|
2019-01-08 23:56:05 -05:00
|
|
|
// Max must be greater or equal to min. The cvsHeight must be a positive
|
|
|
|
// number.
|
|
|
|
func NewYScale(min, max float64, cvsHeight, nonZeroDecimals int) (*YScale, error) {
|
|
|
|
if max < min {
|
|
|
|
return nil, fmt.Errorf("max(%v) cannot be less than min(%v)", max, min)
|
|
|
|
}
|
|
|
|
if min := 1; cvsHeight < min {
|
|
|
|
return nil, fmt.Errorf("cvsHeight cannot be less than %d, got %d", min, cvsHeight)
|
|
|
|
}
|
|
|
|
|
2019-01-07 00:15:31 -05:00
|
|
|
brailleHeight := cvsHeight * braille.RowMult
|
|
|
|
usablePixels := brailleHeight - 1 // One pixel reserved for value zero.
|
|
|
|
|
2019-01-12 00:01:04 -05:00
|
|
|
if min > 0 { // If we only have positive data points, make the scale zero based (min).
|
|
|
|
min = 0
|
|
|
|
}
|
|
|
|
if max < 0 { // If we only have negative data points, make the scale zero based (max).
|
|
|
|
max = 0
|
|
|
|
}
|
2019-01-07 00:15:31 -05:00
|
|
|
diff := max - min
|
|
|
|
step := NewValue(diff/float64(usablePixels), nonZeroDecimals)
|
|
|
|
return &YScale{
|
|
|
|
Min: NewValue(min, nonZeroDecimals),
|
|
|
|
Max: NewValue(max, nonZeroDecimals),
|
|
|
|
Step: step,
|
2019-01-07 22:55:28 -05:00
|
|
|
CvsHeight: cvsHeight,
|
2019-01-07 00:15:31 -05:00
|
|
|
brailleHeight: brailleHeight,
|
2019-01-08 23:56:05 -05:00
|
|
|
}, nil
|
2019-01-07 00:15:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// PixelToValue given a Y coordinate of the pixel, returns its value according
|
|
|
|
// to the scale. The coordinate must be within bounds of the canvas height
|
2019-01-07 22:55:28 -05:00
|
|
|
// provided to NewYScale. Y coordinates grow down.
|
|
|
|
func (ys *YScale) PixelToValue(y int) (float64, error) {
|
|
|
|
pos, err := yToPosition(y, ys.brailleHeight)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
2019-01-07 00:15:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
2019-01-07 22:55:28 -05:00
|
|
|
case pos == 0:
|
2019-01-07 00:15:31 -05:00
|
|
|
return ys.Min.Rounded, nil
|
2019-01-07 22:55:28 -05:00
|
|
|
case pos == ys.brailleHeight-1:
|
2019-01-07 00:15:31 -05:00
|
|
|
return ys.Max.Rounded, nil
|
|
|
|
default:
|
2019-01-07 22:55:28 -05:00
|
|
|
v := float64(pos) * ys.Step.Rounded
|
2019-01-07 00:15:31 -05:00
|
|
|
if ys.Min.Value < 0 {
|
2019-01-12 21:13:03 -05:00
|
|
|
diff := -1 * ys.Min.Value
|
2019-01-07 00:15:31 -05:00
|
|
|
v -= diff
|
|
|
|
}
|
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ValueToPixel given a value, determines the Y coordinate of the pixel that
|
|
|
|
// most closely represents the value on the line chart according to the scale.
|
2019-01-07 22:55:28 -05:00
|
|
|
// The value must be within the bounds provided to NewYScale. Y coordinates
|
|
|
|
// grow down.
|
2019-01-07 00:15:31 -05:00
|
|
|
func (ys *YScale) ValueToPixel(v float64) (int, error) {
|
2019-01-08 23:56:05 -05:00
|
|
|
if ys.Step.Rounded == 0 {
|
|
|
|
return 0, nil
|
|
|
|
}
|
2019-01-07 00:15:31 -05:00
|
|
|
|
|
|
|
if ys.Min.Value < 0 {
|
2019-01-12 21:13:03 -05:00
|
|
|
diff := -1 * ys.Min.Value
|
2019-01-07 00:15:31 -05:00
|
|
|
v += diff
|
|
|
|
}
|
2019-01-07 22:55:28 -05:00
|
|
|
pos := int(numbers.Round(v / ys.Step.Rounded))
|
|
|
|
return positionToY(pos, ys.brailleHeight)
|
2019-01-07 00:15:31 -05:00
|
|
|
}
|
|
|
|
|
2019-01-08 23:36:21 -05:00
|
|
|
// CellLabel given a Y coordinate of a cell on the canvas, determines value of
|
|
|
|
// the label that should be next to it. The Y coordinate must be within the
|
2019-01-07 22:55:28 -05:00
|
|
|
// cvsHeight provided to NewYScale. Y coordinates grow down.
|
|
|
|
func (ys *YScale) CellLabel(y int) (*Value, error) {
|
|
|
|
pos, err := yToPosition(y, ys.CvsHeight)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
pixelY, err := positionToY(pos*braille.RowMult, ys.brailleHeight)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-01-07 00:15:31 -05:00
|
|
|
}
|
|
|
|
|
2019-01-07 22:55:28 -05:00
|
|
|
v, err := ys.PixelToValue(pixelY)
|
2019-01-07 00:15:31 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return NewValue(v, ys.Min.NonZeroDecimals), nil
|
|
|
|
}
|
2019-01-07 22:55:28 -05:00
|
|
|
|
2019-01-08 23:36:21 -05:00
|
|
|
// XScale is the scale of the X axis.
|
|
|
|
type XScale struct {
|
|
|
|
// Min is the minimum value on the axis.
|
|
|
|
Min *Value
|
|
|
|
// Max is the maximum value on the axis.
|
|
|
|
Max *Value
|
|
|
|
// Step is the step in the value between pixels.
|
|
|
|
Step *Value
|
|
|
|
|
|
|
|
// AxisWidth is the width of the canvas that is available to the X axis.
|
|
|
|
AxisWidth int
|
|
|
|
// brailleWidth is the height of the braille canvas based on the AxisWidth.
|
|
|
|
brailleWidth int
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewXScale calculates the scale of the X axis, given the number of data
|
|
|
|
// points in the series and the width on the canvas that is available to the X
|
|
|
|
// axis. The nonZeroDecimals dictates rounding of the calculated scale, see
|
|
|
|
// NewValue for details.
|
2019-01-08 23:56:05 -05:00
|
|
|
// The numPoints must be zero or positive number. The axisWidth must be a
|
|
|
|
// positive number.
|
2019-01-08 23:36:21 -05:00
|
|
|
func NewXScale(numPoints int, axisWidth, nonZeroDecimals int) (*XScale, error) {
|
|
|
|
if numPoints < 0 {
|
|
|
|
return nil, fmt.Errorf("numPoints cannot be negative, got %d", numPoints)
|
|
|
|
}
|
|
|
|
if min := 1; axisWidth < min {
|
|
|
|
return nil, fmt.Errorf("axisWidth must be at least %d, got %d", min, axisWidth)
|
|
|
|
}
|
|
|
|
|
|
|
|
brailleWidth := axisWidth * braille.ColMult
|
|
|
|
usablePixels := brailleWidth - 1 // One pixel reserved for value zero.
|
|
|
|
|
|
|
|
const min float64 = 0
|
|
|
|
max := float64(numPoints - 1)
|
2019-01-08 23:56:05 -05:00
|
|
|
if max < 0 {
|
|
|
|
max = 0
|
|
|
|
}
|
2019-01-08 23:36:21 -05:00
|
|
|
diff := max - min
|
|
|
|
step := NewValue(diff/float64(usablePixels), nonZeroDecimals)
|
|
|
|
return &XScale{
|
|
|
|
Min: NewValue(min, nonZeroDecimals),
|
|
|
|
Max: NewValue(max, nonZeroDecimals),
|
|
|
|
Step: step,
|
|
|
|
AxisWidth: axisWidth,
|
|
|
|
brailleWidth: brailleWidth,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PixelToValue given a X coordinate of the pixel, returns its value according
|
|
|
|
// to the scale. The coordinate must be within bounds of the canvas width
|
|
|
|
// provided to NewXScale. X coordinates grow right.
|
|
|
|
func (xs *XScale) PixelToValue(x int) (float64, error) {
|
|
|
|
if min, max := 0, xs.brailleWidth; x < min || x >= max {
|
|
|
|
return 0, fmt.Errorf("invalid x coordinate %d, must be in range %v < x < %v", x, min, max)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case x == 0:
|
|
|
|
return xs.Min.Rounded, nil
|
|
|
|
case x == xs.brailleWidth-1:
|
|
|
|
return xs.Max.Rounded, nil
|
|
|
|
default:
|
|
|
|
return float64(x) * xs.Step.Rounded, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ValueToPixel given a value, determines the X coordinate of the pixel that
|
|
|
|
// most closely represents the value on the line chart according to the scale.
|
|
|
|
// The value must be within the bounds provided to NewXScale. X coordinates
|
|
|
|
// grow right.
|
|
|
|
func (xs *XScale) ValueToPixel(v int) (int, error) {
|
|
|
|
fv := float64(v)
|
|
|
|
if min, max := xs.Min.Value, xs.Max.Rounded; fv < min || fv > max {
|
|
|
|
return 0, fmt.Errorf("invalid value %v, must be in range %v <= v <= %v", v, min, max)
|
|
|
|
}
|
2019-01-08 23:56:05 -05:00
|
|
|
if xs.Step.Rounded == 0 {
|
|
|
|
return 0, nil
|
|
|
|
}
|
2019-01-08 23:36:21 -05:00
|
|
|
return int(numbers.Round(fv / xs.Step.Rounded)), nil
|
|
|
|
}
|
|
|
|
|
2019-01-12 00:01:04 -05:00
|
|
|
// ValueToCell given a value, determines the X coordinate of the cell that
|
|
|
|
// most closely represents the value on the line chart according to the scale.
|
|
|
|
// The value must be within the bounds provided to NewXScale. X coordinates
|
|
|
|
// grow right.
|
|
|
|
func (xs *XScale) ValueToCell(v int) (int, error) {
|
|
|
|
p, err := xs.ValueToPixel(v)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return p / braille.ColMult, nil
|
|
|
|
}
|
|
|
|
|
2019-01-08 23:36:21 -05:00
|
|
|
// CellLabel given an X coordinate of a cell on the canvas, determines value of the
|
|
|
|
// label that should be next to it. The X coordinate must be within the
|
|
|
|
// axisWidth provided to NewXScale. X coordinates grow right.
|
2019-01-12 00:01:04 -05:00
|
|
|
// The returned value is rounded to the nearest int, rounding half away from zero.
|
2019-01-08 23:36:21 -05:00
|
|
|
func (xs *XScale) CellLabel(x int) (*Value, error) {
|
2019-01-12 00:01:04 -05:00
|
|
|
v, err := xs.PixelToValue(x * braille.ColMult)
|
2019-01-08 23:36:21 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-01-12 00:01:04 -05:00
|
|
|
return NewValue(numbers.Round(v), xs.Min.NonZeroDecimals), nil
|
2019-01-08 23:36:21 -05:00
|
|
|
}
|
|
|
|
|
2019-01-07 22:55:28 -05:00
|
|
|
// positionToY, given a position within the height, returns the Y coordinate of
|
|
|
|
// the position. Positions grow up, coordinates grow down.
|
|
|
|
//
|
|
|
|
// Positions Y Coordinates
|
|
|
|
// 2 | 0
|
|
|
|
// 1 | 1
|
|
|
|
// 0 | 2
|
|
|
|
func positionToY(pos int, height int) (int, error) {
|
|
|
|
max := height - 1
|
|
|
|
if min := 0; pos < min || pos > max {
|
|
|
|
return 0, fmt.Errorf("position %d out of bounds %d <= pos <= %d", pos, min, max)
|
|
|
|
}
|
|
|
|
return max - pos, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// yToPosition is the reverse of positionToY.
|
|
|
|
func yToPosition(y int, height int) (int, error) {
|
|
|
|
max := height - 1
|
|
|
|
if min := 0; y < min || y > max {
|
|
|
|
return 0, fmt.Errorf("Y coordinate %d out of bounds %d <= Y <= %d", y, min, max)
|
|
|
|
}
|
|
|
|
return -1*y + max, nil
|
|
|
|
}
|