diff --git a/axis.go b/axis.go index d1f13743..4264e35b 100644 --- a/axis.go +++ b/axis.go @@ -15,13 +15,15 @@ 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 - Ticks(min, max float64) []Tick + // Ticks returns Ticks in a specified range and formatted according to the + // given format function. + // When format is nil DefaultTickFormat is used. + Ticks(min, max float64, format func(v float64, prec int) string) []Tick } // Normalizer rescales values from the data coordinate system to the @@ -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 format is nil DefaultTickFormat is used. + Format func(v float64, prec int) string } // Scale transforms a value given in the data coordinate system @@ -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 } @@ -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 } @@ -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)) @@ -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 } @@ -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(" ") @@ -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 } @@ -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 } @@ -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") @@ -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 @@ -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 { @@ -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 } @@ -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 } @@ -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{} } @@ -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 == "" { @@ -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) } diff --git a/axis_test.go b/axis_test.go index 32f49b2f..7e7a81a7 100644 --- a/axis_test.go +++ b/axis_test.go @@ -7,6 +7,7 @@ package plot import ( "math" "reflect" + "strconv" "testing" ) @@ -14,35 +15,49 @@ func TestAxisSmallTick(t *testing.T) { d := DefaultTicks{} for _, test := range []struct { min, max float64 + format func(v float64, prec int) string 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) diff --git a/gob/gob_test.go b/gob/gob_test.go index c1b7239f..801843e9 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, format) 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..8721d152 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, 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) { + for _, tk := range plt.Y.Tick.Marker.Ticks(plt.Y.Min, plt.Y.Max, plt.Y.Tick.Format) { if tk.IsMinor() { continue }