From a63a4c6414625b47b8f2e8e254c32325c6e74d83 Mon Sep 17 00:00:00 2001 From: David Gamba Date: Wed, 24 May 2017 23:49:36 -0600 Subject: [PATCH 1/6] fixes-336: Extend the Ticker interface to allow passing a format function This allows for easy wrappers around the plot.DefaultTicks struct with custom format functions: type myCustomFormatTicks struct{} var _ plot.Ticker = myCustomFormatTicks{} func (myCustomFormatTicks) Ticks(min, max float64, format func(v float64, prec int) string) (ticks []plot.Tick) { return plot.DefaultTicks{}.Ticks(min, max, myFormatFloatTick) } func myFormatFloatTick(v float64, prec int) string { return strconv.FormatFloat(floats.Round(v, prec), 'g', 8, 64) } p.Y.Tick.Marker = myCustomFormatTicks{} --- axis.go | 36 +++++++++++++++++++++--------------- axis_test.go | 2 +- gob/gob_test.go | 4 ++-- plotter/grid.go | 4 ++-- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/axis.go b/axis.go index d1f13743..be68b4b8 100644 --- a/axis.go +++ b/axis.go @@ -21,7 +21,7 @@ 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(min, max float64, format func(v float64, prec int) string) []Tick } // Normalizer rescales values from the data coordinate system to the @@ -200,7 +200,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, nil); len(marks) > 0 { if a.drawTicks() { h += a.Tick.Length } @@ -220,7 +220,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, nil) ticklabelheight := tickLabelHeight(a.Tick.Label, marks) for _, t := range marks { x := c.X(a.Norm(t.Value)) @@ -254,7 +254,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, nil) { if t.IsMinor() { continue } @@ -278,7 +278,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, nil); len(marks) > 0 { if lwidth := tickLabelWidth(a.Tick.Label, marks); lwidth > 0 { w += lwidth w += a.Label.Width(" ") @@ -302,7 +302,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, nil) if w := tickLabelWidth(a.Tick.Label, marks); len(marks) > 0 && w > 0 { x += w } @@ -335,7 +335,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, nil) { if t.IsMinor() { continue } @@ -355,11 +355,14 @@ 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") } + if format == nil { + format = formatFloatTick + } tens := math.Pow10(int(math.Floor(math.Log10(max - min)))) n := (max - min) / tens for n < SuggestedTicks { @@ -379,7 +382,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 @@ -421,24 +424,27 @@ 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 { panic("Values must be greater than 0 for a log scale.") } + if format == nil { + format = formatFloatTick + } prec := precisionOf(max) for val < max*10 { 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 } @@ -450,7 +456,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 } @@ -482,7 +488,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{} } @@ -493,7 +499,7 @@ func (t TimeTicks) Ticks(min, max float64) []Tick { t.Time = UTCUnixTime } - ticks := t.Ticker.Ticks(min, max) + ticks := t.Ticker.Ticks(min, max, nil) for i := range ticks { tick := &ticks[i] if tick.Label == "" { diff --git a/axis_test.go b/axis_test.go index 32f49b2f..1690af2f 100644 --- a/axis_test.go +++ b/axis_test.go @@ -42,7 +42,7 @@ func TestAxisSmallTick(t *testing.T) { want: []string{"0.0001", "0.00011", "0.00012", "0.00013", "0.00014"}, }, } { - ticks := d.Ticks(test.min, test.max) + ticks := d.Ticks(test.min, test.max, nil) got := labelsOf(ticks) if !reflect.DeepEqual(got, test.want) { t.Errorf("tick labels mismatch:\ngot: %q\nwant:%q", got, test.want) diff --git a/gob/gob_test.go b/gob/gob_test.go index c1b7239f..a5155be5 100644 --- a/gob/gob_test.go +++ b/gob/gob_test.go @@ -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, nil) for i, t := range tks { if t.Label == "" { // Skip minor ticks, they are fine. continue diff --git a/plotter/grid.go b/plotter/grid.go index 55040686..ef7f32e9 100644 --- a/plotter/grid.go +++ b/plotter/grid.go @@ -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, nil) { if tk.IsMinor() { continue } @@ -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, nil) { if tk.IsMinor() { continue } From 7e9584957de88403b9d61f6db74213d5b0ceb6e4 Mon Sep 17 00:00:00 2001 From: David Gamba Date: Sat, 17 Jun 2017 20:53:51 -0600 Subject: [PATCH 2/6] fixes-336: Extend the Ticker interface to allow passing a format function * Document the extra argument in the Ticker interface. * Weave the format arg through the call to DefaultTicks when applicable. * Add an extra test with the additional argument. --- axis.go | 6 ++++-- axis_test.go | 12 +++++++++++- gob/gob_test.go | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/axis.go b/axis.go index be68b4b8..00a70f9b 100644 --- a/axis.go +++ b/axis.go @@ -20,7 +20,9 @@ const displayPrecision = 4 // Ticker creates Ticks in a specified range type Ticker interface { - // Ticks returns Ticks in a specified range + // 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. Ticks(min, max float64, format func(v float64, prec int) string) []Tick } @@ -499,7 +501,7 @@ func (t TimeTicks) Ticks(min, max float64, format func(v float64, prec int) stri t.Time = UTCUnixTime } - ticks := t.Ticker.Ticks(min, max, nil) + ticks := t.Ticker.Ticks(min, max, format) for i := range ticks { tick := &ticks[i] if tick.Label == "" { diff --git a/axis_test.go b/axis_test.go index 1690af2f..38e9cc26 100644 --- a/axis_test.go +++ b/axis_test.go @@ -7,6 +7,7 @@ package plot import ( "math" "reflect" + "strconv" "testing" ) @@ -14,6 +15,7 @@ func TestAxisSmallTick(t *testing.T) { d := DefaultTicks{} for _, test := range []struct { min, max float64 + format func(v float64, prec int) string want []string }{ { @@ -41,8 +43,16 @@ func TestAxisSmallTick(t *testing.T) { max: 0.00015, 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, nil) + 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) diff --git a/gob/gob_test.go b/gob/gob_test.go index a5155be5..801843e9 100644 --- a/gob/gob_test.go +++ b/gob/gob_test.go @@ -130,7 +130,7 @@ func randomPoints(n int, rnd *rand.Rand) plotter.XYs { type commaTicks struct{} func (commaTicks) Ticks(min, max float64, format func(v float64, prec int) string) []plot.Tick { - tks := plot.DefaultTicks{}.Ticks(min, max, nil) + tks := plot.DefaultTicks{}.Ticks(min, max, format) for i, t := range tks { if t.Label == "" { // Skip minor ticks, they are fine. continue From 068ee8d4204d60c9bd75b78a6291257f3af96a10 Mon Sep 17 00:00:00 2001 From: David Gamba Date: Mon, 4 Sep 2017 21:40:07 -0600 Subject: [PATCH 3/6] fixes-336: Extend the Axis struct to have a Tick.Format field Allows to define a custom Tick format function. --- axis.go | 16 ++++++++++------ plotter/grid.go | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/axis.go b/axis.go index 00a70f9b..f5683f11 100644 --- a/axis.go +++ b/axis.go @@ -76,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. + Format func(v float64, prec int) string } // Scale transforms a value given in the data coordinate system @@ -202,7 +206,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, nil); 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 } @@ -222,7 +226,7 @@ func (a *horizontalAxis) draw(c draw.Canvas) { y += a.Label.Height(a.Label.Text) } - marks := a.Tick.Marker.Ticks(a.Min, a.Max, nil) + 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)) @@ -256,7 +260,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, nil) { + for _, t := range a.Tick.Marker.Ticks(a.Min, a.Max, a.Tick.Format) { if t.IsMinor() { continue } @@ -280,7 +284,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, nil); 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(" ") @@ -304,7 +308,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, nil) + 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 } @@ -337,7 +341,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, nil) { + for _, t := range a.Tick.Marker.Ticks(a.Min, a.Max, a.Tick.Format) { if t.IsMinor() { continue } diff --git a/plotter/grid.go b/plotter/grid.go index ef7f32e9..7573d614 100644 --- a/plotter/grid.go +++ b/plotter/grid.go @@ -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, nil) { + for _, tk := range plt.X.Tick.Marker.Ticks(plt.X.Min, plt.X.Max, plt.X.Tick.Format) { if tk.IsMinor() { continue } @@ -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, nil) { + for _, tk := range plt.Y.Tick.Marker.Ticks(plt.Y.Min, plt.Y.Max, plt.X.Tick.Format) { if tk.IsMinor() { continue } From 92493453f4df4381e3096718fc1296b38f671e6e Mon Sep 17 00:00:00 2001 From: David Gamba Date: Mon, 4 Sep 2017 22:11:04 -0600 Subject: [PATCH 4/6] fixes-336: Fix typo --- plotter/grid.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotter/grid.go b/plotter/grid.go index 7573d614..8721d152 100644 --- a/plotter/grid.go +++ b/plotter/grid.go @@ -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, plt.X.Tick.Format) { + for _, tk := range plt.Y.Tick.Marker.Ticks(plt.Y.Min, plt.Y.Max, plt.Y.Tick.Format) { if tk.IsMinor() { continue } From 6430badb16084a09f8c45d58c11d74942b24c966 Mon Sep 17 00:00:00 2001 From: David Gamba Date: Fri, 8 Sep 2017 22:15:32 -0600 Subject: [PATCH 5/6] fixes-336: Expose DefaultTickFormat function And set it by default on axis.makeAxis --- axis.go | 11 +++-------- axis_test.go | 35 ++++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/axis.go b/axis.go index f5683f11..06054c63 100644 --- a/axis.go +++ b/axis.go @@ -135,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 } @@ -366,9 +367,6 @@ func (DefaultTicks) Ticks(min, max float64, format func(v float64, prec int) str if max < min { panic("illegal range") } - if format == nil { - format = formatFloatTick - } tens := math.Pow10(int(math.Floor(math.Log10(max - min)))) n := (max - min) / tens for n < SuggestedTicks { @@ -436,9 +434,6 @@ func (LogTicks) Ticks(min, max float64, format func(v float64, prec int) string) if min <= 0 { panic("Values must be greater than 0 for a log scale.") } - if format == nil { - format = formatFloatTick - } prec := precisionOf(max) for val < max*10 { for i := 1; i < 10; i++ { @@ -582,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) } diff --git a/axis_test.go b/axis_test.go index 38e9cc26..7e7a81a7 100644 --- a/axis_test.go +++ b/axis_test.go @@ -19,29 +19,34 @@ func TestAxisSmallTick(t *testing.T) { 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, From f4e76739f2540885eb3ebd2e340d954d3cf0c404 Mon Sep 17 00:00:00 2001 From: David Gamba Date: Fri, 15 Sep 2017 22:51:32 -0600 Subject: [PATCH 6/6] fixes-336: Update axis in-code documentation --- axis.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/axis.go b/axis.go index 06054c63..4264e35b 100644 --- a/axis.go +++ b/axis.go @@ -15,14 +15,14 @@ import ( "github.com/gonum/plot/vg/draw" ) -// displayPrecision is a sane level of float precision for a plot. +// displayPrecision default level of float precision for a plot. const displayPrecision = 4 // Ticker creates Ticks in a specified range type Ticker interface { // 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. + // When format is nil DefaultTickFormat is used. Ticks(min, max float64, format func(v float64, prec int) string) []Tick } @@ -78,7 +78,7 @@ type Axis struct { Marker Ticker // Format function used to format the Axis Ticks. - // When no format is provided a sane default is used. + // When format is nil DefaultTickFormat is used. Format func(v float64, prec int) string }