go-hep.org/x/hep@v0.38.1/hplot/label.go (about)

     1  // Copyright ©2020 The go-hep 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 hplot
     6  
     7  import (
     8  	"fmt"
     9  	"image/color"
    10  	"math"
    11  
    12  	"gonum.org/v1/plot"
    13  	"gonum.org/v1/plot/plotter"
    14  	"gonum.org/v1/plot/vg/draw"
    15  )
    16  
    17  // Label displays a user-defined text string on a plot.
    18  //
    19  // Fields of Label should not be modified once the Label has been
    20  // added to an hplot.Plot.
    21  type Label struct {
    22  	Text      string         // Text of the label
    23  	X, Y      float64        // Position of the label
    24  	TextStyle draw.TextStyle // Text style of the label
    25  
    26  	// Normalized indicates whether the label position
    27  	// is in data coordinates or normalized with regard
    28  	// to the canvas space.
    29  	// When normalized, the label position is assumed
    30  	// to fall in the [0, 1] interval. If true, NewLabel
    31  	// panics if x or y are outside [0, 1].
    32  	//
    33  	// Normalized is false by default.
    34  	Normalized bool
    35  
    36  	// AutoAdjust enables auto adjustment of the label
    37  	// position, when Normalized is true and when x
    38  	// and/or y are close to 1 and the label is partly
    39  	// outside the canvas. If false and the label doesn't
    40  	// fit in the canvas, an error is returned.
    41  	//
    42  	// AutoAdjust is false by default.
    43  	AutoAdjust bool
    44  
    45  	// cache of gonum/plot.Labels
    46  	plt *plotter.Labels
    47  }
    48  
    49  // NewLabel creates a new txt label at position (x, y).
    50  func NewLabel(x, y float64, txt string, opts ...LabelOption) *Label {
    51  
    52  	style := draw.TextStyle{
    53  		Color:   color.Black,
    54  		Font:    DefaultStyle.Fonts.Tick, // FIXME(sbinet): add a field in Style?
    55  		Handler: DefaultStyle.TextHandler,
    56  	}
    57  
    58  	cfg := &labelConfig{
    59  		TextStyle: style,
    60  	}
    61  
    62  	for _, opt := range opts {
    63  		opt(cfg)
    64  	}
    65  	if cfg.TextStyle.Handler == nil {
    66  		cfg.TextStyle.Handler = style.Handler
    67  	}
    68  
    69  	if cfg.Normalized {
    70  		if !(0 <= x && x <= 1) {
    71  			panic(fmt.Errorf(
    72  				"hplot: normalized label x-position is outside [0,1]: %g", x,
    73  			))
    74  		}
    75  		if !(0 <= y && y <= 1) {
    76  			panic(fmt.Errorf(
    77  				"hplot: normalized label y-position is outside [0,1]: %g", y,
    78  			))
    79  		}
    80  	}
    81  
    82  	return &Label{
    83  		Text:       txt,
    84  		X:          x,
    85  		Y:          y,
    86  		TextStyle:  cfg.TextStyle,
    87  		Normalized: cfg.Normalized,
    88  		AutoAdjust: cfg.AutoAdjust,
    89  	}
    90  }
    91  
    92  // Plot implements the Plotter interface,
    93  // drawing the label on the canvas.
    94  func (lbl *Label) Plot(c draw.Canvas, p *plot.Plot) {
    95  	lbl.labels(c, p).Plot(c, p)
    96  }
    97  
    98  // DataRange returns the minimum and maximum x and
    99  // y values, implementing the plot.DataRanger interface.
   100  func (lbl *Label) DataRange() (xmin, xmax, ymin, ymax float64) {
   101  	if lbl.Normalized {
   102  		return math.Inf(+1), math.Inf(-1), math.Inf(+1), math.Inf(-1)
   103  	}
   104  
   105  	return lbl.labels(draw.Canvas{}, nil).DataRange()
   106  }
   107  
   108  // GlyphBoxes returns a GlyphBox, corresponding to the label.
   109  // GlyphBoxes implements the plot.GlyphBoxer interface.
   110  func (lbl *Label) GlyphBoxes(p *plot.Plot) []plot.GlyphBox {
   111  	if lbl.plt == nil {
   112  		return nil
   113  	}
   114  	// we expect Label.Plot(c,p) has already been called.
   115  	return lbl.labels(draw.Canvas{}, p).GlyphBoxes(p)
   116  }
   117  
   118  // Internal helper function to get plotter.Labels type.
   119  func (lbl *Label) labels(c draw.Canvas, p *plot.Plot) *plotter.Labels {
   120  	if lbl.plt != nil {
   121  		return lbl.plt
   122  	}
   123  
   124  	var (
   125  		x = lbl.X
   126  		y = lbl.Y
   127  
   128  		err error
   129  	)
   130  
   131  	if lbl.Normalized {
   132  		// Check whether the label fits in the canvas
   133  		box := lbl.TextStyle.Rectangle(lbl.Text)
   134  		rect := c.Rectangle.Size()
   135  		xmax := lbl.X + box.Max.X.Points()/rect.X.Points()
   136  		ymax := lbl.Y + box.Max.Y.Points()/rect.Y.Points()
   137  		if xmax > 1 || ymax > 1 {
   138  			switch {
   139  			case lbl.AutoAdjust:
   140  				x, y = lbl.adjust(1/rect.X.Points(), 1/rect.Y.Points())
   141  			default:
   142  				panic(fmt.Errorf(
   143  					"hplot: label (%g, %g) falls outside data canvas",
   144  					x, y,
   145  				))
   146  			}
   147  		}
   148  
   149  		// Turn relative into absolute coordinates
   150  		x = lbl.scale(x, p.X.Min, p.X.Max, p.X.Scale)
   151  		y = lbl.scale(y, p.Y.Min, p.Y.Max, p.Y.Scale)
   152  	}
   153  
   154  	lbl.plt, err = plotter.NewLabels(plotter.XYLabels{
   155  		XYs:    []plotter.XY{{X: x, Y: y}},
   156  		Labels: []string{lbl.Text},
   157  	})
   158  	if err != nil {
   159  		panic(fmt.Errorf("hplot: could not create labels: %w", err))
   160  	}
   161  
   162  	lbl.plt.TextStyle = []draw.TextStyle{lbl.TextStyle}
   163  
   164  	return lbl.plt
   165  }
   166  
   167  func (lbl *Label) adjust(xnorm, ynorm float64) (x, y float64) {
   168  	x = lbl.adjustX(xnorm)
   169  	y = lbl.adjustY(ynorm)
   170  	return x, y
   171  }
   172  
   173  func (lbl *Label) adjustX(xnorm float64) float64 {
   174  	var (
   175  		box  = lbl.TextStyle.Rectangle(lbl.Text)
   176  		size = box.Size().X.Points() * xnorm
   177  		x    = lbl.X
   178  		dx   = size - (1 - x)
   179  	)
   180  	if x+size > 1 {
   181  		x -= dx
   182  	}
   183  	if x < 0 {
   184  		x = 0
   185  	}
   186  	return x
   187  }
   188  
   189  func (lbl *Label) adjustY(ynorm float64) float64 {
   190  	var (
   191  		box  = lbl.TextStyle.Rectangle(lbl.Text)
   192  		size = box.Size().Y.Points() * ynorm
   193  		y    = lbl.Y
   194  		dy   = size - (1 - y)
   195  	)
   196  	if y+size > 1 {
   197  		y -= dy
   198  	}
   199  	if y < 0 {
   200  		y = 0
   201  	}
   202  	return y
   203  }
   204  
   205  func (Label) scale(v, min, max float64, scaler plot.Normalizer) float64 {
   206  	mid := min + 0.5*(max-min)
   207  	if math.Abs(scaler.Normalize(min, max, mid)-0.5) < 1e-12 {
   208  		return min + v*(max-min)
   209  	}
   210  
   211  	// log-scale
   212  	min = math.Log(min)
   213  	max = math.Log(max)
   214  	return math.Exp(min + v*(max-min))
   215  }
   216  
   217  type labelConfig struct {
   218  	TextStyle  draw.TextStyle
   219  	Normalized bool
   220  	AutoAdjust bool
   221  }
   222  
   223  // LabelOption handles various options to configure a Label.
   224  type LabelOption func(cfg *labelConfig)
   225  
   226  // WithLabelTextStyle specifies the text style of the label.
   227  func WithLabelTextStyle(style draw.TextStyle) LabelOption {
   228  	return func(cfg *labelConfig) {
   229  		cfg.TextStyle = style
   230  	}
   231  }
   232  
   233  // WithLabelNormalized specifies whether the coordinates are
   234  // normalized to the canvas size.
   235  func WithLabelNormalized(norm bool) LabelOption {
   236  	return func(cfg *labelConfig) {
   237  		cfg.Normalized = norm
   238  	}
   239  }
   240  
   241  // WithLabelAutoAdjust specifies whether the coordinates are
   242  // automatically adjusted to the canvas size.
   243  func WithLabelAutoAdjust(auto bool) LabelOption {
   244  	return func(cfg *labelConfig) {
   245  		cfg.AutoAdjust = auto
   246  	}
   247  }
   248  
   249  var (
   250  	_ plot.Plotter    = (*Label)(nil)
   251  	_ plot.DataRanger = (*Label)(nil)
   252  	_ plot.GlyphBoxer = (*Label)(nil)
   253  )