UNPKG

jsroot

Version:
370 lines (314 loc) 12.1 kB
import { create, createHistogram, clTH1F, clTH2F, clTObjString, clTHashList, kNoZoom, kNoStats, BIT } from '../core.mjs'; import { DrawOptions } from '../base/BasePainter.mjs'; import { ObjectPainter } from '../base/ObjectPainter.mjs'; import { FunctionsHandler } from './THistPainter.mjs'; import { TH1Painter, PadDrawOptions } from './TH1Painter.mjs'; import { TGraphPainter } from './TGraphPainter.mjs'; import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs'; const kResetHisto = BIT(17); /** * @summary Painter for TMultiGraph object. * * @private */ class TMultiGraphPainter extends ObjectPainter { #firstpainter; // first painter #painters; // array of sub-painters #funcs_handler; // special instance for functions drawing #restopt; // remaining part of draw options #auto; // extra options for auto colors #is3d; // if 3d drawing #pads; // pads draw option /** @summary Create painter * @param {object|string} dom - DOM element for drawing or element id * @param {object} obj - TMultiGraph object to draw */ constructor(dom, mgraph) { super(dom, mgraph); this.#firstpainter = null; this.#painters = []; // keep painters to be able update objects } /** @summary Cleanup TMultiGraph painter */ cleanup() { this.#painters = []; this.#is3d = undefined; this.#pads = undefined; this.#auto = undefined; this.#restopt = undefined; super.cleanup(); } /** @summary Return true if 3D drawing is used */ is3d() { return this.#is3d; } /** @summary Update TMultiGraph object */ updateObject(obj) { if (!this.matchObjectType(obj)) return false; const mgraph = this.getObject(), graphs = obj.fGraphs, pp = this.getPadPainter(); mgraph.fTitle = obj.fTitle; let isany = false; if (this.#firstpainter) { const histo = this.scanGraphsRange(graphs, obj.fHistogram, pp?.getRootPad(true), true); if (this.#firstpainter.updateObject(histo)) isany = true; } const ngr = Math.min(graphs.arr.length, this.#painters.length); // TODO: handle changing number of graphs for (let i = 0; i < ngr; ++i) { if (this.#painters[i].updateObject(graphs.arr[i], (graphs.opt[i] || this.#restopt) + this.#auto)) isany = true; } this.#funcs_handler = new FunctionsHandler(this, pp, obj.fFunctions); return isany; } /** @summary Redraw TMultiGraph * @desc may redraw histogram which was used to draw axes * @return {Promise} for ready */ async redraw(reason) { const promise = this.#firstpainter?.redraw(reason) ?? Promise.resolve(true), redrawNext = async indx => { if (indx >= this.#painters.length) return this; return this.#painters[indx].redraw(reason).then(() => redrawNext(indx + 1)); }; return promise.then(() => redrawNext(0)).then(() => { const res = this.#funcs_handler?.drawNext(0) ?? this; this.#funcs_handler = undefined; return res; }); } /** @summary Scan graphs range * @return {object} histogram for axes drawing */ scanGraphsRange(graphs, histo, pad, reset_histo) { const mgraph = this.getObject(), rw = { xmin: 0, xmax: 0, ymin: 0, ymax: 0, first: true }, test = (v1, v2) => { return Math.abs(v2 - v1) < 1e-6; }; let maximum, minimum, logx = false, logy = false, src_hist, dummy_histo = false; if (pad) { logx = pad.fLogx; logy = pad.fLogv ?? pad.fLogy; } // ignore existing histogram in 3d case if (this.is3d() && histo && !histo.fXaxis.fLabels) histo = null; if (!histo) src_hist = graphs.arr[0]?.fHistogram; else { dummy_histo = test(histo.fMinimum, -0.05) && test(histo.fMaximum, 1.05) && test(histo.fXaxis.fXmin, -0.05) && test(histo.fXaxis.fXmax, 1.05); src_hist = histo; } graphs.arr.forEach(gr => { if (gr.fNpoints === 0) return; if (gr.TestBit(kResetHisto)) reset_histo = true; if (rw.first) { rw.xmin = rw.xmax = gr.fX[0]; rw.ymin = rw.ymax = gr.fY[0]; rw.first = false; } for (let i = 0; i < gr.fNpoints; ++i) { rw.xmin = Math.min(rw.xmin, gr.fX[i]); rw.xmax = Math.max(rw.xmax, gr.fX[i]); rw.ymin = Math.min(rw.ymin, gr.fY[i]); rw.ymax = Math.max(rw.ymax, gr.fY[i]); } }); if (rw.xmin === rw.xmax) rw.xmax += 1; if (rw.ymin === rw.ymax) rw.ymax += 1; const dx = 0.05 * (rw.xmax - rw.xmin), dy = 0.05 * (rw.ymax - rw.ymin); let uxmin = rw.xmin - dx, uxmax = rw.xmax + dx; if (logy) { if (rw.ymin <= 0) rw.ymin = 0.001 * rw.ymax; minimum = rw.ymin / (1 + 0.5 * Math.log10(rw.ymax / rw.ymin)); maximum = rw.ymax * (1 + 0.2 * Math.log10(rw.ymax / rw.ymin)); } else { minimum = rw.ymin - dy; maximum = rw.ymax + dy; } if (minimum < 0 && rw.ymin >= 0) minimum = 0; if (maximum > 0 && rw.ymax <= 0) maximum = 0; const glob_minimum = minimum, glob_maximum = maximum; if (uxmin < 0 && rw.xmin >= 0) uxmin = logx ? 0.9 * rw.xmin : 0; if (uxmax > 0 && rw.xmax <= 0) uxmax = logx ? 1.1 * rw.xmax : 0; if (mgraph.fMinimum !== kNoZoom) rw.ymin = minimum = mgraph.fMinimum; if (mgraph.fMaximum !== kNoZoom) rw.ymax = maximum = mgraph.fMaximum; if (minimum < 0 && rw.ymin >= 0 && logy) minimum = 0.9 * rw.ymin; if (maximum > 0 && rw.ymax <= 0 && logy) maximum = 1.1 * rw.ymax; if (minimum <= 0 && logy) minimum = 0.001 * maximum; if (!logy && minimum > 0 && minimum < 0.05 * maximum) minimum = 0; if (uxmin <= 0 && logx) uxmin = (uxmax > 1000) ? 1 : 0.001 * uxmax; // Create a temporary histogram to draw the axis (if necessary) if (!histo || reset_histo || dummy_histo) { let xaxis, yaxis; if (this.is3d()) { histo = createHistogram(clTH2F, graphs.arr.length, 10); xaxis = histo.fXaxis; xaxis.fXmin = 0; xaxis.fXmax = graphs.arr.length; xaxis.fLabels = create(clTHashList); for (let i = 0; i < graphs.arr.length; i++) { const lbl = create(clTObjString); lbl.fString = graphs.arr[i].fTitle || `gr${i}`; lbl.fUniqueID = graphs.arr.length - i; // graphs drawn in reverse order xaxis.fLabels.Add(lbl, ''); } xaxis = histo.fYaxis; yaxis = histo.fZaxis; } else { histo = createHistogram(src_hist?._typename ?? clTH1F, src_hist?.fXaxis.fNbins ?? 10); xaxis = histo.fXaxis; yaxis = histo.fYaxis; } if (src_hist) { Object.assign(xaxis, src_hist.fXaxis); yaxis.fTitle = src_hist.fYaxis.fTitle; } histo.fTitle = mgraph.fTitle; if (histo.fTitle.indexOf(';') >= 0) { const t = histo.fTitle.split(';'); histo.fTitle = t[0]; if (t[1]) xaxis.fTitle = t[1]; if (t[2]) yaxis.fTitle = t[2]; } if (!xaxis.fLabels) { xaxis.fXmin = uxmin; xaxis.fXmax = uxmax; } } const axis = this.is3d() ? histo.fZaxis : histo.fYaxis; axis.fXmin = Math.min(minimum, glob_minimum); axis.fXmax = Math.max(maximum, glob_maximum); if (histo.fMinimum === kNoZoom) histo.fMinimum = minimum; if (histo.fMaximum === kNoZoom) histo.fMaximum = maximum; histo.fBits |= kNoStats; return histo; } /** @summary draw special histogram for axis * @return {Promise} when ready */ async drawAxisHist(histo, hopt) { return TH1Painter.draw(this.getDrawDom(), histo, hopt); } /** @summary Draw graph */ async drawGraph(dom, gr, opt /* , pos3d */) { return TGraphPainter.draw(dom, gr, opt); } /** @summary method draws next graph */ async drawNextGraph(indx, pad_painter) { const graphs = this.getObject().fGraphs; // at the end of graphs drawing draw functions (if any) if (indx >= graphs.arr.length) return this; const gr = graphs.arr[indx], draw_opt = (graphs.opt[indx] || this.#restopt) + this.#auto, pos3d = graphs.arr.length - indx, subid = `graphs_${indx}`; // 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.drawGraph(subpad_painter, gr, draw_opt, pos3d).then(subp => { if (subp) { subp.setSecondaryId(this, subid); this.#painters.push(subp); } return this.drawNextGraph(indx + 1, pad_painter); }); } // used in automatic colors numbering if (this.#auto) gr.$num_graphs = graphs.arr.length; return this.drawGraph(this.getPadPainter(), gr, draw_opt, pos3d).then(subp => { if (subp) { subp.setSecondaryId(this, subid); this.#painters.push(subp); } return this.drawNextGraph(indx + 1); }); } /** @summary Fill TMultiGraph context menu */ fillContextMenuItems(menu) { menu.addRedrawMenu(this); } /** @summary Redraw TMultiGraph object using provided option * @private */ async redrawWith(opt, skip_cleanup) { if (!skip_cleanup) { this.#firstpainter = null; this.#painters = []; const pp = this.getPadPainter(); pp?.removePrimitive(this, true); if (this.#pads) pp?.divide(0, 0); } const d = new DrawOptions(opt), mgraph = this.getObject(); this.#is3d = d.check('3D'); this.#auto = ''; this.#pads = d.check('PADS'); ['PFC', 'PLC', 'PMC'].forEach(f => { if (d.check(f)) this.#auto += ' ' + f; }); let hopt = '', pad_painter = null; if (d.check('FB') && this.is3d()) hopt += 'FB'; // will be directly combined with LEGO PadDrawOptions.forEach(name => { if (d.check(name)) hopt += ';' + name; }); this.#restopt = d.remain(); let promise = Promise.resolve(true); if (this.#pads) { promise = ensureTCanvas(this, false).then(() => { pad_painter = this.getPadPainter(); return pad_painter.divide(mgraph.fGraphs.arr.length, 0, true); }); } else if (d.check('A') || !this.getMainPainter()) { const histo = this.scanGraphsRange(mgraph.fGraphs, mgraph.fHistogram, this.getPadPainter()?.getRootPad(true)); promise = this.drawAxisHist(histo, hopt).then(ap => { ap.setSecondaryId(this, 'hist'); // mark that axis painter generated from mg this.#firstpainter = ap; }); } return promise.then(() => { this.addToPadPrimitives(); return this.drawNextGraph(0, pad_painter); }).then(() => { if (this.#pads) return this; const handler = new FunctionsHandler(this, this.getPadPainter(), this.getObject().fFunctions, true); return handler.drawNext(0); // returns painter }); } /** @summary Draw TMultiGraph object in 2D only */ static async draw(dom, mgraph, opt) { const painter = new TMultiGraphPainter(dom, mgraph, opt); return painter.redrawWith(opt, true); } } // class TMultiGraphPainter export { TMultiGraphPainter };