diff --git a/widgets/linechart/internal/axes/axes.go b/widgets/linechart/internal/axes/axes.go index 60e6a52..f5adb06 100644 --- a/widgets/linechart/internal/axes/axes.go +++ b/widgets/linechart/internal/axes/axes.go @@ -73,7 +73,7 @@ type YProperties struct { // ScaleMode determines how the Y axis scales. ScaleMode YScaleMode // ValueFormatter is the formatter used to format numeric values to string representation. - ValueFormatter valueFormatter + ValueFormatter func(float64) string } // NewYDetails retrieves details about the Y axis required to draw it on a diff --git a/widgets/linechart/internal/axes/axes_test.go b/widgets/linechart/internal/axes/axes_test.go index 8f974f4..f3a865d 100644 --- a/widgets/linechart/internal/axes/axes_test.go +++ b/widgets/linechart/internal/axes/axes_test.go @@ -213,8 +213,8 @@ func TestY(t *testing.T) { End: image.Point{1, 2}, Scale: mustNewYScale(0, 3, 2, nonZeroDecimals, YScaleModeAnchored, testValueFormatter), Labels: []*Label{ - {NewFormattedValue(0, nonZeroDecimals, testValueFormatter), image.Point{0, 1}}, - {NewFormattedValue(1.72, nonZeroDecimals, testValueFormatter), image.Point{0, 0}}, + {NewValue(0, nonZeroDecimals, ValueFormatter(testValueFormatter)), image.Point{0, 1}}, + {NewValue(1.72, nonZeroDecimals, ValueFormatter(testValueFormatter)), image.Point{0, 0}}, }, }, }, diff --git a/widgets/linechart/internal/axes/scale.go b/widgets/linechart/internal/axes/scale.go index dfd74a2..d3b8c6f 100644 --- a/widgets/linechart/internal/axes/scale.go +++ b/widgets/linechart/internal/axes/scale.go @@ -69,7 +69,7 @@ type YScale struct { // valueFormatter is the value formatter used for the labels // represented by the values on the scale. - valueFormatter valueFormatter + valueFormatter func(float64) string } // String implements fmt.Stringer. @@ -82,7 +82,7 @@ func (ys *YScale) String() string { // 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, mode YScaleMode, valueFormatter valueFormatter) (*YScale, error) { +func NewYScale(min, max float64, graphHeight, nonZeroDecimals int, mode YScaleMode, valueFormatter func(float64) string) (*YScale, error) { if max < min { return nil, fmt.Errorf("max(%v) cannot be less than min(%v)", max, min) } @@ -199,12 +199,13 @@ func (ys *YScale) CellLabel(y int) (*Value, error) { // yScaleNewValue is a helper method to get new values for the y scale // that selects the correct value factory method depending on the passed // arguments. -func yScaleNewValue(value float64, nonZeroDecimals int, valueFormatter valueFormatter) *Value { +func yScaleNewValue(value float64, nonZeroDecimals int, valueFormatter func(float64) string) *Value { + opts := []ValueOption{} if valueFormatter != nil { - return NewFormattedValue(value, nonZeroDecimals, valueFormatter) + opts = append(opts, ValueFormatter(valueFormatter)) } - return NewValue(value, nonZeroDecimals) + return NewValue(value, nonZeroDecimals, opts...) } // XScale is the scale of the X axis. diff --git a/widgets/linechart/internal/axes/scale_test.go b/widgets/linechart/internal/axes/scale_test.go index bc43011..84e971d 100644 --- a/widgets/linechart/internal/axes/scale_test.go +++ b/widgets/linechart/internal/axes/scale_test.go @@ -22,8 +22,8 @@ import ( ) // mustNewYScale returns a new YScale or panics. -func mustNewYScale(min, max float64, graphHeight, nonZeroDecimals int, mode YScaleMode, formatter valueFormatter) *YScale { - s, err := NewYScale(min, max, graphHeight, nonZeroDecimals, mode, formatter) +func mustNewYScale(min, max float64, graphHeight, nonZeroDecimals int, mode YScaleMode, valueFormatter func(float64) string) *YScale { + s, err := NewYScale(min, max, graphHeight, nonZeroDecimals, mode, valueFormatter) if err != nil { panic(err) } diff --git a/widgets/linechart/internal/axes/value.go b/widgets/linechart/internal/axes/value.go index 636a0a0..dea08b3 100644 --- a/widgets/linechart/internal/axes/value.go +++ b/widgets/linechart/internal/axes/value.go @@ -23,7 +23,30 @@ import ( "github.com/mum4k/termdash/internal/numbers" ) -type valueFormatter = func(float64) string +// ValueOption is used to provide options to the NewValue function. +type ValueOption interface { + // set sets the provided option. + set(*valueOptions) +} + +type valueOptions struct { + formatter func(v float64) string +} + +// valueOption implements ValueOption. +type valueOption func(opts *valueOptions) + +// set implements ValueOption.set. +func (vo valueOption) set(opts *valueOptions) { + vo(opts) +} + +// ValueFormatter sets a custom formatter for the value. +func ValueFormatter(formatter func(float64) string) ValueOption { + return valueOption(func(opts *valueOptions) { + opts.formatter = formatter + }) +} // Value represents one value. type Value struct { @@ -38,10 +61,10 @@ type Value struct { // NonZeroDecimals indicates the rounding precision used, it is provided on // a call to newValue. NonZeroDecimals int - // Formatter will format value to a string representation of the value, - // if Formatter is not present it will fallback to default format. - Formatter valueFormatter + // formatter will format value to a string representation of the value, + // if Formatter is not present it will fallback to default format. + formatter func(float64) string // text value if this value was constructed using NewTextValue. text string } @@ -53,20 +76,19 @@ func (v *Value) String() string { // NewValue returns a new instance representing the provided value, rounding // the value up to the specified number of non-zero decimal places. -func NewValue(v float64, nonZeroDecimals int) *Value { - return NewFormattedValue(v, nonZeroDecimals, nil) -} +func NewValue(v float64, nonZeroDecimals int, opts ...ValueOption) *Value { + opt := &valueOptions{} + for _, o := range opts { + o.set(opt) + } -// NewFormattedValue returns a new instance representing the provided value, -// using a value formatter. -func NewFormattedValue(v float64, nonZeroDecimals int, formatter valueFormatter) *Value { r, zd := numbers.RoundToNonZeroPlaces(v, nonZeroDecimals) return &Value{ Value: v, Rounded: r, ZeroDecimals: zd, NonZeroDecimals: nonZeroDecimals, - Formatter: formatter, + formatter: opt.formatter, } } @@ -85,19 +107,19 @@ func (v *Value) Text() string { return v.text } - if v.Formatter != nil { - return v.Formatter(v.Value) + if v.formatter != nil { + return v.formatter(v.Value) } - return v.defaultFormatter(v.Rounded) + return defaultFormatter(v.Rounded, v.NonZeroDecimals, v.ZeroDecimals) } -func (v *Value) defaultFormatter(value float64) string { +func defaultFormatter(value float64, nonZeroDecimals, zeroDecimals int) string { if math.Ceil(value) == value { return fmt.Sprintf("%.0f", value) } - format := fmt.Sprintf("%%.%df", v.NonZeroDecimals+v.ZeroDecimals) + format := fmt.Sprintf("%%.%df", nonZeroDecimals+zeroDecimals) t := fmt.Sprintf(format, value) if len(t) > 10 { t = fmt.Sprintf("%.2e", value) diff --git a/widgets/linechart/internal/axes/value_test.go b/widgets/linechart/internal/axes/value_test.go index 69d72a4..ee92677 100644 --- a/widgets/linechart/internal/axes/value_test.go +++ b/widgets/linechart/internal/axes/value_test.go @@ -22,10 +22,13 @@ import ( ) func TestValue(t *testing.T) { + formatter := func(float64) string { return "test" } + tests := []struct { desc string float float64 nonZeroDecimals int + formatter func(float64) string want *Value }{ { @@ -61,11 +64,24 @@ func TestValue(t *testing.T) { NonZeroDecimals: 0, }, }, + { + desc: "formatter value when value formatter as option", + float: 1.01234, + nonZeroDecimals: 0, + formatter: formatter, + want: &Value{ + Value: 1.01234, + Rounded: 1.01234, + ZeroDecimals: 1, + NonZeroDecimals: 0, + formatter: formatter, + }, + }, } for _, tc := range tests { t.Run(tc.desc, func(t *testing.T) { - got := NewValue(tc.float, tc.nonZeroDecimals) + got := NewValue(tc.float, tc.nonZeroDecimals, ValueFormatter(tc.formatter)) if diff := pretty.Compare(tc.want, got); diff != "" { t.Errorf("NewValue => unexpected diff (-want, +got):\n%s", diff) } @@ -124,16 +140,3 @@ func TestNewTextValue(t *testing.T) { t.Errorf("v.Text => got %q, want %q", got, want) } } - -func TestFormattedValue(t *testing.T) { - const ( - value = 42 - want = "test" - ) - - v := NewFormattedValue(value, 2, func(float64) string { return "test" }) - got := v.Text() - if got != want { - t.Errorf("v.Text => got %q, want %q", got, want) - } -}