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 )