...

Source file src/gonum.org/v1/plot/axis.go

Documentation: gonum.org/v1/plot

     1  // Copyright ©2015 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package plot
     6  
     7  import (
     8  	"image/color"
     9  	"math"
    10  	"strconv"
    11  	"time"
    12  
    13  	"gonum.org/v1/plot/font"
    14  	"gonum.org/v1/plot/text"
    15  	"gonum.org/v1/plot/vg"
    16  	"gonum.org/v1/plot/vg/draw"
    17  )
    18  
    19  // Ticker creates Ticks in a specified range
    20  type Ticker interface {
    21  	// Ticks returns Ticks in a specified range
    22  	Ticks(min, max float64) []Tick
    23  }
    24  
    25  // Normalizer rescales values from the data coordinate system to the
    26  // normalized coordinate system.
    27  type Normalizer interface {
    28  	// Normalize transforms a value x in the data coordinate system to
    29  	// the normalized coordinate system.
    30  	Normalize(min, max, x float64) float64
    31  }
    32  
    33  // An Axis represents either a horizontal or vertical
    34  // axis of a plot.
    35  type Axis struct {
    36  	// Min and Max are the minimum and maximum data
    37  	// values represented by the axis.
    38  	Min, Max float64
    39  
    40  	Label struct {
    41  		// Text is the axis label string.
    42  		Text string
    43  
    44  		// Padding is the distance between the label and the axis.
    45  		Padding vg.Length
    46  
    47  		// TextStyle is the style of the axis label text.
    48  		// For the vertical axis, one quarter turn
    49  		// counterclockwise will be added to the label
    50  		// text before drawing.
    51  		TextStyle text.Style
    52  
    53  		// Position is where the axis label string should be drawn.
    54  		// The default value is draw.PosCenter, displaying the label
    55  		// at the center of the axis.
    56  		// Valid values are [-1,+1], with +1 being the far right/top
    57  		// of the axis, and -1 the far left/bottom of the axis.
    58  		Position float64
    59  	}
    60  
    61  	// LineStyle is the style of the axis line.
    62  	draw.LineStyle
    63  
    64  	// Padding between the axis line and the data.  Having
    65  	// non-zero padding ensures that the data is never drawn
    66  	// on the axis, thus making it easier to see.
    67  	Padding vg.Length
    68  
    69  	Tick struct {
    70  		// Label is the TextStyle on the tick labels.
    71  		Label text.Style
    72  
    73  		// LineStyle is the LineStyle of the tick lines.
    74  		draw.LineStyle
    75  
    76  		// Length is the length of a major tick mark.
    77  		// Minor tick marks are half of the length of major
    78  		// tick marks.
    79  		Length vg.Length
    80  
    81  		// Marker returns the tick marks.  Any tick marks
    82  		// returned by the Marker function that are not in
    83  		// range of the axis are not drawn.
    84  		Marker Ticker
    85  	}
    86  
    87  	// Scale transforms a value given in the data coordinate system
    88  	// to the normalized coordinate system of the axis—its distance
    89  	// along the axis as a fraction of the axis range.
    90  	Scale Normalizer
    91  
    92  	// AutoRescale enables an axis to automatically adapt its minimum
    93  	// and maximum boundaries, according to its underlying Ticker.
    94  	AutoRescale bool
    95  }
    96  
    97  // makeAxis returns a default Axis.
    98  //
    99  // The default range is (∞, ­∞), and thus any finite
   100  // value is less than Min and greater than Max.
   101  func makeAxis(o orientation) Axis {
   102  
   103  	a := Axis{
   104  		Min: math.Inf(+1),
   105  		Max: math.Inf(-1),
   106  		LineStyle: draw.LineStyle{
   107  			Color: color.Black,
   108  			Width: vg.Points(0.5),
   109  		},
   110  		Padding: vg.Points(5),
   111  		Scale:   LinearScale{},
   112  	}
   113  	a.Label.TextStyle = text.Style{
   114  		Color:   color.Black,
   115  		Font:    font.From(DefaultFont, 12),
   116  		XAlign:  draw.XCenter,
   117  		YAlign:  draw.YBottom,
   118  		Handler: DefaultTextHandler,
   119  	}
   120  	a.Label.Position = draw.PosCenter
   121  
   122  	var (
   123  		xalign draw.XAlignment
   124  		yalign draw.YAlignment
   125  	)
   126  	switch o {
   127  	case vertical:
   128  		xalign = draw.XRight
   129  		yalign = draw.YCenter
   130  	case horizontal:
   131  		xalign = draw.XCenter
   132  		yalign = draw.YTop
   133  	}
   134  
   135  	a.Tick.Label = text.Style{
   136  		Color:   color.Black,
   137  		Font:    font.From(DefaultFont, 10),
   138  		XAlign:  xalign,
   139  		YAlign:  yalign,
   140  		Handler: DefaultTextHandler,
   141  	}
   142  	a.Tick.LineStyle = draw.LineStyle{
   143  		Color: color.Black,
   144  		Width: vg.Points(0.5),
   145  	}
   146  	a.Tick.Length = vg.Points(8)
   147  	a.Tick.Marker = DefaultTicks{}
   148  
   149  	return a
   150  }
   151  
   152  // sanitizeRange ensures that the range of the
   153  // axis makes sense.
   154  func (a *Axis) sanitizeRange() {
   155  	if math.IsInf(a.Min, 0) {
   156  		a.Min = 0
   157  	}
   158  	if math.IsInf(a.Max, 0) {
   159  		a.Max = 0
   160  	}
   161  	if a.Min > a.Max {
   162  		a.Min, a.Max = a.Max, a.Min
   163  	}
   164  	if a.Min == a.Max {
   165  		a.Min--
   166  		a.Max++
   167  	}
   168  
   169  	if a.AutoRescale {
   170  		marks := a.Tick.Marker.Ticks(a.Min, a.Max)
   171  		for _, t := range marks {
   172  			a.Min = math.Min(a.Min, t.Value)
   173  			a.Max = math.Max(a.Max, t.Value)
   174  		}
   175  	}
   176  }
   177  
   178  // LinearScale an be used as the value of an Axis.Scale function to
   179  // set the axis to a standard linear scale.
   180  type LinearScale struct{}
   181  
   182  var _ Normalizer = LinearScale{}
   183  
   184  // Normalize returns the fractional distance of x between min and max.
   185  func (LinearScale) Normalize(min, max, x float64) float64 {
   186  	return (x - min) / (max - min)
   187  }
   188  
   189  // LogScale can be used as the value of an Axis.Scale function to
   190  // set the axis to a log scale.
   191  type LogScale struct{}
   192  
   193  var _ Normalizer = LogScale{}
   194  
   195  // Normalize returns the fractional logarithmic distance of
   196  // x between min and max.
   197  func (LogScale) Normalize(min, max, x float64) float64 {
   198  	if min <= 0 || max <= 0 || x <= 0 {
   199  		panic("Values must be greater than 0 for a log scale.")
   200  	}
   201  	logMin := math.Log(min)
   202  	return (math.Log(x) - logMin) / (math.Log(max) - logMin)
   203  }
   204  
   205  // InvertedScale can be used as the value of an Axis.Scale function to
   206  // invert the axis using any Normalizer.
   207  type InvertedScale struct{ Normalizer }
   208  
   209  var _ Normalizer = InvertedScale{}
   210  
   211  // Normalize returns a normalized [0, 1] value for the position of x.
   212  func (is InvertedScale) Normalize(min, max, x float64) float64 {
   213  	return is.Normalizer.Normalize(max, min, x)
   214  }
   215  
   216  // Norm returns the value of x, given in the data coordinate
   217  // system, normalized to its distance as a fraction of the
   218  // range of this axis.  For example, if x is a.Min then the return
   219  // value is 0, and if x is a.Max then the return value is 1.
   220  func (a Axis) Norm(x float64) float64 {
   221  	return a.Scale.Normalize(a.Min, a.Max, x)
   222  }
   223  
   224  // drawTicks returns true if the tick marks should be drawn.
   225  func (a Axis) drawTicks() bool {
   226  	return a.Tick.Width > 0 && a.Tick.Length > 0
   227  }
   228  
   229  // A horizontalAxis draws horizontally across the bottom
   230  // of a plot.
   231  type horizontalAxis struct {
   232  	Axis
   233  }
   234  
   235  // size returns the height of the axis.
   236  func (a horizontalAxis) size() (h vg.Length) {
   237  	if a.Label.Text != "" { // We assume that the label isn't rotated.
   238  		h += a.Label.TextStyle.FontExtents().Descent
   239  		h += a.Label.TextStyle.Height(a.Label.Text)
   240  		h += a.Label.Padding
   241  	}
   242  
   243  	marks := a.Tick.Marker.Ticks(a.Min, a.Max)
   244  	if len(marks) > 0 {
   245  		if a.drawTicks() {
   246  			h += a.Tick.Length
   247  		}
   248  		h += tickLabelHeight(a.Tick.Label, marks)
   249  	}
   250  	h += a.Width / 2
   251  	h += a.Padding
   252  
   253  	return h
   254  }
   255  
   256  // draw draws the axis along the lower edge of a draw.Canvas.
   257  func (a horizontalAxis) draw(c draw.Canvas) {
   258  	var (
   259  		x vg.Length
   260  		y = c.Min.Y
   261  	)
   262  	switch a.Label.Position {
   263  	case draw.PosCenter:
   264  		x = c.Center().X
   265  	case draw.PosRight:
   266  		x = c.Max.X
   267  		x -= a.Label.TextStyle.Width(a.Label.Text) / 2
   268  	}
   269  	if a.Label.Text != "" {
   270  		descent := a.Label.TextStyle.FontExtents().Descent
   271  		c.FillText(a.Label.TextStyle, vg.Point{X: x, Y: y + descent}, a.Label.Text)
   272  		y += a.Label.TextStyle.Height(a.Label.Text)
   273  		y += a.Label.Padding
   274  	}
   275  
   276  	marks := a.Tick.Marker.Ticks(a.Min, a.Max)
   277  	ticklabelheight := tickLabelHeight(a.Tick.Label, marks)
   278  	descent := a.Tick.Label.FontExtents().Descent
   279  	for _, t := range marks {
   280  		x := c.X(a.Norm(t.Value))
   281  		if !c.ContainsX(x) || t.IsMinor() {
   282  			continue
   283  		}
   284  		c.FillText(a.Tick.Label, vg.Point{X: x, Y: y + ticklabelheight + descent}, t.Label)
   285  	}
   286  
   287  	if len(marks) > 0 {
   288  		y += ticklabelheight
   289  	} else {
   290  		y += a.Width / 2
   291  	}
   292  
   293  	if len(marks) > 0 && a.drawTicks() {
   294  		len := a.Tick.Length
   295  		for _, t := range marks {
   296  			x := c.X(a.Norm(t.Value))
   297  			if !c.ContainsX(x) {
   298  				continue
   299  			}
   300  			start := t.lengthOffset(len)
   301  			c.StrokeLine2(a.Tick.LineStyle, x, y+start, x, y+len)
   302  		}
   303  		y += len
   304  	}
   305  
   306  	c.StrokeLine2(a.LineStyle, c.Min.X, y, c.Max.X, y)
   307  }
   308  
   309  // GlyphBoxes returns the GlyphBoxes for the tick labels.
   310  func (a horizontalAxis) GlyphBoxes(p *Plot) []GlyphBox {
   311  	var (
   312  		boxes []GlyphBox
   313  		yoff  font.Length
   314  	)
   315  
   316  	if a.Label.Text != "" {
   317  		x := a.Norm(p.X.Max)
   318  		switch a.Label.Position {
   319  		case draw.PosCenter:
   320  			x = a.Norm(0.5 * (p.X.Max + p.X.Min))
   321  		case draw.PosRight:
   322  			x -= a.Norm(0.5 * a.Label.TextStyle.Width(a.Label.Text).Points()) // FIXME(sbinet): want data coordinates
   323  		}
   324  		descent := a.Label.TextStyle.FontExtents().Descent
   325  		boxes = append(boxes, GlyphBox{
   326  			X:         x,
   327  			Rectangle: a.Label.TextStyle.Rectangle(a.Label.Text).Add(vg.Point{Y: yoff + descent}),
   328  		})
   329  		yoff += a.Label.TextStyle.Height(a.Label.Text)
   330  		yoff += a.Label.Padding
   331  	}
   332  
   333  	var (
   334  		marks   = a.Tick.Marker.Ticks(a.Min, a.Max)
   335  		height  = tickLabelHeight(a.Tick.Label, marks)
   336  		descent = a.Tick.Label.FontExtents().Descent
   337  	)
   338  	for _, t := range marks {
   339  		if t.IsMinor() {
   340  			continue
   341  		}
   342  		box := GlyphBox{
   343  			X:         a.Norm(t.Value),
   344  			Rectangle: a.Tick.Label.Rectangle(t.Label).Add(vg.Point{Y: yoff + height + descent}),
   345  		}
   346  		boxes = append(boxes, box)
   347  	}
   348  	return boxes
   349  }
   350  
   351  // A verticalAxis is drawn vertically up the left side of a plot.
   352  type verticalAxis struct {
   353  	Axis
   354  }
   355  
   356  // size returns the width of the axis.
   357  func (a verticalAxis) size() (w vg.Length) {
   358  	if a.Label.Text != "" { // We assume that the label isn't rotated.
   359  		w += a.Label.TextStyle.FontExtents().Descent
   360  		w += a.Label.TextStyle.Height(a.Label.Text)
   361  		w += a.Label.Padding
   362  	}
   363  
   364  	marks := a.Tick.Marker.Ticks(a.Min, a.Max)
   365  	if len(marks) > 0 {
   366  		if lwidth := tickLabelWidth(a.Tick.Label, marks); lwidth > 0 {
   367  			w += lwidth
   368  			w += a.Label.TextStyle.Width(" ")
   369  		}
   370  		if a.drawTicks() {
   371  			w += a.Tick.Length
   372  		}
   373  	}
   374  	w += a.Width / 2
   375  	w += a.Padding
   376  
   377  	return w
   378  }
   379  
   380  // draw draws the axis along the left side of a draw.Canvas.
   381  func (a verticalAxis) draw(c draw.Canvas) {
   382  	var (
   383  		x = c.Min.X
   384  		y vg.Length
   385  	)
   386  	if a.Label.Text != "" {
   387  		sty := a.Label.TextStyle
   388  		sty.Rotation += math.Pi / 2
   389  		x += a.Label.TextStyle.Height(a.Label.Text)
   390  		switch a.Label.Position {
   391  		case draw.PosCenter:
   392  			y = c.Center().Y
   393  		case draw.PosTop:
   394  			y = c.Max.Y
   395  			y -= a.Label.TextStyle.Width(a.Label.Text) / 2
   396  		}
   397  		descent := a.Label.TextStyle.FontExtents().Descent
   398  		c.FillText(sty, vg.Point{X: x - descent, Y: y}, a.Label.Text)
   399  		x += descent
   400  		x += a.Label.Padding
   401  	}
   402  	marks := a.Tick.Marker.Ticks(a.Min, a.Max)
   403  	if w := tickLabelWidth(a.Tick.Label, marks); len(marks) > 0 && w > 0 {
   404  		x += w
   405  	}
   406  
   407  	major := false
   408  	descent := a.Tick.Label.FontExtents().Descent
   409  	for _, t := range marks {
   410  		y := c.Y(a.Norm(t.Value))
   411  		if !c.ContainsY(y) || t.IsMinor() {
   412  			continue
   413  		}
   414  		c.FillText(a.Tick.Label, vg.Point{X: x, Y: y + descent}, t.Label)
   415  		major = true
   416  	}
   417  	if major {
   418  		x += a.Tick.Label.Width(" ")
   419  	}
   420  	if a.drawTicks() && len(marks) > 0 {
   421  		len := a.Tick.Length
   422  		for _, t := range marks {
   423  			y := c.Y(a.Norm(t.Value))
   424  			if !c.ContainsY(y) {
   425  				continue
   426  			}
   427  			start := t.lengthOffset(len)
   428  			c.StrokeLine2(a.Tick.LineStyle, x+start, y, x+len, y)
   429  		}
   430  		x += len
   431  	}
   432  
   433  	c.StrokeLine2(a.LineStyle, x, c.Min.Y, x, c.Max.Y)
   434  }
   435  
   436  // GlyphBoxes returns the GlyphBoxes for the tick labels
   437  func (a verticalAxis) GlyphBoxes(p *Plot) []GlyphBox {
   438  	var (
   439  		boxes []GlyphBox
   440  		xoff  font.Length
   441  	)
   442  
   443  	if a.Label.Text != "" {
   444  		yoff := a.Norm(p.Y.Max)
   445  		switch a.Label.Position {
   446  		case draw.PosCenter:
   447  			yoff = a.Norm(0.5 * (p.Y.Max + p.Y.Min))
   448  		case draw.PosTop:
   449  			yoff -= a.Norm(0.5 * a.Label.TextStyle.Width(a.Label.Text).Points()) // FIXME(sbinet): want data coordinates
   450  		}
   451  
   452  		sty := a.Label.TextStyle
   453  		sty.Rotation += math.Pi / 2
   454  
   455  		xoff += a.Label.TextStyle.Height(a.Label.Text)
   456  		descent := a.Label.TextStyle.FontExtents().Descent
   457  		boxes = append(boxes, GlyphBox{
   458  			Y:         yoff,
   459  			Rectangle: sty.Rectangle(a.Label.Text).Add(vg.Point{X: xoff - descent}),
   460  		})
   461  		xoff += descent
   462  		xoff += a.Label.Padding
   463  	}
   464  
   465  	marks := a.Tick.Marker.Ticks(a.Min, a.Max)
   466  	if w := tickLabelWidth(a.Tick.Label, marks); len(marks) != 0 && w > 0 {
   467  		xoff += w
   468  	}
   469  
   470  	var (
   471  		ext  = a.Tick.Label.FontExtents()
   472  		desc = ext.Height - ext.Ascent // descent + linegap
   473  	)
   474  	for _, t := range marks {
   475  		if t.IsMinor() {
   476  			continue
   477  		}
   478  		box := GlyphBox{
   479  			Y:         a.Norm(t.Value),
   480  			Rectangle: a.Tick.Label.Rectangle(t.Label).Add(vg.Point{X: xoff, Y: desc}),
   481  		}
   482  		boxes = append(boxes, box)
   483  	}
   484  	return boxes
   485  }
   486  
   487  // DefaultTicks is suitable for the Tick.Marker field of an Axis,
   488  // it returns a reasonable default set of tick marks.
   489  type DefaultTicks struct{}
   490  
   491  var _ Ticker = DefaultTicks{}
   492  
   493  // Ticks returns Ticks in the specified range.
   494  func (DefaultTicks) Ticks(min, max float64) []Tick {
   495  	if max <= min {
   496  		panic("illegal range")
   497  	}
   498  
   499  	const suggestedTicks = 3
   500  
   501  	labels, step, q, mag := talbotLinHanrahan(min, max, suggestedTicks, withinData, nil, nil, nil)
   502  	majorDelta := step * math.Pow10(mag)
   503  	if q == 0 {
   504  		// Simple fall back was chosen, so
   505  		// majorDelta is the label distance.
   506  		majorDelta = labels[1] - labels[0]
   507  	}
   508  
   509  	// Choose a reasonable, but ad
   510  	// hoc formatting for labels.
   511  	fc := byte('f')
   512  	var off int
   513  	if mag < -1 || 6 < mag {
   514  		off = 1
   515  		fc = 'g'
   516  	}
   517  	if math.Trunc(q) != q {
   518  		off += 2
   519  	}
   520  	prec := minInt(6, maxInt(off, -mag))
   521  	ticks := make([]Tick, len(labels))
   522  	for i, v := range labels {
   523  		ticks[i] = Tick{Value: v, Label: strconv.FormatFloat(v, fc, prec, 64)}
   524  	}
   525  
   526  	var minorDelta float64
   527  	// See talbotLinHanrahan for the values used here.
   528  	switch step {
   529  	case 1, 2.5:
   530  		minorDelta = majorDelta / 5
   531  	case 2, 3, 4, 5:
   532  		minorDelta = majorDelta / step
   533  	default:
   534  		if majorDelta/2 < dlamchP {
   535  			return ticks
   536  		}
   537  		minorDelta = majorDelta / 2
   538  	}
   539  
   540  	// Find the first minor tick not greater
   541  	// than the lowest data value.
   542  	var i float64
   543  	for labels[0]+(i-1)*minorDelta > min {
   544  		i--
   545  	}
   546  	// Add ticks at minorDelta intervals when
   547  	// they are not within minorDelta/2 of a
   548  	// labelled tick.
   549  	for {
   550  		val := labels[0] + i*minorDelta
   551  		if val > max {
   552  			break
   553  		}
   554  		found := false
   555  		for _, t := range ticks {
   556  			if math.Abs(t.Value-val) < minorDelta/2 {
   557  				found = true
   558  			}
   559  		}
   560  		if !found {
   561  			ticks = append(ticks, Tick{Value: val})
   562  		}
   563  		i++
   564  	}
   565  
   566  	return ticks
   567  }
   568  
   569  func minInt(a, b int) int {
   570  	if a < b {
   571  		return a
   572  	}
   573  	return b
   574  }
   575  
   576  func maxInt(a, b int) int {
   577  	if a > b {
   578  		return a
   579  	}
   580  	return b
   581  }
   582  
   583  // LogTicks is suitable for the Tick.Marker field of an Axis,
   584  // it returns tick marks suitable for a log-scale axis.
   585  type LogTicks struct {
   586  	// Prec specifies the precision of tick rendering
   587  	// according to the documentation for strconv.FormatFloat.
   588  	Prec int
   589  }
   590  
   591  var _ Ticker = LogTicks{}
   592  
   593  // Ticks returns Ticks in a specified range
   594  func (t LogTicks) Ticks(min, max float64) []Tick {
   595  	if min <= 0 || max <= 0 {
   596  		panic("Values must be greater than 0 for a log scale.")
   597  	}
   598  
   599  	val := math.Pow10(int(math.Log10(min)))
   600  	max = math.Pow10(int(math.Ceil(math.Log10(max))))
   601  	var ticks []Tick
   602  	for val < max {
   603  		for i := 1; i < 10; i++ {
   604  			if i == 1 {
   605  				ticks = append(ticks, Tick{Value: val, Label: formatFloatTick(val, t.Prec)})
   606  			}
   607  			ticks = append(ticks, Tick{Value: val * float64(i)})
   608  		}
   609  		val *= 10
   610  	}
   611  	ticks = append(ticks, Tick{Value: val, Label: formatFloatTick(val, t.Prec)})
   612  
   613  	return ticks
   614  }
   615  
   616  // ConstantTicks is suitable for the Tick.Marker field of an Axis.
   617  // This function returns the given set of ticks.
   618  type ConstantTicks []Tick
   619  
   620  var _ Ticker = ConstantTicks{}
   621  
   622  // Ticks returns Ticks in a specified range
   623  func (ts ConstantTicks) Ticks(float64, float64) []Tick {
   624  	return ts
   625  }
   626  
   627  // UnixTimeIn returns a time conversion function for the given location.
   628  func UnixTimeIn(loc *time.Location) func(t float64) time.Time {
   629  	return func(t float64) time.Time {
   630  		return time.Unix(int64(t), 0).In(loc)
   631  	}
   632  }
   633  
   634  // UTCUnixTime is the default time conversion for TimeTicks.
   635  var UTCUnixTime = UnixTimeIn(time.UTC)
   636  
   637  // TimeTicks is suitable for axes representing time values.
   638  type TimeTicks struct {
   639  	// Ticker is used to generate a set of ticks.
   640  	// If nil, DefaultTicks will be used.
   641  	Ticker Ticker
   642  
   643  	// Format is the textual representation of the time value.
   644  	// If empty, time.RFC3339 will be used
   645  	Format string
   646  
   647  	// Time takes a float64 value and converts it into a time.Time.
   648  	// If nil, UTCUnixTime is used.
   649  	Time func(t float64) time.Time
   650  }
   651  
   652  var _ Ticker = TimeTicks{}
   653  
   654  // Ticks implements plot.Ticker.
   655  func (t TimeTicks) Ticks(min, max float64) []Tick {
   656  	if t.Ticker == nil {
   657  		t.Ticker = DefaultTicks{}
   658  	}
   659  	if t.Format == "" {
   660  		t.Format = time.RFC3339
   661  	}
   662  	if t.Time == nil {
   663  		t.Time = UTCUnixTime
   664  	}
   665  
   666  	ticks := t.Ticker.Ticks(min, max)
   667  	for i := range ticks {
   668  		tick := &ticks[i]
   669  		if tick.Label == "" {
   670  			continue
   671  		}
   672  		tick.Label = t.Time(tick.Value).Format(t.Format)
   673  	}
   674  	return ticks
   675  }
   676  
   677  // A Tick is a single tick mark on an axis.
   678  type Tick struct {
   679  	// Value is the data value marked by this Tick.
   680  	Value float64
   681  
   682  	// Label is the text to display at the tick mark.
   683  	// If Label is an empty string then this is a minor
   684  	// tick mark.
   685  	Label string
   686  }
   687  
   688  // IsMinor returns true if this is a minor tick mark.
   689  func (t Tick) IsMinor() bool {
   690  	return t.Label == ""
   691  }
   692  
   693  // lengthOffset returns an offset that should be added to the
   694  // tick mark's line to accout for its length.  I.e., the start of
   695  // the line for a minor tick mark must be shifted by half of
   696  // the length.
   697  func (t Tick) lengthOffset(len vg.Length) vg.Length {
   698  	if t.IsMinor() {
   699  		return len / 2
   700  	}
   701  	return 0
   702  }
   703  
   704  // tickLabelHeight returns height of the tick mark labels.
   705  func tickLabelHeight(sty text.Style, ticks []Tick) vg.Length {
   706  	maxHeight := vg.Length(0)
   707  	for _, t := range ticks {
   708  		if t.IsMinor() {
   709  			continue
   710  		}
   711  		r := sty.Rectangle(t.Label)
   712  		h := r.Max.Y - r.Min.Y
   713  		if h > maxHeight {
   714  			maxHeight = h
   715  		}
   716  	}
   717  	return maxHeight
   718  }
   719  
   720  // tickLabelWidth returns the width of the widest tick mark label.
   721  func tickLabelWidth(sty text.Style, ticks []Tick) vg.Length {
   722  	maxWidth := vg.Length(0)
   723  	for _, t := range ticks {
   724  		if t.IsMinor() {
   725  			continue
   726  		}
   727  		r := sty.Rectangle(t.Label)
   728  		w := r.Max.X - r.Min.X
   729  		if w > maxWidth {
   730  			maxWidth = w
   731  		}
   732  	}
   733  	return maxWidth
   734  }
   735  
   736  // formatFloatTick returns a g-formated string representation of v
   737  // to the specified precision.
   738  func formatFloatTick(v float64, prec int) string {
   739  	return strconv.FormatFloat(v, 'g', prec, 64)
   740  }
   741  
   742  // TickerFunc is suitable for the Tick.Marker field of an Axis.
   743  // It is an adapter which allows to quickly setup a Ticker using a function with an appropriate signature.
   744  type TickerFunc func(min, max float64) []Tick
   745  
   746  var _ Ticker = TickerFunc(nil)
   747  
   748  // Ticks implements plot.Ticker.
   749  func (f TickerFunc) Ticks(min, max float64) []Tick {
   750  	return f(min, max)
   751  }
   752  

View as plain text