go-hep.org/x/hep@v0.38.1/hplot/h1d.go (about) 1 // Copyright ©2016 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 "errors" 9 "fmt" 10 "image/color" 11 "math" 12 13 "go-hep.org/x/hep/hbook" 14 "gonum.org/v1/plot" 15 "gonum.org/v1/plot/font" 16 "gonum.org/v1/plot/plotter" 17 "gonum.org/v1/plot/text" 18 "gonum.org/v1/plot/vg" 19 "gonum.org/v1/plot/vg/draw" 20 ) 21 22 // H1D implements the plotter.Plotter interface, 23 // drawing a histogram of the data. 24 type H1D struct { 25 // Hist is the histogramming data 26 Hist *hbook.H1D 27 28 // FillColor is the color used to fill each 29 // bar of the histogram. If the color is nil 30 // then the bars are not filled. 31 FillColor color.Color 32 33 // LineStyle is the style of the outline of each 34 // bar of the histogram. 35 draw.LineStyle 36 37 // GlyphStyle is the style of the glyphs drawn 38 // at the top of each histogram bar. 39 GlyphStyle draw.GlyphStyle 40 41 // LogY allows rendering with a log-scaled Y axis. 42 // When enabled, histogram bins with no entries will be discarded from 43 // the histogram's DataRange. 44 // The lowest Y value for the DataRange will be corrected to leave an 45 // arbitrary amount of height for the smallest bin entry so it is visible 46 // on the final plot. 47 LogY bool 48 49 // InfoStyle is the style of infos displayed for 50 // the histogram (entries, mean, rms). 51 Infos HInfos 52 53 // YErrs is the y error bars plotter. 54 YErrs *plotter.YErrorBars 55 56 // Band displays a colored band between the y-min and y-max error bars. 57 // The band is shown in the legend thumbnail only if there is no filling. 58 Band *BinnedErrBand 59 } 60 61 type HInfoStyle uint32 62 63 const ( 64 HInfoNone HInfoStyle = 0 65 HInfoEntries HInfoStyle = 1 << iota 66 HInfoMean 67 HInfoRMS 68 HInfoStdDev 69 HInfoSummary HInfoStyle = HInfoEntries | HInfoMean | HInfoStdDev 70 ) 71 72 type HInfos struct { 73 Style HInfoStyle 74 } 75 76 // NewH1FromXYer returns a new histogram 77 // that represents the distribution of values 78 // using the given number of bins. 79 // 80 // Each y value is assumed to be the frequency 81 // count for the corresponding x. 82 // 83 // It panics if the number of bins is non-positive. 84 func NewH1FromXYer(xy plotter.XYer, n int, opts ...Options) *H1D { 85 if n <= 0 { 86 panic(errors.New("hplot: histogram with non-positive number of bins")) 87 } 88 h := newHistFromXYer(xy, n) 89 return NewH1D(h, opts...) 90 } 91 92 // NewH1FromValuer returns a new histogram, as in 93 // NewH1FromXYer, except that it accepts a plotter.Valuer 94 // instead of an XYer. 95 func NewH1FromValuer(vs plotter.Valuer, n int, opts ...Options) *H1D { 96 return NewH1FromXYer(unitYs{vs}, n, opts...) 97 } 98 99 type unitYs struct { 100 plotter.Valuer 101 } 102 103 func (u unitYs) XY(i int) (float64, float64) { 104 return u.Value(i), 1.0 105 } 106 107 // NewH1D returns a new histogram, as in 108 // NewH1DFromXYer, except that it accepts a hbook.H1D 109 // instead of a plotter.XYer 110 func NewH1D(h *hbook.H1D, opts ...Options) *H1D { 111 h1 := &H1D{ 112 Hist: h, 113 LineStyle: plotter.DefaultLineStyle, 114 } 115 116 cfg := newConfig(opts) 117 118 h1.LogY = cfg.log.y 119 h1.Infos = cfg.hinfos 120 121 if cfg.band { 122 h1.Band = h1.withBand() 123 } 124 125 if cfg.bars.yerrs { 126 h1.YErrs = h1.withYErrBars(nil) 127 } 128 129 if cfg.glyph != (draw.GlyphStyle{}) { 130 h1.GlyphStyle = cfg.glyph 131 } 132 133 return h1 134 } 135 136 // withYErrBars enables the Y error bars 137 func (h *H1D) withYErrBars(yoffs []float64) *plotter.YErrorBars { 138 bins := h.Hist.Binning.Bins 139 if yoffs == nil { 140 yoffs = make([]float64, len(bins)) 141 } 142 data := make(plotter.XYs, 0, len(bins)) 143 yerr := make(plotter.YErrors, 0, len(bins)) 144 for i, bin := range bins { 145 if bin.Entries() == 0 { 146 continue 147 } 148 data = append(data, plotter.XY{ 149 X: bin.XMid(), 150 Y: yoffs[i] + bin.SumW(), 151 }) 152 ey := 0.5 * bin.ErrW() 153 yerr = append(yerr, struct{ Low, High float64 }{ey, ey}) 154 } 155 156 type yerrT struct { 157 plotter.XYer 158 plotter.YErrorer 159 } 160 161 yplt, err := plotter.NewYErrorBars(yerrT{data, yerr}) 162 if err != nil { 163 panic(err) 164 } 165 yplt.LineStyle.Color = h.LineStyle.Color 166 yplt.LineStyle.Width = h.LineStyle.Width 167 168 return yplt 169 } 170 171 // withBand enables the band between ymin-ymax error bars. 172 func (h1 *H1D) withBand() *BinnedErrBand { 173 b := NewBinnedErrBand(h1.Hist.Counts()) 174 b.FillColor = color.Gray{200} 175 b.LogY = h1.LogY 176 return b 177 } 178 179 // DataRange returns the minimum and maximum X and Y values 180 func (h *H1D) DataRange() (xmin, xmax, ymin, ymax float64) { 181 182 if !h.LogY { 183 xmin, xmax, ymin, ymax = h.Hist.DataRange() 184 if h.YErrs != nil { 185 xmin1, xmax1, ymin1, ymax1 := h.YErrs.DataRange() 186 xmin = math.Min(xmin, xmin1) 187 ymin = math.Min(ymin, ymin1) 188 xmax = math.Max(xmax, xmax1) 189 ymax = math.Max(ymax, ymax1) 190 } 191 if h.Band != nil { 192 xmin1, xmax1, ymin1, ymax1 := h.Band.DataRange() 193 xmin = math.Min(xmin, xmin1) 194 ymin = math.Min(ymin, ymin1) 195 xmax = math.Max(xmax, xmax1) 196 ymax = math.Max(ymax, ymax1) 197 } 198 return 199 } 200 201 xmin = math.Inf(+1) 202 xmax = math.Inf(-1) 203 ymin = math.Inf(+1) 204 ymax = math.Inf(-1) 205 ylow := math.Inf(+1) // ylow will hold the smallest positive y value. 206 for _, bin := range h.Hist.Binning.Bins { 207 xmax = math.Max(bin.XMax(), xmax) 208 xmin = math.Min(bin.XMin(), xmin) 209 ymax = math.Max(bin.SumW(), ymax) 210 ymin = math.Min(bin.SumW(), ymin) 211 if bin.SumW() > 0 { 212 ylow = math.Min(bin.SumW(), ylow) 213 } 214 } 215 216 if ymin == 0 && !math.IsInf(ylow, +1) { 217 // Reserve a bit of space for the smallest bin to be displayed still. 218 ymin = ylow * 0.5 219 } 220 221 if h.YErrs != nil { 222 xmin1, xmax1, ymin1, ymax1 := h.YErrs.DataRange() 223 xmin = math.Min(xmin, xmin1) 224 ymin = math.Min(ymin, ymin1) 225 xmax = math.Max(xmax, xmax1) 226 ymax = math.Min(ymax, ymax1) 227 } 228 229 if h.Band != nil { 230 xmin1, xmax1, ymin1, ymax1 := h.Band.DataRange() 231 xmin = math.Min(xmin, xmin1) 232 ymin = math.Min(ymin, ymin1) 233 xmax = math.Max(xmax, xmax1) 234 ymax = math.Max(ymax, ymax1) 235 } 236 237 return 238 } 239 240 // Plot implements the Plotter interface, drawing a line 241 // that connects each point in the Line. 242 func (h *H1D) Plot(c draw.Canvas, p *plot.Plot) { 243 trX, trY := p.Transforms(&c) 244 var pts []vg.Point 245 hist := h.Hist 246 bins := h.Hist.Binning.Bins 247 nbins := len(bins) 248 249 yfct := func(sumw float64) (ymin, ymax vg.Length) { 250 return trY(0), trY(sumw) 251 } 252 if h.LogY { 253 yfct = func(sumw float64) (ymin, ymax vg.Length) { 254 ymin = c.Min.Y 255 ymax = c.Min.Y 256 if sumw != 0 { 257 ymax = trY(sumw) 258 } 259 return ymin, ymax 260 } 261 } 262 263 var glyphs []vg.Point 264 265 for i, bin := range bins { 266 xmin := trX(bin.XMin()) 267 xmax := trX(bin.XMax()) 268 sumw := bin.SumW() 269 ymin, ymax := yfct(sumw) 270 switch i { 271 case 0: 272 pts = append(pts, vg.Point{X: xmin, Y: ymin}) 273 pts = append(pts, vg.Point{X: xmin, Y: ymax}) 274 pts = append(pts, vg.Point{X: xmax, Y: ymax}) 275 276 case nbins - 1: 277 lft := bins[i-1] 278 xlft := trX(lft.XMax()) 279 _, ylft := yfct(lft.SumW()) 280 pts = append(pts, vg.Point{X: xlft, Y: ylft}) 281 pts = append(pts, vg.Point{X: xmin, Y: ymax}) 282 pts = append(pts, vg.Point{X: xmax, Y: ymax}) 283 pts = append(pts, vg.Point{X: xmax, Y: ymin}) 284 285 default: 286 lft := bins[i-1] 287 xlft := trX(lft.XMax()) 288 _, ylft := yfct(lft.SumW()) 289 pts = append(pts, vg.Point{X: xlft, Y: ylft}) 290 pts = append(pts, vg.Point{X: xmin, Y: ymax}) 291 pts = append(pts, vg.Point{X: xmax, Y: ymax}) 292 } 293 294 if h.GlyphStyle.Radius != 0 { 295 x := trX(bin.XMid()) 296 _, y := yfct(bin.SumW()) 297 // capture glyph location, to be drawn after 298 // the histogram line, if any. 299 glyphs = append(glyphs, vg.Point{X: x, Y: y}) 300 } 301 } 302 303 if h.FillColor != nil { 304 c.FillPolygon(h.FillColor, c.ClipPolygonXY(pts)) 305 } 306 307 if h.Band != nil { 308 h.Band.Plot(c, p) 309 } 310 311 c.StrokeLines(h.LineStyle, c.ClipLinesXY(pts)...) 312 313 if h.YErrs != nil { 314 h.YErrs.Plot(c, p) 315 } 316 317 if h.GlyphStyle.Radius != 0 { 318 for _, glyph := range glyphs { 319 c.DrawGlyph(h.GlyphStyle, glyph) 320 } 321 } 322 323 if h.Infos.Style != HInfoNone { 324 fnt := font.From(DefaultStyle.Fonts.Tick, DefaultStyle.Fonts.Tick.Size) 325 sty := text.Style{ 326 Font: fnt, 327 Handler: p.Title.TextStyle.Handler, 328 } 329 legend := histLegend{ 330 ColWidth: DefaultStyle.Fonts.Tick.Size, 331 TextStyle: sty, 332 } 333 334 for i := uint32(0); i < 32; i++ { 335 switch h.Infos.Style & (1 << i) { 336 case HInfoEntries: 337 legend.Add("Entries", hist.Entries()) 338 case HInfoMean: 339 legend.Add("Mean", hist.XMean()) 340 case HInfoRMS: 341 legend.Add("RMS", hist.XRMS()) 342 case HInfoStdDev: 343 legend.Add("Std Dev", hist.XStdDev()) 344 default: 345 } 346 } 347 legend.Top = true 348 349 legend.draw(c) 350 } 351 } 352 353 // GlyphBoxes returns a slice of GlyphBoxes, 354 // one for each of the bins, implementing the 355 // plot.GlyphBoxer interface. 356 func (h *H1D) GlyphBoxes(p *plot.Plot) []plot.GlyphBox { 357 bins := h.Hist.Binning.Bins 358 bs := make([]plot.GlyphBox, 0, len(bins)) 359 for i := range bins { 360 bin := bins[i] 361 y := bin.SumW() 362 if h.LogY && y == 0 { 363 continue 364 } 365 var box plot.GlyphBox 366 xmin := bin.XMin() 367 w := p.X.Norm(bin.XWidth()) 368 box.X = p.X.Norm(xmin + 0.5*w) 369 box.Y = p.Y.Norm(y) 370 box.Rectangle.Min.X = vg.Length(xmin - 0.5*w) 371 box.Rectangle.Min.Y = vg.Length(y - 0.5*w) 372 box.Rectangle.Max.X = vg.Length(w) 373 box.Rectangle.Max.Y = vg.Length(0) 374 375 r := vg.Points(5) 376 box.Rectangle.Min = vg.Point{X: 0, Y: 0} 377 box.Rectangle.Max = vg.Point{X: 0, Y: r} 378 bs = append(bs, box) 379 } 380 return bs 381 } 382 383 // Normalize normalizes the histogram so that the 384 // total area beneath it sums to a given value. 385 // func (h *Histogram) Normalize(sum float64) { 386 // mass := 0.0 387 // for _, b := range h.Bins { 388 // mass += b.Weight 389 // } 390 // for i := range h.Bins { 391 // h.Bins[i].Weight *= sum / (h.Width * mass) 392 // } 393 // } 394 395 // Thumbnail draws a rectangle in the given style of the histogram. 396 func (h *H1D) Thumbnail(c *draw.Canvas) { 397 ymin := c.Min.Y 398 ymax := c.Max.Y 399 xmin := c.Min.X 400 xmax := c.Max.X 401 dy := ymax - ymin 402 403 // Style of the histogram 404 hasFill := h.FillColor != nil 405 hasLine := h.LineStyle.Width != 0 406 hasGlyph := h.GlyphStyle != (draw.GlyphStyle{}) 407 hasBand := h.Band != nil 408 409 // Define default behaviour with priority 410 // 1) w/ fill: boxline, disregard band 411 // 2) w/o fill: skyline, band and markers 412 drawFill := hasFill 413 drawBand := !drawFill && hasBand 414 drawGlyph := hasGlyph 415 drawSkyLine := !drawFill && hasLine 416 drawBoxLine := hasFill && hasLine 417 418 if drawFill { 419 pts := []vg.Point{ 420 {X: xmin, Y: ymin}, 421 {X: xmax, Y: ymin}, 422 {X: xmax, Y: ymax}, 423 {X: xmin, Y: ymax}, 424 {X: xmin, Y: ymin}, 425 } 426 c.FillPolygon(h.FillColor, c.ClipPolygonXY(pts)) 427 } 428 429 if drawBand { 430 pts := []vg.Point{ 431 {X: xmin, Y: ymin + 0.0*dy}, 432 {X: xmax, Y: ymin + 0.0*dy}, 433 {X: xmax, Y: ymax - 0.0*dy}, 434 {X: xmin, Y: ymax - 0.0*dy}, 435 {X: xmin, Y: ymin + 0.0*dy}, 436 } 437 c.FillPolygon(h.Band.FillColor, c.ClipPolygonXY(pts)) 438 } 439 440 if drawBoxLine { 441 line := []vg.Point{ 442 {X: xmin, Y: ymin}, 443 {X: xmax, Y: ymin}, 444 {X: xmax, Y: ymax}, 445 {X: xmin, Y: ymax}, 446 {X: xmin, Y: ymin}, 447 } 448 c.StrokeLines(h.LineStyle, c.ClipLinesX(line)...) 449 } 450 451 if drawSkyLine { 452 ymid := c.Center().Y 453 line := []vg.Point{{X: xmin, Y: ymid}, {X: xmax, Y: ymid}} 454 c.StrokeLines(h.LineStyle, c.ClipLinesX(line)...) 455 } 456 457 if drawGlyph { 458 c.DrawGlyph(h.GlyphStyle, c.Center()) 459 if h.YErrs != nil { 460 var ( 461 yerrs = h.YErrs 462 vsize = 0.5 * dy * 0.95 463 x = c.Center().X 464 ylo = c.Center().Y - vsize 465 yup = c.Center().Y + vsize 466 xylo = vg.Point{X: x, Y: ylo} 467 xyup = vg.Point{X: x, Y: yup} 468 line = []vg.Point{xylo, xyup} 469 bar = c.ClipLinesY(line) 470 ) 471 c.StrokeLines(yerrs.LineStyle, bar...) 472 for _, pt := range []vg.Point{xylo, xyup} { 473 if c.Contains(pt) { 474 c.StrokeLine2(yerrs.LineStyle, 475 pt.X-yerrs.CapWidth/2, 476 pt.Y, 477 pt.X+yerrs.CapWidth/2, 478 pt.Y, 479 ) 480 } 481 } 482 } 483 } 484 } 485 486 func newHistFromXYer(xys plotter.XYer, n int) *hbook.H1D { 487 xmin, xmax := plotter.Range(plotter.XValues{XYer: xys}) 488 h := hbook.NewH1D(n, xmin, xmax) 489 490 for i := range xys.Len() { 491 x, y := xys.XY(i) 492 h.Fill(x, y) 493 } 494 495 return h 496 } 497 498 // A Legend gives a description of the meaning of different 499 // data elements of the plot. Each legend entry has a name 500 // and a thumbnail, where the thumbnail shows a small 501 // sample of the display style of the corresponding data. 502 type histLegend struct { 503 // TextStyle is the style given to the legend 504 // entry texts. 505 TextStyle draw.TextStyle 506 507 // Padding is the amount of padding to add 508 // betweeneach entry of the legend. If Padding 509 // is zero then entries are spaced based on the 510 // font size. 511 Padding vg.Length 512 513 // Top and Left specify the location of the legend. 514 // If Top is true the legend is located along the top 515 // edge of the plot, otherwise it is located along 516 // the bottom edge. If Left is true then the legend 517 // is located along the left edge of the plot, and the 518 // text is positioned after the icons, otherwise it is 519 // located along the right edge and the text is 520 // positioned before the icons. 521 Top, Left bool 522 523 // XOffs and YOffs are added to the legend's 524 // final position. 525 XOffs, YOffs vg.Length 526 527 // ColWidth is the width of legend names 528 ColWidth vg.Length 529 530 // entries are all of the legendEntries described 531 // by this legend. 532 entries []legendEntry 533 } 534 535 // A legendEntry represents a single line of a legend, it 536 // has a name and an icon. 537 type legendEntry struct { 538 // text is the text associated with this entry. 539 text string 540 541 // value is the value associated with this entry 542 value string 543 } 544 545 // draw draws the legend to the given canvas. 546 func (l *histLegend) draw(c draw.Canvas) { 547 textx := c.Min.X 548 hdr := l.entryWidth() //+ l.TextStyle.Width(" ") 549 l.ColWidth = hdr 550 if !l.Left { 551 textx = c.Max.X - l.ColWidth 552 } 553 textx += l.XOffs 554 555 enth := l.entryHeight() 556 y := c.Max.Y - enth 557 if !l.Top { 558 y = c.Min.Y + (enth+l.Padding)*(vg.Length(len(l.entries))-1) 559 } 560 y += l.YOffs 561 562 colx := &draw.Canvas{ 563 Canvas: c.Canvas, 564 Rectangle: vg.Rectangle{ 565 Min: vg.Point{X: c.Min.X, Y: y}, 566 Max: vg.Point{X: 2 * l.ColWidth, Y: enth}, 567 }, 568 } 569 for _, e := range l.entries { 570 yoffs := (enth - l.TextStyle.Height(e.text)) / 2 571 txt := l.TextStyle 572 txt.XAlign = draw.XLeft 573 c.FillText(txt, vg.Point{X: textx - hdr, Y: colx.Min.Y + yoffs}, e.text) 574 txt.XAlign = draw.XRight 575 c.FillText(txt, vg.Point{X: textx + hdr, Y: colx.Min.Y + yoffs}, e.value) 576 colx.Min.Y -= enth + l.Padding 577 } 578 579 bboxXmin := textx - hdr - l.TextStyle.Width(" ") 580 bboxXmax := c.Max.X 581 bboxYmin := colx.Min.Y + enth 582 bboxYmax := c.Max.Y 583 bbox := []vg.Point{ 584 {X: bboxXmin, Y: bboxYmax}, 585 {X: bboxXmin, Y: bboxYmin}, 586 {X: bboxXmax, Y: bboxYmin}, 587 {X: bboxXmax, Y: bboxYmax}, 588 {X: bboxXmin, Y: bboxYmax}, 589 } 590 c.StrokeLines(plotter.DefaultLineStyle, bbox) 591 } 592 593 // entryHeight returns the height of the tallest legend 594 // entry text. 595 func (l *histLegend) entryHeight() (height vg.Length) { 596 for _, e := range l.entries { 597 if h := l.TextStyle.Height(e.text); h > height { 598 height = h 599 } 600 } 601 return 602 } 603 604 // entryWidth returns the width of the largest legend 605 // entry text. 606 func (l *histLegend) entryWidth() (width vg.Length) { 607 for _, e := range l.entries { 608 if w := l.TextStyle.Width(e.value); w > width { 609 width = w 610 } 611 } 612 return 613 } 614 615 // Add adds an entry to the legend with the given name. 616 // The entry's thumbnail is drawn as the composite of all of the 617 // thumbnails. 618 func (l *histLegend) Add(name string, value any) { 619 str := "" 620 switch value.(type) { 621 case float64, float32: 622 str = fmt.Sprintf("%6.4g ", value) 623 default: 624 str = fmt.Sprintf("%v ", value) 625 } 626 l.entries = append(l.entries, legendEntry{text: name, value: str}) 627 } 628 629 var ( 630 _ plot.Plotter = (*H1D)(nil) 631 _ plot.Thumbnailer = (*H1D)(nil) 632 )