go-hep.org/x/hep@v0.38.1/hplot/hstack.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  	"math"
    10  
    11  	"go-hep.org/x/hep/hbook"
    12  	"gonum.org/v1/plot"
    13  	"gonum.org/v1/plot/font"
    14  	"gonum.org/v1/plot/vg"
    15  	"gonum.org/v1/plot/vg/draw"
    16  )
    17  
    18  // HStack implements the plot.Plotter interface,
    19  // drawing a stack of histograms.
    20  type HStack struct {
    21  	hs []*H1D
    22  
    23  	// LogY allows rendering with a log-scaled Y axis.
    24  	// When enabled, histogram bins with no entries will be discarded from
    25  	// the histogram's DataRange.
    26  	// The lowest Y value for the DataRange will be corrected to leave an
    27  	// arbitrary amount of height for the smallest bin entry so it is visible
    28  	// on the final plot.
    29  	LogY bool
    30  
    31  	// Stack specifies how histograms are displayed.
    32  	// Default is to display histograms stacked on top of each other.
    33  	// If not stacked, individual histogram uncertainty bands will be
    34  	// displayed when defined.
    35  	// If stacked, individual uncertainty bands will not be diplayed
    36  	// but the total band can be displayed with the hplot.WithBand(true)
    37  	// option.
    38  	Stack HStackKind
    39  
    40  	// Band displays a colored band between the y-min and y-max error bars.
    41  	// Error bars are computed as the bin-by-bin quadratic sum of individual
    42  	// histogram uncertainties.
    43  	Band *BinnedErrBand
    44  }
    45  
    46  // HStackKind customizes how a HStack should behave.
    47  type HStackKind int
    48  
    49  const (
    50  	// HStackOn instructs HStack to display histograms
    51  	// stacked on top of each other.
    52  	HStackOn HStackKind = iota
    53  	// HStackOff instructs HStack to display histograms
    54  	// with the stack disabled.
    55  	HStackOff
    56  )
    57  
    58  func (hsk HStackKind) yoffs(i int, ys []float64, v float64) {
    59  	switch hsk {
    60  	case HStackOn:
    61  		ys[i] += v
    62  	case HStackOff:
    63  		// no-op
    64  	default:
    65  		panic(fmt.Errorf("hplot: unknow HStackKind value %d", hsk))
    66  	}
    67  }
    68  
    69  // NewHStack creates a new histogram stack from the provided list of histograms.
    70  // NewHStack panicks if the list of histograms is empty.
    71  // NewHStack panicks if the histograms have different binning.
    72  func NewHStack(histos []*H1D, opts ...Options) *HStack {
    73  	if len(histos) == 0 {
    74  		panic(fmt.Errorf("hplot: not enough histograms to make a stack"))
    75  	}
    76  
    77  	cfg := newConfig(opts)
    78  
    79  	hstack := &HStack{
    80  		hs:    make([]*H1D, len(histos)),
    81  		Stack: HStackOn,
    82  		LogY:  cfg.log.y,
    83  	}
    84  	copy(hstack.hs, histos)
    85  
    86  	ref := hstack.hs[0].Hist.Binning.Bins
    87  	for _, h := range hstack.hs {
    88  		h.LogY = cfg.log.y
    89  		hstack.checkBins(ref, h.Hist.Binning.Bins)
    90  	}
    91  
    92  	if cfg.band {
    93  		hstack.Band = NewH1D(hstack.summedH1D(),
    94  			WithBand(true),
    95  			WithLogY(cfg.log.y),
    96  		).Band
    97  	}
    98  
    99  	return hstack
   100  }
   101  
   102  // summedH1D returns the summed histogram
   103  func (hstack *HStack) summedH1D() *hbook.H1D {
   104  	bookHtot := hstack.hs[0].Hist
   105  	for _, h := range hstack.hs[1:] {
   106  		bookHtot = hbook.AddH1D(bookHtot, h.Hist)
   107  	}
   108  	return bookHtot
   109  }
   110  
   111  // DataRange returns the minimum and maximum X and Y values
   112  func (hstack *HStack) DataRange() (xmin, xmax, ymin, ymax float64) {
   113  	xmin = math.Inf(+1)
   114  	xmax = math.Inf(-1)
   115  	ymin = math.Inf(+1)
   116  	ymax = math.Inf(-1)
   117  	ylow := math.Inf(+1) // ylow will hold the smallest non-zero y value.
   118  
   119  	yoffs := make([]float64, len(hstack.hs[0].Hist.Binning.Bins))
   120  	for _, h := range hstack.hs {
   121  		for i, bin := range h.Hist.Binning.Bins {
   122  			sumw := bin.SumW()
   123  			xmax = math.Max(bin.XMax(), xmax)
   124  			xmin = math.Min(bin.XMin(), xmin)
   125  			ymax = math.Max(yoffs[i]+sumw, ymax)
   126  			ymin = math.Min(yoffs[i]+sumw, ymin)
   127  			if bin.SumW() != 0 && !(sumw <= 0 && hstack.LogY) {
   128  				ylow = math.Min(bin.SumW(), ylow)
   129  			}
   130  			hstack.Stack.yoffs(i, yoffs, sumw)
   131  		}
   132  	}
   133  
   134  	// Handle error bands the error band of individual histograms
   135  	switch hstack.Stack {
   136  	case HStackOff:
   137  		for _, h := range hstack.hs {
   138  			if h.Band != nil {
   139  				_, _, ymin1, ymax1 := h.Band.DataRange()
   140  				for i := range h.Hist.Binning.Bins {
   141  					ymin = math.Min(yoffs[i]+ymin1, ymin)
   142  					ymax = math.Max(yoffs[i]+ymax1, ymax)
   143  				}
   144  			}
   145  		}
   146  	case HStackOn:
   147  		if hstack.Band != nil {
   148  			_, _, ymin1, ymax1 := hstack.Band.DataRange()
   149  			ymin = math.Min(ymin, ymin1)
   150  			ymax = math.Max(ymax, ymax1)
   151  		}
   152  	}
   153  
   154  	if hstack.LogY {
   155  		if ymin <= 0 && !math.IsInf(ylow, +1) {
   156  			// Reserve a bit of space for the smallest bin to be displayed still.
   157  			ymin = ylow * 0.5
   158  		}
   159  	}
   160  
   161  	return xmin, xmax, ymin, ymax
   162  }
   163  
   164  // Plot implements the Plotter interface, drawing a line
   165  // that connects each point in the Line.
   166  func (hstack *HStack) Plot(c draw.Canvas, p *plot.Plot) {
   167  	for i := range hstack.hs {
   168  		hstack.hs[i].LogY = hstack.LogY
   169  	}
   170  
   171  	yoffs := make([]float64, len(hstack.hs[0].Hist.Binning.Bins))
   172  	for i, h := range hstack.hs {
   173  		hstack.hplot(c, p, h, yoffs, hstack.Stack, i)
   174  	}
   175  }
   176  
   177  func (hstack *HStack) checkBins(refs, bins []hbook.Bin1D) {
   178  	if len(refs) != len(bins) {
   179  		panic("hplot: bins length mismatch")
   180  	}
   181  	for i := range refs {
   182  		ref := refs[i]
   183  		bin := bins[i]
   184  		if ref.Range != bin.Range {
   185  			panic("hplot: bin range mismatch")
   186  		}
   187  	}
   188  }
   189  
   190  func (hs *HStack) hplot(c draw.Canvas, p *plot.Plot, h *H1D, yoffs []float64, hsk HStackKind, ih int) {
   191  	trX, trY := p.Transforms(&c)
   192  	var pts []vg.Point
   193  	hist := h.Hist
   194  	bins := h.Hist.Binning.Bins
   195  	nbins := len(bins)
   196  
   197  	yfct := func(i int, sumw float64) (ymin, ymax vg.Length) {
   198  		return trY(yoffs[i]), trY(yoffs[i] + sumw)
   199  	}
   200  	if h.LogY {
   201  		yfct = func(i int, sumw float64) (ymin, ymax vg.Length) {
   202  			ymin = c.Min.Y
   203  			if yoffs[i] > 0 {
   204  				ymin = trY(yoffs[i])
   205  			}
   206  			ymax = c.Min.Y
   207  			if sumw+yoffs[i] > 0 {
   208  				ymax = trY(yoffs[i] + sumw)
   209  			}
   210  			return ymin, ymax
   211  		}
   212  	}
   213  
   214  	for i, bin := range bins {
   215  		xmin := trX(bin.XMin())
   216  		xmax := trX(bin.XMax())
   217  		sumw := bin.SumW()
   218  		ymin, ymax := yfct(i, sumw)
   219  		switch i {
   220  		case 0:
   221  			pts = append(pts, vg.Point{X: xmin, Y: ymin})
   222  			pts = append(pts, vg.Point{X: xmin, Y: ymax})
   223  			pts = append(pts, vg.Point{X: xmax, Y: ymax})
   224  
   225  		case nbins - 1:
   226  			lft := bins[i-1]
   227  			xlft := trX(lft.XMax())
   228  			_, ylft := yfct(i-1, lft.SumW())
   229  			pts = append(pts, vg.Point{X: xlft, Y: ylft})
   230  			pts = append(pts, vg.Point{X: xmin, Y: ymax})
   231  			pts = append(pts, vg.Point{X: xmax, Y: ymax})
   232  			pts = append(pts, vg.Point{X: xmax, Y: ymin})
   233  
   234  		default:
   235  			lft := bins[i-1]
   236  			xlft := trX(lft.XMax())
   237  			_, ylft := yfct(i-1, lft.SumW())
   238  			pts = append(pts, vg.Point{X: xlft, Y: ylft})
   239  			pts = append(pts, vg.Point{X: xmin, Y: ymax})
   240  			pts = append(pts, vg.Point{X: xmax, Y: ymax})
   241  		}
   242  
   243  		if h.GlyphStyle.Radius != 0 {
   244  			x := trX(bin.XMid())
   245  			_, y := yfct(i, bin.SumW())
   246  			c.DrawGlyph(h.GlyphStyle, vg.Point{X: x, Y: y})
   247  		}
   248  	}
   249  
   250  	if h.FillColor != nil {
   251  		poly := pts
   252  		for i := range yoffs {
   253  			j := len(yoffs) - 1 - i
   254  			bin := bins[j]
   255  			xmin := trX(bin.XMin())
   256  			xmax := trX(bin.XMax())
   257  			ymin, _ := yfct(j, bin.SumW())
   258  			poly = append(poly, vg.Point{X: xmax, Y: ymin})
   259  			poly = append(poly, vg.Point{X: xmin, Y: ymin})
   260  		}
   261  		c.FillPolygon(h.FillColor, c.ClipPolygonXY(poly))
   262  	}
   263  
   264  	// Plot individual histo band when not stacked or total band
   265  	switch hsk {
   266  	case HStackOff:
   267  		if h.Band != nil {
   268  			h.Band.Plot(c, p)
   269  		}
   270  	case HStackOn:
   271  		if ih == len(hs.hs)-1 && hs.Band != nil {
   272  			hs.Band.Plot(c, p)
   273  		}
   274  	}
   275  
   276  	c.StrokeLines(h.LineStyle, c.ClipLinesXY(pts)...)
   277  
   278  	if h.YErrs != nil {
   279  		yerrs := h.withYErrBars(yoffs)
   280  
   281  		yerrs.LineStyle = h.YErrs.LineStyle
   282  		yerrs.CapWidth = h.YErrs.CapWidth
   283  
   284  		yerrs.Plot(c, p)
   285  	}
   286  
   287  	if h.Infos.Style != HInfoNone {
   288  		fnt := font.From(DefaultStyle.Fonts.Tick, DefaultStyle.Fonts.Tick.Size)
   289  		sty := draw.TextStyle{Font: fnt}
   290  		legend := histLegend{
   291  			ColWidth:  DefaultStyle.Fonts.Tick.Size,
   292  			TextStyle: sty,
   293  		}
   294  
   295  		for i := uint32(0); i < 32; i++ {
   296  			switch h.Infos.Style & (1 << i) {
   297  			case HInfoEntries:
   298  				legend.Add("Entries", hist.Entries())
   299  			case HInfoMean:
   300  				legend.Add("Mean", hist.XMean())
   301  			case HInfoRMS:
   302  				legend.Add("RMS", hist.XRMS())
   303  			case HInfoStdDev:
   304  				legend.Add("Std Dev", hist.XStdDev())
   305  			default:
   306  			}
   307  		}
   308  		legend.Top = true
   309  
   310  		legend.draw(c)
   311  	}
   312  
   313  	// handle stack, if any.
   314  	for i, bin := range bins {
   315  		sumw := bin.SumW()
   316  		hsk.yoffs(i, yoffs, sumw)
   317  	}
   318  }
   319  
   320  var (
   321  	_ plot.Plotter = (*HStack)(nil)
   322  )