UNPKG

jsroot

Version:
322 lines (255 loc) 10.1 kB
import { gStyle, clTH1I, kNoStats, createHistogram } from '../core.mjs'; import { DrawOptions, floatToString, buildSvgCurve } from '../base/BasePainter.mjs'; import { ObjectPainter } from '../base/ObjectPainter.mjs'; import { TH1Painter } from '../hist/TH1Painter.mjs'; /** * @summary Painter for TSpline objects. * * @private */ class TSplinePainter extends ObjectPainter { /** @summary Update TSpline object * @private */ updateObject(obj, opt) { const spline = this.getObject(); if (spline._typename !== obj._typename) return false; if (spline !== obj) Object.assign(spline, obj); if (opt !== undefined) this.decodeOptions(opt); return true; } /** @summary Evaluate spline at given position * @private */ eval(knot, x) { const dx = x - knot.fX; if (knot._typename === 'TSplinePoly3') return knot.fY + dx*(knot.fB + dx*(knot.fC + dx*knot.fD)); if (knot._typename === 'TSplinePoly5') return knot.fY + dx*(knot.fB + dx*(knot.fC + dx*(knot.fD + dx*(knot.fE + dx*knot.fF)))); return knot.fY + dx; } /** @summary Find idex for x value * @private */ findX(x) { const spline = this.getObject(); let klow = 0, khig = spline.fNp - 1; if (x <= spline.fXmin) return 0; if (x >= spline.fXmax) return khig; if (spline.fKstep) { // Equidistant knots, use histogram klow = Math.round((x - spline.fXmin)/spline.fDelta); // Correction for rounding errors if (x < spline.fPoly[klow].fX) klow = Math.max(klow-1, 0); else if (klow < khig) if (x > spline.fPoly[klow+1].fX) ++klow; } else { // Non equidistant knots, binary search while (khig - klow > 1) { const khalf = Math.round((klow + khig)/2); if (x > spline.fPoly[khalf].fX) klow = khalf; else khig = khalf; } } return klow; } /** @summary Create histogram for axes drawing * @private */ createDummyHisto() { const spline = this.getObject(); let xmin = 0, xmax = 1, ymin = 0, ymax = 1; if (spline.fPoly) { xmin = xmax = spline.fPoly[0].fX; ymin = ymax = spline.fPoly[0].fY; spline.fPoly.forEach(knot => { xmin = Math.min(knot.fX, xmin); xmax = Math.max(knot.fX, xmax); ymin = Math.min(knot.fY, ymin); ymax = Math.max(knot.fY, ymax); }); if (ymax > 0) ymax *= (1 + gStyle.fHistTopMargin); if (ymin < 0) ymin *= (1 + gStyle.fHistTopMargin); } const histo = createHistogram(clTH1I, 10); histo.fName = spline.fName + '_hist'; histo.fTitle = spline.fTitle; histo.fBits |= kNoStats; histo.fXaxis.fXmin = xmin; histo.fXaxis.fXmax = xmax; histo.fYaxis.fXmin = ymin; histo.fYaxis.fXmax = ymax; histo.fMinimum = ymin; histo.fMaximum = ymax; return histo; } /** @summary Process tooltip event * @private */ processTooltipEvent(pnt) { const spline = this.getObject(), funcs = this.getFramePainter()?.getGrFuncs(this.options.second_x, this.options.second_y); let cleanup = false, xx, yy, knot = null, indx = 0; if ((pnt === null) || !spline || !funcs) cleanup = true; else { xx = funcs.revertAxis('x', pnt.x); indx = this.findX(xx); knot = spline.fPoly[indx]; yy = this.eval(knot, xx); if ((indx < spline.fN-1) && (Math.abs(spline.fPoly[indx+1].fX-xx) < Math.abs(xx-knot.fX))) knot = spline.fPoly[++indx]; if (Math.abs(funcs.grx(knot.fX) - pnt.x) < 0.5*this.knot_size) { xx = knot.fX; yy = knot.fY; } else { knot = null; if ((xx < spline.fXmin) || (xx > spline.fXmax)) cleanup = true; } } let gbin = this.draw_g?.selectChild('.tooltip_bin'); const radius = this.lineatt.width + 3; if (cleanup || !this.draw_g) { gbin?.remove(); return null; } if (gbin.empty()) { gbin = this.draw_g.append('svg:circle') .attr('class', 'tooltip_bin') .style('pointer-events', 'none') .attr('r', radius) .style('fill', 'none') .call(this.lineatt.func); } const res = { name: this.getObject().fName, title: this.getObject().fTitle, x: funcs.grx(xx), y: funcs.gry(yy), color1: this.lineatt.color, lines: [], exact: (knot !== null) || (Math.abs(funcs.gry(yy) - pnt.y) < radius) }; res.changed = gbin.property('current_xx') !== xx; res.menu = res.exact; res.menu_dist = Math.sqrt((res.x-pnt.x)**2 + (res.y-pnt.y)**2); if (res.changed) { gbin.attr('cx', Math.round(res.x)) .attr('cy', Math.round(res.y)) .property('current_xx', xx); } const name = this.getObjectHint(); if (name) res.lines.push(name); res.lines.push(`x = ${funcs.axisAsText('x', xx)}`, `y = ${funcs.axisAsText('y', yy)}`); if (knot !== null) { res.lines.push(`knot = ${indx}`, `B = ${floatToString(knot.fB, gStyle.fStatFormat)}`, `C = ${floatToString(knot.fC, gStyle.fStatFormat)}`, `D = ${floatToString(knot.fD, gStyle.fStatFormat)}`); if ((knot.fE !== undefined) && (knot.fF !== undefined)) { res.lines.push(`E = ${floatToString(knot.fE, gStyle.fStatFormat)}`, `F = ${floatToString(knot.fF, gStyle.fStatFormat)}`); } } return res; } /** @summary Redraw object * @private */ redraw() { const spline = this.getObject(), pmain = this.getFramePainter(), funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y), w = pmain.getFrameWidth(), h = pmain.getFrameHeight(); this.createG(true); this.knot_size = 5; // used in tooltip handling this.createAttLine({ attr: spline }); if (this.options.Line || this.options.Curve) { const npx = Math.max(10, spline.fNpx), bins = []; // index of current knot let xmin = Math.max(funcs.scale_xmin, spline.fXmin), xmax = Math.min(funcs.scale_xmax, spline.fXmax), indx = this.findX(xmin); if (pmain.logx) { xmin = Math.log(xmin); xmax = Math.log(xmax); } for (let n = 0; n < npx; ++n) { let x = xmin + (xmax-xmin)/npx*(n-1); if (pmain.logx) x = Math.exp(x); while ((indx < spline.fNp-1) && (x > spline.fPoly[indx+1].fX)) ++indx; const y = this.eval(spline.fPoly[indx], x); bins.push({ x, y, grx: funcs.grx(x), gry: funcs.gry(y) }); } this.draw_g.append('svg:path') .attr('class', 'line') .attr('d', buildSvgCurve(bins)) .style('fill', 'none') .call(this.lineatt.func); } if (this.options.Mark) { // for tooltips use markers only if nodes where not created let path = ''; this.createAttMarker({ attr: spline }); this.markeratt.resetPos(); this.knot_size = this.markeratt.getFullSize(); for (let n = 0; n < spline.fPoly.length; n++) { const knot = spline.fPoly[n], grx = funcs.grx(knot.fX); if ((grx > -this.knot_size) && (grx < w + this.knot_size)) { const gry = funcs.gry(knot.fY); if ((gry > -this.knot_size) && (gry < h + this.knot_size)) path += this.markeratt.create(grx, gry); } } if (path) { this.draw_g.append('svg:path') .attr('d', path) .call(this.markeratt.func); } } } /** @summary Checks if it makes sense to zoom inside specified axis range */ canZoomInside(axis /* , min, max */) { if (axis !== 'x') return false; // spline can always be calculated and therefore one can zoom inside return Boolean(this.getObject()); } /** @summary Decode options for TSpline drawing */ decodeOptions(opt) { const d = new DrawOptions(opt); if (!this.options) this.options = {}; const has_main = Boolean(this.getMainPainter()); Object.assign(this.options, { Same: d.check('SAME'), Line: d.check('L'), Curve: d.check('C'), Mark: d.check('P'), Hopt: '', second_x: false, second_y: false }); if (!this.options.Line && !this.options.Curve && !this.options.Mark) this.options.Curve = true; if (d.check('X+')) { this.options.Hopt += 'X+'; this.options.second_x = has_main; } if (d.check('Y+')) { this.options.Hopt += 'Y+'; this.options.second_y = has_main; } this.storeDrawOpt(opt); } /** @summary Draw TSpline */ static async draw(dom, spline, opt) { const painter = new TSplinePainter(dom, spline); painter.decodeOptions(opt); const no_main = !painter.getMainPainter(); let promise = Promise.resolve(); if (no_main || painter.options.second_x || painter.options.second_y) { if (painter.options.Same && no_main) { console.warn('TSpline painter requires histogram to be drawn'); return null; } const histo = painter.createDummyHisto(); promise = TH1Painter.draw(dom, histo, painter.options.Hopt); } return promise.then(() => { painter.addToPadPrimitives(); painter.redraw(); return painter; }); } } // class TSplinePainter export { TSplinePainter };