UNPKG

jsroot

Version:
547 lines (442 loc) 18.4 kB
import { clone, create, createHistogram, setHistogramTitle, BIT, gStyle, clTH1F, clTH2, clTH2F, clTObjArray, 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'; const kIsZoomed = BIT(16); // bit set when zooming on Y axis /** * @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, pp) { this.fStack = null; if (!stack.fHists) return false; const nhists = stack.fHists.arr.length; if (nhists <= 0) return false; let arr = pp?.findInPrimitives(undefined, clTObjArray); if ((arr?.arr.length === nhists) && (arr?.name === stack.fName)) { this.fStack = arr; return true; } arr = create(clTObjArray); let hprev = clone(stack.fHists.arr[0]); arr.arr.push(hprev); for (let i = 1; i < nhists; ++i) { const hnext = clone(stack.fHists.arr[i]), 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 arr2 = new Array(hprev.fNcells).fill(0); for (let n = 1; n <= xnext.fNbins; ++n) arr2[n] = hnext.fArray[n]; hnext.fNcells = hprev.fNcells; Object.assign(xnext, xprev); hnext.fArray = arr2; match = true; } if (!match) { console.warn(`When drawing THStack, cannot sum-up histograms ${hnext.fName} and ${hprev.fName}`); return false; } // trivial sum of histograms for (let n = 0; n < hnext.fArray.length; ++n) hnext.fArray[n] += hprev.fArray[n]; arr.arr.push(hnext); hprev = hnext; } this.fStack = arr; return true; } /** @summary Returns stack min/max values */ getMinMax(iserr) { const stack = this.getObject(), pad = this.getPadPainter()?.getRootPad(true), logscale = pad?.fLogv ?? (this.options.ndim === 1 ? pad?.fLogy : pad?.fLogz); let themin = 0, themax = 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; } } let err = 0; for (let j = j1; j <= j2; ++j) { for (let i = i1; i <= i2; ++i) { const val = hist.getBinContent(i, j); if (witherr) err = hist.getBinError(hist.getBin(i, j)); if (logscale && (val - err <= 0)) continue; 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) { themin = resh.min; themax = resh.max; } else { themin = Math.min(themin, resh.min); themax = Math.max(themax, resh.max); } } } else { themin = getHistMinMax(this.fStack.arr.at(0), iserr).min; themax = getHistMinMax(this.fStack.arr.at(-1), iserr).max; } if (logscale) themin = (themin > 0) ? themin*0.9 : themax*1e-3; else if (themin > 0) themin = 0; if (stack.fMaximum !== kNoZoom) themax = stack.fMaximum; if (stack.fMinimum !== kNoZoom) themin = stack.fMinimum; // redo code from THStack::BuildAndPaint if (!this.options.nostack || (stack.fMaximum === kNoZoom)) { if (logscale) { if (themin > 0) themax *= (1+0.2*Math.log10(themax/themin)); } else if (stack.fMaximum === kNoZoom) themax *= (1 + gStyle.fHistTopMargin); } if (!this.options.nostack || (stack.fMinimum === kNoZoom)) { if (logscale) themin = (themin > 0) ? themin/(1+0.5*Math.log10(themax/themin)) : 1e-3*themax; } const res = { min: themin, max: themax, hopt: `;hmin:${themin};hmax:${themax}` }; if (stack.fHistogram?.TestBit(kIsZoomed)) res.hopt += ';zoom_min_max'; return res; } /** @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.zscale) { const p = hopt.toUpperCase().indexOf('COLZ'); if (p >= 0) hopt = hopt.slice(0, p + 3) + hopt.slice(p + 4); } 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 : this.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, stack.fHists.opt[rindx]); // handling of 'pads' draw option if (pad_painter) { const subpad_painter = pad_painter.getSubPadPainter(indx+1); if (!subpad_painter) return this; subpad_painter.cleanPrimitives(true); return this.drawHist(subpad_painter, hist, hopt).then(subp => { if (subp) { subp.setSecondaryId(this, subid); this.painters.push(subp); } return this.drawNextHisto(indx+1, pad_painter); }); } // special handling of stacked histograms // 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.drawHist(this.getPadPainter(), 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) || (this.fStack ? this.fStack.arr[0] : null), hasErrors = hist2 => { const len = hist2.fSumw2?.length ?? 0; for (let n = 0; n < len; ++n) { if (hist2.fSumw2[n] > 0) return true; } return false; }; if (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 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().trim(); // use remaining draw options for histogram draw const dolego = d.check('LEGO'); this.options.errors = d.check('E'); this.options.zscale = d.check('COLZ'); // 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(clTH1F, 100); setHistogramTitle(histo, stack.fTitle); histo.fBits |= kNoStats; return histo; } const h0 = histos.arr[0], histo = createHistogram((this.options.ndim === 1) ? clTH1F : clTH2F, 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(), pp = this.getPadPainter(); stack.fHists = obj.fHists; stack.fTitle = obj.fTitle; stack.fMinimum = obj.fMinimum; stack.fMaximum = obj.fMaximum; if (!this.options.nostack) this.options.nostack = !this.buildStack(stack, pp); 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.hmin = mm.min; this.firstpainter.options.hmax = mm.max; this.firstpainter._checked_zooming = false; // force to check 3d zooming if (this.options.ndim === 1) { this.firstpainter.ymin = mm.min; this.firstpainter.ymax = mm.max; } else { this.firstpainter.zmin = mm.min; this.firstpainter.zmax = mm.max; } this.firstpainter.updateObject(src); this.firstpainter.options.zoom_min_max = src.TestBit(kIsZoomed); } // and now update histograms const hlst = this.options.nostack ? stack.fHists : this.fStack, nhists = hlst?.arr?.length ?? 0; if (nhists !== this.painters.length) { this.did_update = 1; pp?.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, stack.fHists.opt[rindx])); } } return true; } /** @summary Redraw THStack * @desc Do something if previous update had changed number of histograms */ redraw(reason) { if (!this.did_update) return; const full_redraw = this.did_update === 1; delete this.did_update; let pr = Promise.resolve(this); if (this.firstpainter) { const mm = this.getMinMax(this.options.errors || this.options.draw_errors); this.firstpainter.decodeOptions(this.options.hopt + mm.hopt); pr = this.firstpainter.redraw(reason); } return pr.then(() => { if (full_redraw) return this.drawNextHisto(0, this.options.pads ? this.getPadPainter() : null); const redrawSub = indx => { if (indx >= this.painters.length) return this; return this.painters[indx].redraw(reason).then(() => redrawSub(indx+1)); }; return redrawSub(0); }); } /** @summary Fill THStack context menu */ fillContextMenuItems(menu) { menu.addRedrawMenu(this); if (!this.options.pads) { menu.addchk(this.options.draw_errors, 'Draw errors', flag => { this.options.draw_errors = flag; const stack = this.getObject(), hlst = this.options.nostack ? stack.fHists : this.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, stack.fHists.opt[rindx])); } this.redrawPad(); }, 'Change draw erros in the stack'); } } /** @summary Invoke histogram drawing */ drawHist(dom, hist, hopt) { const func = (this.options.ndim === 1) ? TH1Painter.draw : TH2Painter.draw; return func(dom, hist, hopt); } /** @summary Access or modify histogram min/max * @private */ accessMM(ismin, v) { const name = ismin ? 'fMinimum' : 'fMaximum', stack = this.getObject(); if (v === undefined) return stack[name]; this.did_update = 2; stack[name] = v; this.interactiveRedraw('pad', ismin ? `exec:SetMinimum(${v})` : `exec:SetMaximum(${v})`); } /** @summary Full stack redraw with specified draw option */ async redrawWith(opt, skip_cleanup) { const pp = this.getPadPainter(); if (!skip_cleanup && pp) { this.firstpainter = null; this.painters = []; if (this.options.pads) pp.divide(0, 0); pp.removePrimitive(this, true); } this.decodeOptions(opt); const stack = this.getObject(); let pr = Promise.resolve(this), pad_painter = null; if (this.options.pads) { pr = ensureTCanvas(this, false).then(() => { pad_painter = this.getPadPainter(); return pad_painter.divide(this.options.nhist, 0, true); }); } else { if (!this.options.nostack) this.options.nostack = !this.buildStack(stack, pp); if (!this.options.same && stack.fHists?.arr.length) { if (!stack.fHistogram) stack.fHistogram = this.createHistogram(stack); const mm = this.getMinMax(this.options.errors || this.options.draw_errors); pr = this.drawHist(this.getDrawDom(), stack.fHistogram, this.options.hopt + mm.hopt).then(subp => { this.firstpainter = subp; subp.$stack_hist = true; subp.setSecondaryId(this, 'hist'); // mark hist painter as created by THStack }); } } return pr.then(() => this.drawNextHisto(0, pad_painter)).then(() => { if (!this.options.pads) this.addToPadPrimitives(); return this; }); } /** @summary draw THStack object in 2D only */ static async draw(dom, stack, opt) { if (!stack.fHists || !stack.fHists.arr) return null; // drawing not needed const painter = new THStackPainter(dom, stack, opt); return painter.redrawWith(opt, true); } } // class THStackPainter export { THStackPainter };