UNPKG

jsroot

Version:
384 lines (313 loc) 11.6 kB
import { settings, gStyle, isStr, isFunc, clTH1D, createHistogram, setHistogramTitle, clTF1, clTF12, kNoStats } from '../core.mjs'; import { floatToString } from '../base/BasePainter.mjs'; import { getElementPadPainter, getElementMainPainter } from '../base/ObjectPainter.mjs'; import { THistPainter } from '../hist2d/THistPainter.mjs'; import { TH1Painter } from '../hist2d/TH1Painter.mjs'; import { proivdeEvalPar, _getTF1Save } from '../base/func.mjs'; /** @summary Create log scale for axis bins * @private */ function produceTAxisLogScale(axis, num, min, max) { let lmin, lmax; if (max > 0) { lmax = Math.log(max); lmin = min > 0 ? Math.log(min) : lmax - 5; } else { lmax = -10; lmin = -15; } axis.fNbins = num; axis.fXbins = new Array(num + 1); for (let i = 0; i <= num; ++i) axis.fXbins[i] = Math.exp(lmin + i / num * (lmax - lmin)); axis.fXmin = Math.exp(lmin); axis.fXmax = Math.exp(lmax); } function scanTF1Options(opt) { if (!isStr(opt)) opt = ''; let p = opt.indexOf(';webcanv_hist'), _webcanv_hist = false, _use_saved = 0; if (p >= 0) { _webcanv_hist = true; opt = opt.slice(0, p); } p = opt.indexOf(';force_saved'); if (p >= 0) { _use_saved = 2; opt = opt.slice(0, p); } p = opt.indexOf(';prefer_saved'); if (p >= 0) { _use_saved = 1; opt = opt.slice(0, p); } return { opt, _webcanv_hist, _use_saved }; } /** * @summary Painter for TF1 object * * @private */ class TF1Painter extends TH1Painter { #use_saved_points; // use saved points for drawing #func; // func object #tmp_tooltip; // temporary tooltip #fail_eval; // fail evaluation of function /** @summary Assign function */ setFunc(f) { this.#func = f; } /** @summary Returns drawn object name */ getObjectName() { return this.#func?.fName ?? 'func'; } /** @summary Returns drawn object class name */ getClassName() { return this.#func?._typename ?? clTF1; } /** @summary Returns true while function is drawn */ isTF1() { return true; } isTF12() { return this.getClassName() === clTF12; } /** @summary Returns primary function which was then drawn as histogram */ getPrimaryObject() { return this.#func; } /** @summary Update function */ updateObject(obj /* , opt */) { if (!obj || (this.getClassName() !== obj._typename)) return false; delete obj.evalPar; const histo = this.getHisto(); if (this._webcanv_hist) { const h0 = this.getPadPainter()?.findInPrimitives('Func', clTH1D); if (h0) this.updateAxes(histo, h0, this.getFramePainter()); } this.setFunc(obj); this.createTF1Histogram(obj, histo); this.scanContent(); return true; } /** @summary Redraw TF1 * @private */ redraw(reason) { if (!this.#use_saved_points && (reason === 'logx' || reason === 'zoom')) { this.createTF1Histogram(this.#func, this.getHisto()); this.scanContent(); } return super.redraw(reason); } /** @summary Create histogram for TF1 drawing * @private */ createTF1Histogram(tf1, hist) { const fp = this.getFramePainter(), pad = this.getPadPainter()?.getRootPad(true), logx = pad?.fLogx, gr = fp?.getGrFuncs(this.second_x, this.second_y); let xmin = tf1.fXmin, xmax = tf1.fXmax, np = Math.max(tf1.fNpx, 100); if (gr?.zoom_xmin !== gr?.zoom_xmax) { const dx = (xmax - xmin) / np; if ((xmin < gr.zoom_xmin) && (gr.zoom_xmin < xmax)) xmin = Math.max(xmin, gr.zoom_xmin - dx); if ((xmin < gr.zoom_xmax) && (gr.zoom_xmax < xmax)) xmax = Math.min(xmax, gr.zoom_xmax + dx); } this.#use_saved_points = (tf1.fSave.length > 3) && (settings.PreferSavedPoints || (this._use_saved > 1)); const ensureBins = num => { if (hist.fNcells !== num + 2) { hist.fNcells = num + 2; hist.fArray = new Float32Array(hist.fNcells); } hist.fArray.fill(0); hist.fXaxis.fNbins = num; hist.fXaxis.fXbins = []; }; this.#fail_eval = undefined; // this.#use_saved_points = true; if (!this.#use_saved_points) { let iserror = false; if (!tf1.evalPar) { try { if (this.isTF12()) { if (proivdeEvalPar(tf1.fF2)) { tf1.evalPar = function(x) { return this.fCase ? this.fF2.evalPar(x, this.fXY) : this.fF2.evalPar(this.fXY, x); }; } else iserror = true; } else if (!proivdeEvalPar(tf1)) iserror = true; } catch { iserror = true; } } ensureBins(np); if (logx) produceTAxisLogScale(hist.fXaxis, np, xmin, xmax); else { hist.fXaxis.fXmin = xmin; hist.fXaxis.fXmax = xmax; } for (let n = 0; (n < np) && !iserror; n++) { const x = hist.fXaxis.GetBinCenter(n + 1); let y = 0; try { y = tf1.evalPar(x); } catch { iserror = true; } if (!iserror) hist.setBinContent(n + 1, Number.isFinite(y) ? y : 0); } if (iserror) this.#fail_eval = true; if (iserror && (tf1.fSave.length > 3)) this.#use_saved_points = true; } // in the case there were points have saved and we cannot calculate function // if we don't have the user's function if (this.#use_saved_points) { np = tf1.fSave.length - 3; let custom_xaxis = null; xmin = tf1.fSave[np + 1]; xmax = tf1.fSave[np + 2]; if (xmin === xmax) { const mp = this.getMainPainter(); if (isFunc(mp?.getHisto)) custom_xaxis = mp?.getHisto()?.fXaxis; } if (custom_xaxis) { ensureBins(hist.fXaxis.fNbins); Object.assign(hist.fXaxis, custom_xaxis); // TODO: find first bin for (let n = 0; n < np; ++n) { const y = tf1.fSave[n]; hist.setBinContent(n + 1, Number.isFinite(y) ? y : 0); } } else { ensureBins(tf1.fNpx); hist.fXaxis.fXmin = tf1.fXmin; hist.fXaxis.fXmax = tf1.fXmax; for (let n = 0; n < tf1.fNpx; ++n) { const y = _getTF1Save(tf1, hist.fXaxis.GetBinCenter(n + 1)); hist.setBinContent(n + 1, Number.isFinite(y) ? y : 0); } } } hist.fName = 'Func'; setHistogramTitle(hist, tf1.fTitle); hist.fMinimum = tf1.fMinimum; hist.fMaximum = tf1.fMaximum; hist.fLineColor = tf1.fLineColor; hist.fLineStyle = tf1.fLineStyle; hist.fLineWidth = tf1.fLineWidth; hist.fFillColor = tf1.fFillColor; hist.fFillStyle = tf1.fFillStyle; hist.fMarkerColor = tf1.fMarkerColor; hist.fMarkerStyle = tf1.fMarkerStyle; hist.fMarkerSize = tf1.fMarkerSize; hist.fBits |= kNoStats; } /** @summary Extract function ranges */ extractAxesProperties(ndim) { super.extractAxesProperties(ndim); const func = this.#func, nsave = func?.fSave.length ?? 0; if (nsave > 3 && this.#use_saved_points) { this.xmin = Math.min(this.xmin, func.fSave[nsave - 2]); this.xmax = Math.max(this.xmax, func.fSave[nsave - 1]); } if (func) { this.xmin = Math.min(this.xmin, func.fXmin); this.xmax = Math.max(this.xmax, func.fXmax); } } /** @summary Checks if it makes sense to zoom inside specified axis range */ canZoomInside(axis, min, max) { const nsave = this.#func?.fSave.length ?? 0; if ((nsave > 3) && this.#use_saved_points && (axis === 'x')) { // in the case where the points have been saved, useful for example // if we don't have the user's function const nb_points = nsave - 2, xmin = this.#func.fSave[nsave - 2], xmax = this.#func.fSave[nsave - 1]; return Math.abs(xmax - xmin) / nb_points < Math.abs(max - min); } // if function calculated, one always could zoom inside return (axis === 'x') || (axis === 'y'); } /** @summary return tooltips for TF2 */ getTF1Tooltips(pnt) { this.#tmp_tooltip = undefined; const lines = [this.getObjectHint()], o = this.getOptions(), funcs = this.getFramePainter()?.getGrFuncs(o.second_x, o.second_y); if (!funcs || !isFunc(this.#func?.evalPar)) { lines.push('grx = ' + pnt.x, 'gry = ' + pnt.y); return lines; } const x = funcs.revertAxis('x', pnt.x); let y = 0, gry = 0, iserror = false; try { y = this.#func.evalPar(x); gry = Math.round(funcs.gry(y)); } catch { iserror = true; } lines.push('x = ' + funcs.axisAsText('x', x), 'value = ' + (iserror ? '<fail>' : floatToString(y, gStyle.fStatFormat))); if (!iserror) this.#tmp_tooltip = { y, gry }; return lines; } /** @summary process tooltip event for TF1 object */ processTooltipEvent(pnt) { if (this.#use_saved_points) return super.processTooltipEvent(pnt); let ttrect = this.getG()?.selectChild('.tooltip_bin'); if (!this.getG() || !pnt) { ttrect?.remove(); return null; } const res = { name: this.#func?.fName, title: this.#func?.fTitle, x: pnt.x, y: pnt.y, color1: this.lineatt?.color ?? 'green', color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue', lines: this.getTF1Tooltips(pnt), exact: true, menu: true }; if (pnt.disabled) ttrect.remove(); else { if (ttrect.empty()) { ttrect = this.getG().append('svg:circle') .attr('class', 'tooltip_bin') .style('pointer-events', 'none') .style('fill', 'none') .attr('r', (this.lineatt?.width ?? 1) + 4); } ttrect.attr('cx', pnt.x) .attr('cy', this.#tmp_tooltip?.gry ?? pnt.y); if (this.lineatt) ttrect.call(this.lineatt.func); } return res; } /** @summary fill information for TWebCanvas * @desc Used to inform web canvas when evaluation failed * @private */ fillWebObjectOptions(opt) { opt.fcust = this.#fail_eval && !this._use_saved ? 'func_fail' : ''; } /** @summary draw TF1 object */ static async draw(dom, tf1, opt) { const web = scanTF1Options(opt); opt = web.opt; delete web.opt; let hist; if (web._webcanv_hist) hist = getElementPadPainter(dom)?.findInPrimitives('Func', clTH1D); if (!hist) { hist = createHistogram(clTH1D, 100); hist.fBits |= kNoStats; } if (!opt && getElementMainPainter(dom)) opt = 'same'; const painter = new TF1Painter(dom, hist); painter.setFunc(tf1); Object.assign(painter, web); painter.createTF1Histogram(tf1, hist); return THistPainter._drawHist(painter, opt); } } // class TF1Painter export { TF1Painter, produceTAxisLogScale, scanTF1Options };