UNPKG

jsroot

Version:
567 lines (460 loc) 18.2 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 { #firstpainter; // first painter on stack #painters; // array of sub-painters #stack; // internal stack of histograms #did_update; // flag used in update /** @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); }); this.#firstpainter = null; this.#painters = []; this.#stack = undefined; super.cleanup(); } /** @summary Build sum of all histograms * @desc Build a separate #stack containing the running sum of all histograms */ buildStack(stack, pp) { this.#stack = 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.#stack = 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.#stack = arr; return true; } /** @summary Returns stack min/max values */ getMinMax(iserr) { const stack = this.getObject(), o = this.getOptions(), pad = this.getPadPainter()?.getRootPad(true), logscale = pad?.fLogv ?? (o.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 (o.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.#stack.arr.at(0), iserr).min; themax = getHistMinMax(this.#stack.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 (!o.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 (!o.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) { const o = this.getOptions(); let hopt = opt || hist.fOption || o.hopt; if (hopt.toUpperCase().indexOf(o.hopt) < 0) hopt += ' ' + o.hopt; if (o.draw_errors && !hopt) hopt = 'E'; if (o.zscale) { const p = hopt.toUpperCase().indexOf('COLZ'); if (p >= 0) hopt = hopt.slice(0, p + 3) + hopt.slice(p + 4); } if (!o.pads) hopt += ' same nostat' + o.auto; return hopt; } /** @summary Draw next stack histogram */ async drawNextHisto(indx, pad_painter) { const stack = this.getObject(), o = this.getOptions(), hlst = o.nostack ? stack.fHists : this.#stack, nhists = hlst?.arr?.length || 0; if (indx >= nhists) return this; const rindx = o.horder ? indx : nhists - indx - 1, subid = o.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) && !o.nostack) hist.$baseh = hlst.arr[rindx - 1]; // this number used for auto colors creation if (o.auto) hist.$num_histos = nhists; const dom = this.#firstpainter?.getPadPainter() || this.getDrawDom(); return this.drawHist(dom, 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) { const o = this.setOptions({ ndim: 1, nostack: false, same: false, horder: true, has_errors: false, draw_errors: false, hopt: '', auto: '' }), stack = this.getObject(), hist = stack.fHistogram || stack.fHists?.arr[0] || this.#stack?.arr[0]; if (hist?._typename.indexOf(clTH2) === 0) o.ndim = 2; if ((o.ndim === 2) && !opt) opt = 'lego1'; if (!o.nostack) { stack.fHists?.arr.forEach(h => { const len = h.fSumw2?.length ?? 0; for (let n = 0; n < len; ++n) { if (h.fSumw2[n] > 0) { o.has_errors = true; break; } } }); } o.nhist = stack.fHists?.arr?.length ?? 1; const d = new DrawOptions(opt); o.nostack = d.check('NOSTACK'); if (d.check('STACK')) o.nostack = false; o.same = d.check('SAME'); d.check('NOCLEAR'); // ignore option ['PFC', 'PLC', 'PMC'].forEach(f => { if (d.check(f)) o.auto += ' ' + f; }); o.pads = d.check('PADS'); if (o.pads) o.nostack = true; o.hopt = d.remain().trim(); // use remaining draw options for histogram draw const dolego = d.check('LEGO'); o.errors = d.check('E'); o.zscale = d.check('COLZ'); // if any histogram appears with pre-calculated errors, use E for all histograms if (!o.nostack && o.has_errors && !dolego && !d.check('HIST') && (o.hopt.indexOf('E') < 0)) o.draw_errors = true; o.horder = o.nostack || dolego; } /** @summary Create main histogram for THStack axis drawing */ createHistogram(stack) { const o = this.getOptions(), histos = stack.fHists, numhistos = 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((o.ndim === 1) ? clTH1F : clTH2F, h0.fXaxis.fNbins, h0.fYaxis.fNbins); histo.fName = 'axis_hist'; histo.fBits |= kNoStats; Object.assign(histo.fXaxis, h0.fXaxis); if (o.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 ((o.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(), o = this.getOptions(); stack.fHists = obj.fHists; stack.fTitle = obj.fTitle; stack.fMinimum = obj.fMinimum; stack.fMaximum = obj.fMaximum; if (!o.nostack) o.nostack = !this.buildStack(stack, pp); if (this.#firstpainter) { let src = obj.fHistogram; if (!src) src = stack.fHistogram = this.createHistogram(stack); const mm = this.getMinMax(o.errors || o.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 (o.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 = o.nostack ? stack.fHists : this.#stack, 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 = o.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; this.#did_update = undefined; let pr = Promise.resolve(this); const o = this.getOptions(); if (this.#firstpainter) { const mm = this.getMinMax(o.errors || o.draw_errors); this.#firstpainter.decodeOptions(o.hopt + mm.hopt); pr = this.#firstpainter.redraw(reason); } return pr.then(() => { if (full_redraw) return this.drawNextHisto(0, o.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) { const o = this.getOptions(); menu.addRedrawMenu(this); if (!o.pads) { menu.addchk(o.draw_errors, 'Draw errors', flag => { o.draw_errors = flag; const stack = this.getObject(), hlst = o.nostack ? stack.fHists : this.#stack, nhists = hlst?.arr?.length ?? 0; for (let indx = 0; indx < nhists; ++indx) { const rindx = o.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.getOptions().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(), o = this.getOptions(); if (!skip_cleanup && pp) { this.#firstpainter = null; this.#painters = []; if (o.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 (o.pads) { pr = ensureTCanvas(this, false).then(() => { pad_painter = this.getPadPainter(); return pad_painter.divide(o.nhist, 0, true); }); } else { if (!o.nostack) o.nostack = !this.buildStack(stack, pp); if (!o.same && stack.fHists?.arr.length) { if (!stack.fHistogram) stack.fHistogram = this.createHistogram(stack); const mm = this.getMinMax(o.errors || o.draw_errors); pr = this.drawHist(this.getDrawDom(), stack.fHistogram, o.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 (!o.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 };