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 )