Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes-336: Extend the Ticker interface to allow passing a format function #362

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 25 additions & 18 deletions axis.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ const displayPrecision = 4

// Ticker creates Ticks in a specified range
type Ticker interface {
// Ticks returns Ticks in a specified range
Ticks(min, max float64) []Tick
// Ticks returns Ticks in a specified range and formatted according to the
// given format function.
// When no format is provided (nil) a sane default is used.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// When format is nil DefaultTickFormat is used.

Sanity is subjective. We can claim this in talks, blogs and wiki articles, but I don't think it's appropriate in documentation.

Ticks(min, max float64, format func(v float64, prec int) string) []Tick
}

// Normalizer rescales values from the data coordinate system to the
Expand Down Expand Up @@ -74,6 +76,10 @@ type Axis struct {
// returned by the Marker function that are not in
// range of the axis are not drawn.
Marker Ticker

// Format function used to format the Axis Ticks.
// When no format is provided a sane default is used.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// When format is nil DefaultTickFormat is used.

Format func(v float64, prec int) string
}

// Scale transforms a value given in the data coordinate system
Expand Down Expand Up @@ -129,6 +135,7 @@ func makeAxis(orientation bool) (Axis, error) {
}
a.Tick.Length = vg.Points(8)
a.Tick.Marker = DefaultTicks{}
a.Tick.Format = DefaultTickFormat

return a, nil
}
Expand Down Expand Up @@ -200,7 +207,7 @@ func (a *horizontalAxis) size() (h vg.Length) {
h -= a.Label.Font.Extents().Descent
h += a.Label.Height(a.Label.Text)
}
if marks := a.Tick.Marker.Ticks(a.Min, a.Max); len(marks) > 0 {
if marks := a.Tick.Marker.Ticks(a.Min, a.Max, a.Tick.Format); len(marks) > 0 {
if a.drawTicks() {
h += a.Tick.Length
}
Expand All @@ -220,7 +227,7 @@ func (a *horizontalAxis) draw(c draw.Canvas) {
y += a.Label.Height(a.Label.Text)
}

marks := a.Tick.Marker.Ticks(a.Min, a.Max)
marks := a.Tick.Marker.Ticks(a.Min, a.Max, a.Tick.Format)
ticklabelheight := tickLabelHeight(a.Tick.Label, marks)
for _, t := range marks {
x := c.X(a.Norm(t.Value))
Expand Down Expand Up @@ -254,7 +261,7 @@ func (a *horizontalAxis) draw(c draw.Canvas) {

// GlyphBoxes returns the GlyphBoxes for the tick labels.
func (a *horizontalAxis) GlyphBoxes(*Plot) (boxes []GlyphBox) {
for _, t := range a.Tick.Marker.Ticks(a.Min, a.Max) {
for _, t := range a.Tick.Marker.Ticks(a.Min, a.Max, a.Tick.Format) {
if t.IsMinor() {
continue
}
Expand All @@ -278,7 +285,7 @@ func (a *verticalAxis) size() (w vg.Length) {
w -= a.Label.Font.Extents().Descent
w += a.Label.Height(a.Label.Text)
}
if marks := a.Tick.Marker.Ticks(a.Min, a.Max); len(marks) > 0 {
if marks := a.Tick.Marker.Ticks(a.Min, a.Max, a.Tick.Format); len(marks) > 0 {
if lwidth := tickLabelWidth(a.Tick.Label, marks); lwidth > 0 {
w += lwidth
w += a.Label.Width(" ")
Expand All @@ -302,7 +309,7 @@ func (a *verticalAxis) draw(c draw.Canvas) {
c.FillText(sty, vg.Point{X: x, Y: c.Center().Y}, a.Label.Text)
x += -a.Label.Font.Extents().Descent
}
marks := a.Tick.Marker.Ticks(a.Min, a.Max)
marks := a.Tick.Marker.Ticks(a.Min, a.Max, a.Tick.Format)
if w := tickLabelWidth(a.Tick.Label, marks); len(marks) > 0 && w > 0 {
x += w
}
Expand Down Expand Up @@ -335,7 +342,7 @@ func (a *verticalAxis) draw(c draw.Canvas) {

// GlyphBoxes returns the GlyphBoxes for the tick labels
func (a *verticalAxis) GlyphBoxes(*Plot) (boxes []GlyphBox) {
for _, t := range a.Tick.Marker.Ticks(a.Min, a.Max) {
for _, t := range a.Tick.Marker.Ticks(a.Min, a.Max, a.Tick.Format) {
if t.IsMinor() {
continue
}
Expand All @@ -355,7 +362,7 @@ type DefaultTicks struct{}
var _ Ticker = DefaultTicks{}

// Ticks returns Ticks in a specified range
func (DefaultTicks) Ticks(min, max float64) (ticks []Tick) {
func (DefaultTicks) Ticks(min, max float64, format func(v float64, prec int) string) (ticks []Tick) {
const SuggestedTicks = 3
if max < min {
panic("illegal range")
Expand All @@ -379,7 +386,7 @@ func (DefaultTicks) Ticks(min, max float64) (ticks []Tick) {
prec := precisionOf(majorDelta)
for val <= max {
if val >= min && val <= max {
ticks = append(ticks, Tick{Value: val, Label: formatFloatTick(val, prec)})
ticks = append(ticks, Tick{Value: val, Label: format(val, prec)})
}
if math.Nextafter(val, val+majorDelta) == val {
break
Expand Down Expand Up @@ -421,7 +428,7 @@ type LogTicks struct{}
var _ Ticker = LogTicks{}

// Ticks returns Ticks in a specified range
func (LogTicks) Ticks(min, max float64) []Tick {
func (LogTicks) Ticks(min, max float64, format func(v float64, prec int) string) []Tick {
var ticks []Tick
val := math.Pow10(int(math.Floor(math.Log10(min))))
if min <= 0 {
Expand All @@ -432,13 +439,13 @@ func (LogTicks) Ticks(min, max float64) []Tick {
for i := 1; i < 10; i++ {
tick := Tick{Value: val * float64(i)}
if i == 1 {
tick.Label = formatFloatTick(val*float64(i), prec)
tick.Label = format(val*float64(i), prec)
}
ticks = append(ticks, tick)
}
val *= 10
}
tick := Tick{Value: val, Label: formatFloatTick(val, prec)}
tick := Tick{Value: val, Label: format(val, prec)}
ticks = append(ticks, tick)
return ticks
}
Expand All @@ -450,7 +457,7 @@ type ConstantTicks []Tick
var _ Ticker = ConstantTicks{}

// Ticks returns Ticks in a specified range
func (ts ConstantTicks) Ticks(float64, float64) []Tick {
func (ts ConstantTicks) Ticks(float64, float64, func(v float64, prec int) string) []Tick {
return ts
}

Expand Down Expand Up @@ -482,7 +489,7 @@ type TimeTicks struct {
var _ Ticker = TimeTicks{}

// Ticks implements plot.Ticker.
func (t TimeTicks) Ticks(min, max float64) []Tick {
func (t TimeTicks) Ticks(min, max float64, format func(v float64, prec int) string) []Tick {
if t.Ticker == nil {
t.Ticker = DefaultTicks{}
}
Expand All @@ -493,7 +500,7 @@ func (t TimeTicks) Ticks(min, max float64) []Tick {
t.Time = UTCUnixTime
}

ticks := t.Ticker.Ticks(min, max)
ticks := t.Ticker.Ticks(min, max, format)
for i := range ticks {
tick := &ticks[i]
if tick.Label == "" {
Expand Down Expand Up @@ -570,9 +577,9 @@ func log(x float64) float64 {
return math.Log(x)
}

// formatFloatTick returns a g-formated string representation of v
// DefaultTickFormat returns a g-formated string representation of v
// to the specified precision.
func formatFloatTick(v float64, prec int) string {
func DefaultTickFormat(v float64, prec int) string {
return strconv.FormatFloat(floats.Round(v, prec), 'g', displayPrecision, 64)
}

Expand Down
47 changes: 31 additions & 16 deletions axis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,57 @@ package plot
import (
"math"
"reflect"
"strconv"
"testing"
)

func TestAxisSmallTick(t *testing.T) {
d := DefaultTicks{}
for _, test := range []struct {
min, max float64
format func(v float64, prec int) string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a test where format is nil.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where my lack of knowledge of the codebase comes in. Initially I added this check:

       if format == nil {
               format = formatFloatTick
       }

In DefaultTicks.Ticks and LogTicks.Ticks. However, @sbinet told me that the check wasn't required if I applied the DefaultTickFormat in makeAxis ( #362 (comment) ).
Right now, if you call DefaultTicks.Ticks directly without a format you get a panic in the test. I am assuming this will never happen but I don't have enough insight into it.

@sbinet and @kortschak please let me know what course to take on this one.

want []string
}{
{
min: -1.9846500878911073,
max: 0.4370974820125605,
want: []string{"-1.6", "-0.8", "0"},
min: -1.9846500878911073,
max: 0.4370974820125605,
format: DefaultTickFormat,
want: []string{"-1.6", "-0.8", "0"},
},
{
min: -1.985e-15,
max: 0.4371e-15,
want: []string{"-1.6e-15", "-8e-16", "0"},
min: -1.985e-15,
max: 0.4371e-15,
format: DefaultTickFormat,
want: []string{"-1.6e-15", "-8e-16", "0"},
},
{
min: -1.985e15,
max: 0.4371e15,
want: []string{"-1.6e+15", "-8e+14", "0"},
min: -1.985e15,
max: 0.4371e15,
format: DefaultTickFormat,
want: []string{"-1.6e+15", "-8e+14", "0"},
},
{
min: math.MaxFloat64 / 4,
max: math.MaxFloat64 / 3,
want: []string{"4.8e+307", "5.2e+307", "5.6e+307"},
min: math.MaxFloat64 / 4,
max: math.MaxFloat64 / 3,
format: DefaultTickFormat,
want: []string{"4.8e+307", "5.2e+307", "5.6e+307"},
},
{
min: 0.00010,
max: 0.00015,
want: []string{"0.0001", "0.00011", "0.00012", "0.00013", "0.00014"},
min: 0.00010,
max: 0.00015,
format: DefaultTickFormat,
want: []string{"0.0001", "0.00011", "0.00012", "0.00013", "0.00014"},
},
{
min: 0.0001,
max: 0.0005,
format: func(v float64, prec int) string {
return strconv.FormatFloat(v, 'e', 1, 64)
},
want: []string{"1.0e-04", "2.0e-04", "3.0e-04", "4.0e-04", "5.0e-04"},
},
} {
ticks := d.Ticks(test.min, test.max)
ticks := d.Ticks(test.min, test.max, test.format)
got := labelsOf(ticks)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("tick labels mismatch:\ngot: %q\nwant:%q", got, test.want)
Expand Down
4 changes: 2 additions & 2 deletions gob/gob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ func randomPoints(n int, rnd *rand.Rand) plotter.XYs {
// into the labels for the major tick marks.
type commaTicks struct{}

func (commaTicks) Ticks(min, max float64) []plot.Tick {
tks := plot.DefaultTicks{}.Ticks(min, max)
func (commaTicks) Ticks(min, max float64, format func(v float64, prec int) string) []plot.Tick {
tks := plot.DefaultTicks{}.Ticks(min, max, format)
for i, t := range tks {
if t.Label == "" { // Skip minor ticks, they are fine.
continue
Expand Down
4 changes: 2 additions & 2 deletions plotter/grid.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (g *Grid) Plot(c draw.Canvas, plt *plot.Plot) {
if g.Vertical.Color == nil {
goto horiz
}
for _, tk := range plt.X.Tick.Marker.Ticks(plt.X.Min, plt.X.Max) {
for _, tk := range plt.X.Tick.Marker.Ticks(plt.X.Min, plt.X.Max, plt.X.Tick.Format) {
if tk.IsMinor() {
continue
}
Expand All @@ -68,7 +68,7 @@ horiz:
if g.Horizontal.Color == nil {
return
}
for _, tk := range plt.Y.Tick.Marker.Ticks(plt.Y.Min, plt.Y.Max) {
for _, tk := range plt.Y.Tick.Marker.Ticks(plt.Y.Min, plt.Y.Max, plt.Y.Tick.Format) {
if tk.IsMinor() {
continue
}
Expand Down