UNPKG

jsrootdi

Version:
474 lines (385 loc) 17.3 kB
import { clone, create, createHistogram, setHistogramTitle, gStyle, clTList, clTH1I, clTH2, clTH2I, kNoZoom, kNoStats } from '../core.mjs'; import { DrawOptions } from '../base/BasePainter.mjs'; import { ObjectPainter, EAxisBits } from '../base/ObjectPainter.mjs'; import { TH1Painter } from './TH1Painter.mjs'; import { TH2Painter } from './TH2Painter.mjs'; import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs'; /** * @summary Painter class for THStack * * @private */ class THStackPainter extends ObjectPainter { /** @summary constructor * @param {object|string} dom - DOM element for drawing or element id * @param {object} stack - THStack object * @param {string} [opt] - draw options */ constructor(dom, stack, opt) { super(dom, stack, opt); this.firstpainter = null; this.painters = []; // keep painters to be able update objects } /** @summary Cleanup THStack painter */ cleanup() { this.getPadPainter()?.cleanPrimitives(objp => { return (objp === this.firstpainter) || (this.painters.indexOf(objp) >= 0); }); delete this.firstpainter; delete this.painters; super.cleanup(); } /** @summary Build sum of all histograms * @desc Build a separate list fStack containing the running sum of all histograms */ buildStack(stack) { if (!stack.fHists) return false; const nhists = stack.fHists.arr.length; if (nhists <= 0) return false; const lst = create(clTList); lst.Add(clone(stack.fHists.arr[0]), stack.fHists.opt[0]); for (let i = 1; i < nhists; ++i) { const hnext = clone(stack.fHists.arr[i]), hnextopt = stack.fHists.opt[i], hprev = lst.arr[i-1], xnext = hnext.fXaxis, xprev = hprev.fXaxis; let match = (xnext.fNbins === xprev.fNbins) && (xnext.fXmin === xprev.fXmin) && (xnext.fXmax === xprev.fXmax); if (!match && (xnext.fNbins > 0) && (xnext.fNbins < xprev.fNbins) && (xnext.fXmin === xprev.fXmin) && (Math.abs((xnext.fXmax - xnext.fXmin)/xnext.fNbins - (xprev.fXmax - xprev.fXmin)/xprev.fNbins) < 0.0001)) { // simple extension of histogram to make sum const arr = new Array(hprev.fNcells).fill(0); for (let n = 1; n <= xnext.fNbins; ++n) arr[n] = hnext.fArray[n]; hnext.fNcells = hprev.fNcells; Object.assign(xnext, xprev); hnext.fArray = arr; match = true; } if (!match) { console.warn(`When drawing THStack, cannot sum-up histograms ${hnext.fName} and ${hprev.fName}`); lst.Clear(); return false; } // trivial sum of histograms for (let n = 0; n < hnext.fArray.length; ++n) hnext.fArray[n] += hprev.fArray[n]; lst.Add(hnext, hnextopt); } stack.fStack = lst; return true; } /** @summary Returns stack min/max values */ getMinMax(iserr) { const stack = this.getObject(), pad = this.getPadPainter().getRootPad(true); let min = 0, max = 0; const getHistMinMax = (hist, witherr) => { const res = { min: 0, max: 0 }; let domin = true, domax = true; if (hist.fMinimum !== kNoZoom) { res.min = hist.fMinimum; domin = false; } if (hist.fMaximum !== kNoZoom) { res.max = hist.fMaximum; domax = false; } if (!domin && !domax) return res; let i1 = 1, i2 = hist.fXaxis.fNbins, j1 = 1, j2 = 1, first = true; if (hist.fXaxis.TestBit(EAxisBits.kAxisRange)) { i1 = hist.fXaxis.fFirst; i2 = hist.fXaxis.fLast; } if (hist._typename.indexOf(clTH2) === 0) { j2 = hist.fYaxis.fNbins; if (hist.fYaxis.TestBit(EAxisBits.kAxisRange)) { j1 = hist.fYaxis.fFirst; j2 = hist.fYaxis.fLast; } } for (let j = j1; j <= j2; ++j) { for (let i = i1; i <= i2; ++i) { const val = hist.getBinContent(i, j), err = witherr ? hist.getBinError(hist.getBin(i, j)) : 0; if (domin && (first || (val-err < res.min))) res.min = val-err; if (domax && (first || (val+err > res.max))) res.max = val+err; first = false; } } return res; }; if (this.options.nostack) { for (let i = 0; i < stack.fHists.arr.length; ++i) { const resh = getHistMinMax(stack.fHists.arr[i], iserr); if (i === 0) { min = resh.min; max = resh.max; } else { min = Math.min(min, resh.min); max = Math.max(max, resh.max); } } } else { min = getHistMinMax(stack.fStack.arr[0], iserr).min; max = getHistMinMax(stack.fStack.arr[stack.fStack.arr.length-1], iserr).max; } const adjustRange = () => { if (pad && (pad.fLogv ?? (this.options.ndim === 1 ? pad.fLogy : pad.fLogz))) { if (max <= 0) max = 1; if (min <= 0) min = 1e-4*max; const kmin = 1/(1 + 0.5*Math.log10(max / min)), kmax = 1 + 0.2*Math.log10(max / min); min *= kmin; max *= kmax; } else if ((min > 0) && (min < 0.05*max)) min = 0; }; max *= (1 + gStyle.fHistTopMargin); adjustRange(); let max0 = max, min0 = min, zoomed = false; if (stack.fMaximum !== kNoZoom) { max = stack.fMaximum; max0 = Math.max(max, max0); zoomed = true; } if (stack.fMinimum !== kNoZoom) { min = stack.fMinimum; min0 = Math.min(min, min0); zoomed = true; } if (zoomed) adjustRange(); else min = max = kNoZoom; return { min, max, min0, max0, zoomed, hopt: `hmin:${min0};hmax:${max0};minimum:${min};maximum:${max}` }; } /** @summary Provide draw options for the histogram */ getHistDrawOption(hist, opt) { let hopt = opt || hist.fOption || this.options.hopt; if (hopt.toUpperCase().indexOf(this.options.hopt) < 0) hopt += ' ' + this.options.hopt; if (this.options.draw_errors && !hopt) hopt = 'E'; if (!this.options.pads) hopt += ' same nostat' + this.options.auto; return hopt; } /** @summary Draw next stack histogram */ async drawNextHisto(indx, pad_painter) { const stack = this.getObject(), hlst = this.options.nostack ? stack.fHists : stack.fStack, nhists = hlst?.arr?.length || 0; if (indx >= nhists) return this; const rindx = this.options.horder ? indx : nhists-indx-1, subid = this.options.nostack ? `hists_${rindx}` : `stack_${rindx}`, hist = hlst.arr[rindx], hopt = this.getHistDrawOption(hist, hlst.opt[rindx]); // handling of 'pads' draw option if (pad_painter) { const subpad_painter = pad_painter.getSubPadPainter(indx+1); if (!subpad_painter) return this; const prev_name = subpad_painter.selectCurrentPad(subpad_painter.this_pad_name); return this.hdraw_func(subpad_painter.getDom(), hist, hopt).then(subp => { if (subp) { subp.setSecondaryId(this, subid); this.painters.push(subp); } subpad_painter.selectCurrentPad(prev_name); return this.drawNextHisto(indx+1, pad_painter); }); } // special handling of stacked histograms - set $baseh object for correct drawing // also used to provide tooltips if ((rindx > 0) && !this.options.nostack) hist.$baseh = hlst.arr[rindx - 1]; // this number used for auto colors creation if (this.options.auto) hist.$num_histos = nhists; return this.hdraw_func(this.getDom(), hist, hopt).then(subp => { subp.setSecondaryId(this, subid); this.painters.push(subp); return this.drawNextHisto(indx+1, pad_painter); }); } /** @summary Decode draw options of THStack painter */ decodeOptions(opt) { if (!this.options) this.options = {}; Object.assign(this.options, { ndim: 1, nostack: false, same: false, horder: true, has_errors: false, draw_errors: false, hopt: '', auto: '' }); const stack = this.getObject(), hist = stack.fHistogram || (stack.fHists ? stack.fHists.arr[0] : null) || (stack.fStack ? stack.fStack.arr[0] : null), hasErrors = hist => { if (hist.fSumw2 && (hist.fSumw2.length > 0)) { for (let n = 0; n < hist.fSumw2.length; ++n) if (hist.fSumw2[n] > 0) return true; } return false; }; if (hist && (hist._typename.indexOf(clTH2) === 0)) this.options.ndim = 2; if ((this.options.ndim === 2) && !opt) opt = 'lego1'; if (stack.fHists && !this.options.nostack) { for (let k = 0; k < stack.fHists.arr.length; ++k) this.options.has_errors = this.options.has_errors || hasErrors(stack.fHists.arr[k]); } this.options.nhist = stack.fHists?.arr?.length ?? 1; const d = new DrawOptions(opt); this.options.nostack = d.check('NOSTACK'); if (d.check('STACK')) this.options.nostack = false; this.options.same = d.check('SAME'); d.check('NOCLEAR'); // ignore noclear option ['PFC', 'PLC', 'PMC'].forEach(f => { if (d.check(f)) this.options.auto += ' ' + f; }); this.options.pads = d.check('PADS'); if (this.options.pads) this.options.nostack = true; this.options.hopt = d.remain(); // use remaining draw options for histogram draw const dolego = d.check('LEGO'); this.options.errors = d.check('E'); // if any histogram appears with pre-calculated errors, use E for all histograms if (!this.options.nostack && this.options.has_errors && !dolego && !d.check('HIST') && (this.options.hopt.indexOf('E') < 0)) this.options.draw_errors = true; this.options.horder = this.options.nostack || dolego; } /** @summary Create main histogram for THStack axis drawing */ createHistogram(stack) { const histos = stack.fHists, numhistos = histos ? histos.arr.length : 0; if (!numhistos) { const histo = createHistogram(clTH1I, 100); setHistogramTitle(histo, stack.fTitle); histo.fBits |= kNoStats; return histo; } const h0 = histos.arr[0], histo = createHistogram((this.options.ndim === 1) ? clTH1I : clTH2I, h0.fXaxis.fNbins, h0.fYaxis.fNbins); histo.fName = 'axis_hist'; histo.fBits |= kNoStats; Object.assign(histo.fXaxis, h0.fXaxis); if (this.options.ndim === 2) Object.assign(histo.fYaxis, h0.fYaxis); // this code is not exists in ROOT painter, can be skipped? for (let n = 1; n < numhistos; ++n) { const h = histos.arr[n]; if (!histo.fXaxis.fLabels) { histo.fXaxis.fXmin = Math.min(histo.fXaxis.fXmin, h.fXaxis.fXmin); histo.fXaxis.fXmax = Math.max(histo.fXaxis.fXmax, h.fXaxis.fXmax); } if ((this.options.ndim === 2) && !histo.fYaxis.fLabels) { histo.fYaxis.fXmin = Math.min(histo.fYaxis.fXmin, h.fYaxis.fXmin); histo.fYaxis.fXmax = Math.max(histo.fYaxis.fXmax, h.fYaxis.fXmax); } } histo.fTitle = stack.fTitle; return histo; } /** @summary Update thstack object */ updateObject(obj) { if (!this.matchObjectType(obj)) return false; const stack = this.getObject(); stack.fHists = obj.fHists; stack.fStack = obj.fStack; stack.fTitle = obj.fTitle; stack.fMinimum = obj.fMinimum; stack.fMaximum = obj.fMaximum; if (!this.options.nostack) this.options.nostack = !this.buildStack(stack); if (this.firstpainter) { let src = obj.fHistogram; if (!src) src = stack.fHistogram = this.createHistogram(stack); const mm = this.getMinMax(this.options.errors || this.options.draw_errors); this.firstpainter.options.minimum = mm.min; this.firstpainter.options.maximum = mm.max; this.firstpainter._checked_zooming = false; // force to check 3d zooming if (this.options.ndim === 1) { this.firstpainter.ymin = mm.min0; this.firstpainter.ymax = mm.max0; } else { this.firstpainter.zmin = mm.min0; this.firstpainter.zmax = mm.max0; } this.firstpainter.updateObject(src); } // and now update histograms const hlst = this.options.nostack ? stack.fHists : stack.fStack, nhists = hlst?.arr?.length ?? 0; if (nhists !== this.painters.length) { this.did_update = 1; this.getPadPainter()?.cleanPrimitives(objp => this.painters.indexOf(objp) >= 0); this.painters = []; } else { this.did_update = 2; for (let indx = 0; indx < nhists; ++indx) { const rindx = this.options.horder ? indx : nhists - indx - 1, hist = hlst.arr[rindx]; this.painters[indx].updateObject(hist, this.getHistDrawOption(hist, hlst.opt[rindx])); } } return true; } /** @summary Redraw THStack * @desc Do something if previous update had changed number of histograms */ redraw(reason) { if (this.did_update === 1) { delete this.did_update; return this.drawNextHisto(0, this.options.pads ? this.getPadPainter() : null); } else if (this.did_update === 2) { delete this.did_update; const redrawSub = indx => { if (indx >= this.painters.length) return Promise.resolve(this); return this.painters[indx].redraw(reason).then(() => redrawSub(indx+1)); }; return redrawSub(0); } } /** @summary Fill hstack context menu */ fillContextMenuItems(menu) { menu.addchk(this.options.draw_errors, 'Draw errors', flag => { this.options.draw_errors = flag; const stack = this.getObject(), hlst = this.options.nostack ? stack.fHists : stack.fStack, nhists = hlst?.arr?.length ?? 0; for (let indx = 0; indx < nhists; ++indx) { const rindx = this.options.horder ? indx : nhists - indx - 1, hist = hlst.arr[rindx]; this.painters[indx].decodeOptions(this.getHistDrawOption(hist, hlst.opt[rindx])); } this.redrawPad(); }, 'Change draw erros in the stack'); } /** @summary draw THStack object */ static async draw(dom, stack, opt) { if (!stack.fHists || !stack.fHists.arr) return null; // drawing not needed const painter = new THStackPainter(dom, stack, opt); let pad_painter = null, skip_drawing = false; return ensureTCanvas(painter, false).then(() => { painter.decodeOptions(opt); painter.hdraw_func = (painter.options.ndim === 1) ? TH1Painter.draw : TH2Painter.draw; if (painter.options.pads) { pad_painter = painter.getPadPainter(); if (pad_painter.doingDraw() && pad_painter.pad?.fPrimitives && (pad_painter.pad.fPrimitives.arr.length > 1) && (pad_painter.pad.fPrimitives.arr.indexOf(stack) === 0)) { skip_drawing = true; console.log('special case with THStack with is already rendered - do nothing'); return; } pad_painter.cleanPrimitives(p => p !== painter); return pad_painter.divide(painter.options.nhist); } if (!painter.options.nostack) painter.options.nostack = !painter.buildStack(stack); if (painter.options.same) return; const no_histogram = !stack.fHistogram; if (no_histogram) stack.fHistogram = painter.createHistogram(stack); const mm = painter.getMinMax(painter.options.errors || painter.options.draw_errors), hopt = painter.options.hopt + ';' + mm.hopt; return painter.hdraw_func(dom, stack.fHistogram, hopt).then(subp => { painter.addToPadPrimitives(); painter.firstpainter = subp; subp.setSecondaryId(painter, 'hist'); // mark hist painter as created by hstack }); }).then(() => skip_drawing ? painter : painter.drawNextHisto(0, pad_painter)); } } // class THStackPainter export { THStackPainter };