UNPKG

jsroot

Version:
1,413 lines (1,180 loc) 63.3 kB
import { gStyle, BIT, settings, create, createHistogram, setHistogramTitle, isFunc, isStr, clTPaveStats, clTCutG, clTH1F, clTH2F, clTF1, clTF2, clTPad, kNoZoom, kNoStats } from '../core.mjs'; import { select as d3_select } from '../d3.mjs'; import { DrawOptions, buildSvgCurve, makeTranslate, addHighlightStyle } from '../base/BasePainter.mjs'; import { ObjectPainter, kAxisNormal } from '../base/ObjectPainter.mjs'; import { FunctionsHandler } from './THistPainter.mjs'; import { TH1Painter, PadDrawOptions } from './TH1Painter.mjs'; import { kBlack, kWhite } from '../base/colors.mjs'; import { addMoveHandler } from '../gui/utils.mjs'; import { assignContextMenu, kNoReorder } from '../gui/menu.mjs'; const kNotEditable = BIT(18), // bit set if graph is non editable clTGraphErrors = 'TGraphErrors', clTGraphAsymmErrors = 'TGraphAsymmErrors', clTGraphBentErrors = 'TGraphBentErrors', clTGraphMultiErrors = 'TGraphMultiErrors'; /** * @summary Painter for TGraph object. * * @private */ class TGraphPainter extends ObjectPainter { #bins; // extracted graph bins #barwidth; // width of each bar #baroffset; // offset of each bar #redraw_hist; // indicate that histogram need to be redrawn #auto_exec; // can be reused when sending option back to server #funcs_handler; // special instance for functions drawing #frame_layer; // frame layer used for drawing #cutg; // is cutg object #cutg_lastsame; // indicate that last point is same as first #own_histogram; // if histogram created by TGraphPainter #marker_size; // used marker size #move_binindx; // index of moving bin #move_funcs; // moving functions #move_bin; // moving bin #move_x0; // initial x position #move_y0; // initial y position #pos_dx; // accumulated x change #pos_dy; // accumulated y change #has_errors; // if has errors #is_bent; // if graph has bent errors #draw_kind; // way how graph is drawn constructor(dom, graph) { super(dom, graph); this.axes_draw = false; // indicate if graph histogram was drawn for axes this.xmin = this.ymin = this.xmax = this.ymax = 0; this.#is_bent = (graph._typename === clTGraphBentErrors); this.#has_errors = (graph._typename === clTGraphErrors) || (graph._typename === clTGraphMultiErrors) || (graph._typename === clTGraphAsymmErrors) || this.#is_bent || graph._typename.match(/^RooHist/); this.#draw_kind = ''; } /** @summary Use in frame painter to check zoom Y is allowed * @protected */ get _wheel_zoomy() { return true; } /** @summary Return drawn graph object */ getGraph() { return this.getObject(); } /** @summary Return histogram object used for axis drawings */ getHistogram() { return this.getObject()?.fHistogram; } /** @summary Return true if histogram not present or has dummy ranges (for requested axis) */ isDummyHistogram(check_axis) { const histo = this.getHistogram(); if (!histo) return true; let is_normal = false; if (check_axis !== 'y') is_normal ||= (histo.fXaxis.fXmin !== 0.0011) || (histo.fXaxis.fXmax !== 1.1); if (check_axis !== 'x') { is_normal ||= (histo.fYaxis.fXmin !== 0.0011) || (histo.fYaxis.fXmax !== 1.1) || (histo.fMinimum !== 0.0011) || (histo.fMaximum !== 1.1); } return !is_normal; } /** @summary Set histogram object to graph */ setHistogram(histo) { const obj = this.getObject(); if (obj) obj.fHistogram = histo; } /** @summary Is TScatter object */ isScatter() { return false; } /** @summary Redraw graph * @desc may redraw histogram which was used to draw axes * @return {Promise} for ready */ async redraw() { let promise = Promise.resolve(true); if (this.#redraw_hist) { this.#redraw_hist = undefined; const hist_painter = this.getMainPainter(); if (hist_painter?.isSecondary(this) && this.axes_draw) promise = hist_painter.redraw(); } return promise.then(() => this.drawGraph()).then(() => { const res = this.#funcs_handler?.drawNext(0) ?? this; this.#funcs_handler = undefined; return res; }); } /** @summary Cleanup graph painter */ cleanup() { this.#bins = undefined; this.#own_histogram = undefined; super.cleanup(); } /** @summary Returns object if this drawing TGraphMultiErrors object */ get_gme() { const graph = this.getGraph(); return graph?._typename === clTGraphMultiErrors ? graph : null; } /** @summary Decode options */ decodeOptions(opt, first_time) { if (isStr(opt) && (opt.indexOf('same ') === 0)) opt = opt.slice(5); const graph = this.getGraph(), is_gme = Boolean(this.get_gme()), has_main = first_time ? Boolean(this.getMainPainter()) : !this.axes_draw; function decodeBlock(d, res) { Object.assign(res, { Line: 0, Curve: 0, Rect: 0, Mark: 0, Bar: 0, OutRange: 0, EF: 0, Fill: 0, MainError: 1, Ends: 1, ScaleErrX: 1 }); if (is_gme && d.check('S=', true)) res.ScaleErrX = d.partAsFloat(); if (d.check('L')) res.Line = 1; if (d.check('F')) res.Fill = 1; if (d.check('CC')) res.Curve = 2; // draw all points without reduction if (d.check('C')) res.Curve = 1; if (d.check('*')) res.Mark = 103; if (d.check('P0')) res.Mark = 104; if (d.check('P')) res.Mark = 1; if (d.check('B')) { res.Bar = 1; res.Errors = 0; } if (d.check('Z')) { res.Errors = 1; res.Ends = 0; } if (d.check('||')) { res.Errors = 1; res.MainError = 0; res.Ends = 1; } if (d.check('[]')) { res.Errors = 1; res.MainError = 0; res.Ends = 2; } if (d.check('|>')) { res.Errors = 1; res.Ends = 3; } if (d.check('>')) { res.Errors = 1; res.Ends = 4; } if (d.check('0')) { res.Mark = 1; res.Errors = 1; res.OutRange = 1; } if (d.check('1') && (res.Bar === 1)) res.Bar = 2; if (d.check('2')) { res.Rect = 1; res.Errors = 0; } if (d.check('3')) { res.EF = 1; res.Errors = 0; } if (d.check('4')) { res.EF = 2; res.Errors = 0; } if (d.check('5')) { res.Rect = 2; res.Errors = 0; } if (d.check('X')) res.Errors = 0; } const res = this.setOptions({ Axis: '', NoOpt: 0, PadStats: false, PadPalette: false, original: opt, second_x: false, second_y: false, individual_styles: false }); let blocks_gme = []; if (is_gme && opt) { if (opt.indexOf(';') > 0) { blocks_gme = opt.split(';'); opt = blocks_gme.shift(); } else if (opt.indexOf('_') > 0) { blocks_gme = opt.split('_'); opt = blocks_gme.shift(); } } let d = new DrawOptions(opt), hopt = ''; PadDrawOptions.forEach(name => { if (d.check(name)) hopt += ';' + name; }); if (d.check('XAXIS_', true)) hopt += ';XAXIS_' + d.part; if (d.check('YAXIS_', true)) hopt += ';YAXIS_' + d.part; if (d.empty()) { res.original = has_main ? 'lp' : 'alp'; d = new DrawOptions(res.original); } if (d.check('FILL_', 'color')) { res.graphFillColor = d.color; res.graphFillPattern = 1001; } if (d.check('FILLPAT_', true)) res.graphFillPattern = d.partAsInt(); if (d.check('LINE_', 'color')) res.graphLineColor = this.getColor(d.color); if (d.check('WIDTH_', true)) res.graphLineWidth = d.partAsInt(); if (d.check('NOOPT')) res.NoOpt = 1; if (d.check('POS3D_', true)) res.pos3d = d.partAsInt() - 0.5; if (d.check('PFC') && !res._pfc) res._pfc = 2; if (d.check('PLC') && !res._plc) res._plc = 2; if (d.check('PMC') && !res._pmc) res._pmc = 2; if (d.check('A')) res.Axis = d.check('I') ? 'A;' : ' '; // I means invisible axis if (d.check('X+')) { res.Axis += 'X+'; res.second_x = has_main; } if (d.check('Y+')) { res.Axis += 'Y+'; res.second_y = has_main; } if (d.check('RX')) res.Axis += 'RX'; if (d.check('RY')) res.Axis += 'RY'; if (is_gme) { res.blocks = []; res.skip_errors_x0 = res.skip_errors_y0 = false; if (d.check('X0')) res.skip_errors_x0 = true; if (d.check('Y0')) res.skip_errors_y0 = true; } decodeBlock(d, res); if (is_gme && d.check('S')) res.individual_styles = true; if (res.Errors === undefined) res.Errors = this.#has_errors && (!is_gme || !blocks_gme.length) ? 1 : 0; // special case - one could use svg:path to draw many pixels ( if ((res.Mark === 1) && (graph.fMarkerStyle === 1)) res.Mark = 101; // if no drawing option is selected and if opt === '' nothing is done. if ((res.Line + res.Fill + res.Curve + res.Mark + res.Bar + res.EF + res.Rect + res.Errors === 0) && d.empty()) res.Line = 1; if (this.matchObjectType(clTGraphErrors)) { const len = graph.fEX.length; let m = 0; for (let k = 0; k < len; ++k) m = Math.max(m, graph.fEX[k], graph.fEY[k]); if (m < 1e-100) res.Errors = 0; } this.#cutg = this.matchObjectType(clTCutG); this.#cutg_lastsame = this.#cutg && (graph.fNpoints > 3) && (graph.fX[0] === graph.fX[graph.fNpoints - 1]) && (graph.fY[0] === graph.fY[graph.fNpoints - 1]); if (!res.Axis) { // check if axis should be drawn // either graph drawn directly or // graph is first object in list of primitives const pad = this.getPadPainter()?.getRootPad(true); if (!pad || (pad?.fPrimitives?.arr[0] === this.getObject())) res.Axis = ' '; } res.Axis += hopt; for (let bl = 0; bl < blocks_gme.length; ++bl) { const subd = new DrawOptions(blocks_gme[bl]), subres = {}; decodeBlock(subd, subres); subres.skip_errors_x0 = res.skip_errors_x0; subres.skip_errors_y0 = res.skip_errors_y0; res.blocks.push(subres); } } /** @summary Return prepared graph bins * @protected */ _getBins() { return this.#bins; } /** @summary Create bins for TF1 drawing */ createBins() { const gr = this.getGraph(), o = this.getOptions(); if (!gr) return; let kind = 0, npoints = gr.fNpoints; if (this.#cutg && this.#cutg_lastsame) npoints--; if (gr._typename === clTGraphErrors) kind = 1; else if (gr._typename === clTGraphMultiErrors) kind = 2; else if (gr._typename === clTGraphAsymmErrors || gr._typename === clTGraphBentErrors || gr._typename.match(/^RooHist/)) kind = 3; this.#bins = new Array(npoints); for (let p = 0; p < npoints; ++p) { const bin = this.#bins[p] = { x: gr.fX[p], y: gr.fY[p], indx: p }; switch (kind) { case 1: bin.exlow = bin.exhigh = gr.fEX[p]; bin.eylow = bin.eyhigh = gr.fEY[p]; break; case 2: bin.exlow = gr.fExL[p]; bin.exhigh = gr.fExH[p]; bin.eylow = gr.fEyL[0][p]; bin.eyhigh = gr.fEyH[0][p]; break; case 3: bin.exlow = gr.fEXlow[p]; bin.exhigh = gr.fEXhigh[p]; bin.eylow = gr.fEYlow[p]; bin.eyhigh = gr.fEYhigh[p]; break; } if (p === 0) { this.xmin = this.xmax = bin.x; this.ymin = this.ymax = bin.y; } if (kind > 0) { this.xmin = Math.min(this.xmin, bin.x - bin.exlow, bin.x + bin.exhigh); this.xmax = Math.max(this.xmax, bin.x - bin.exlow, bin.x + bin.exhigh); this.ymin = Math.min(this.ymin, bin.y - bin.eylow, bin.y + bin.eyhigh); this.ymax = Math.max(this.ymax, bin.y - bin.eylow, bin.y + bin.eyhigh); } else { this.xmin = Math.min(this.xmin, bin.x); this.xmax = Math.max(this.xmax, bin.x); this.ymin = Math.min(this.ymin, bin.y); this.ymax = Math.max(this.ymax, bin.y); } } // workaround, are there better way to show marker at 0,0 on the top of the frame? this.#frame_layer = true; if ((this.xmin === 0) && (this.ymin === 0) && (npoints > 0) && (this.#bins[0].x === 0) && (this.#bins[0].y === 0) && o.Mark && !o.Line && !o.Curve && !o.Fill) this.#frame_layer = 'upper_layer'; } /** @summary Return margins for histogram ranges */ getHistRangeMargin() { return 0.1; } /** @summary Create histogram for graph * @desc graph bins should be created when calling this function * @param {boolean} [set_x] - set X axis range * @param {boolean} [set_y] - set Y axis range */ createHistogram(set_x = true, set_y = true) { const graph = this.getGraph(), xmin = this.xmin, margin = this.getHistRangeMargin(); let xmax = this.xmax, ymin = this.ymin, ymax = this.ymax; if (xmin >= xmax) xmax = xmin + 1; if (ymin >= ymax) ymax = ymin + 1; const dx = (xmax - xmin) * margin, dy = (ymax - ymin) * margin; let uxmin = xmin - dx, uxmax = xmax + dx, minimum = ymin - dy, maximum = ymax + dy; if ((ymin > 0) && (minimum <= 0)) minimum = (1 - margin) * ymin; if ((ymax < 0) && (maximum >= 0)) maximum = (1 - margin) * ymax; const minimum0 = minimum, maximum0 = maximum; let histo = this.getHistogram(); if (!this.isScatter() && !histo?.fXaxis.fTimeDisplay) { const pad_logx = this.getPadPainter()?.getPadLog('x'); if ((uxmin < 0) && (xmin >= 0)) uxmin = pad_logx ? xmin * (1 - margin) : 0; if ((uxmax > 0) && (xmax <= 0)) uxmax = pad_logx ? (1 + margin) * xmax : 0; } if (!histo) { histo = this.isScatter() ? createHistogram(clTH2F, 30, 30) : createHistogram(clTH1F, 100); histo.fName = graph.fName + '_h'; histo.fBits |= kNoStats; this.#own_histogram = true; this.setHistogram(histo); } else if ((histo.fMaximum !== kNoZoom) && (histo.fMinimum !== kNoZoom) && !this.isDummyHistogram('y')) { minimum = histo.fMinimum; maximum = histo.fMaximum; } if (graph.fMinimum !== kNoZoom) minimum = ymin = graph.fMinimum; if (graph.fMaximum !== kNoZoom) maximum = graph.fMaximum; if ((minimum < 0) && (ymin >= 0)) minimum = (1 - margin) * ymin; if ((ymax < 0) && (maximum >= 0)) maximum = (1 - margin) * ymax; setHistogramTitle(histo, this.getObject().fTitle); if (set_x && !histo.fXaxis.fLabels) { histo.fXaxis.fXmin = uxmin; histo.fXaxis.fXmax = uxmax; } if (set_y && !histo.fYaxis.fLabels) { histo.fYaxis.fXmin = Math.min(minimum0, minimum); histo.fYaxis.fXmax = Math.max(maximum0, maximum); if (!this.isScatter()) { histo.fMinimum = minimum; histo.fMaximum = maximum; } } histo.$xmin_nz = xmin > 0 ? xmin : undefined; histo.$ymin_nz = ymin > 0 ? ymin : undefined; return histo; } /** @summary Check if user range can be un-zommed * @desc Used when graph points covers larger range than provided histogram */ unzoomUserRange(dox, doy /* , doz */) { const graph = this.getGraph(); if (this.#own_histogram || !graph) return false; const histo = this.getHistogram(); dox = dox && histo && ((histo.fXaxis.fXmin > this.xmin) || (histo.fXaxis.fXmax < this.xmax)); doy = doy && histo && ((histo.fYaxis.fXmin > this.ymin) || (histo.fYaxis.fXmax < this.ymax)); if (!dox && !doy) return false; this.createHistogram(dox, doy); this.getMainPainter()?.extractAxesProperties(1); // just to enforce ranges extraction return true; } /** @summary Returns true if graph drawing can be optimize */ canOptimize() { return (settings.OptimizeDraw > 0) && !this.getOptions().NoOpt; } /** @summary Returns optimized bins - if optimization enabled */ optimizeBins(maxpnt, filter_func) { if ((this.#bins.length < 30) && !filter_func) return this.#bins; let selbins = null; if (isFunc(filter_func)) { for (let n = 0; n < this.#bins.length; ++n) { if (filter_func(this.#bins[n], n)) { if (!selbins) selbins = (n === 0) ? [] : this.#bins.slice(0, n); } else if (selbins) selbins.push(this.#bins[n]); } } if (!selbins) selbins = this.#bins; if (!maxpnt) maxpnt = 500000; if ((selbins.length < maxpnt) || !this.canOptimize()) return selbins; const optbins = [], step = Math.max(2, Math.floor(selbins.length / maxpnt)); for (let n = 0; n < selbins.length; n += step) optbins.push(selbins[n]); return optbins; } /** @summary Check if such function should be drawn directly */ needDrawFunc(graph, func) { if (func._typename === clTPaveStats) return (func.fName !== 'stats') || !graph.TestBit(kNoStats); // kNoStats is same for graph and histogram if ((func._typename === clTF1) || (func._typename === clTF2)) return !func.TestBit(BIT(9)); // TF1::kNotDraw return true; } /** @summary Returns tooltip for specified bin */ getTooltips(d) { const fp = this.get_fp(), lines = [], o = this.getOptions(), funcs = fp.getGrFuncs(o.second_x, o.second_y), gme = this.get_gme(); lines.push(this.getObjectHint()); if (d && funcs) { if (d.indx !== undefined) lines.push('p = ' + d.indx); lines.push('x = ' + funcs.axisAsText('x', d.x), 'y = ' + funcs.axisAsText('y', d.y)); if (gme) lines.push('error x = -' + funcs.axisAsText('x', gme.fExL[d.indx]) + '/+' + funcs.axisAsText('x', gme.fExH[d.indx])); else if (o.Errors && (funcs.x_handle.kind === kAxisNormal) && (d.exlow || d.exhigh)) lines.push('error x = -' + funcs.axisAsText('x', d.exlow) + '/+' + funcs.axisAsText('x', d.exhigh)); if (gme) { for (let ny = 0; ny < gme.fNYErrors; ++ny) lines.push(`error y${ny} = -${funcs.axisAsText('y', gme.fEyL[ny][d.indx])}/+${funcs.axisAsText('y', gme.fEyH[ny][d.indx])}`); } else if ((o.Errors || (o.EF > 0)) && (funcs.y_handle.kind === kAxisNormal) && (d.eylow || d.eyhigh)) lines.push('error y = -' + funcs.axisAsText('y', d.eylow) + '/+' + funcs.axisAsText('y', d.eyhigh)); } return lines; } /** @summary Provide frame painter for graph * @desc If not exists, emulate its behavior */ get_fp() { let fp = this.getFramePainter(); if (fp?.grx && fp?.gry) return fp; // FIXME: check if needed, can be removed easily const pp = this.getPadPainter(), rect = pp?.getPadRect() || { width: 800, height: 600 }; fp = { pad_layer: true, pad: pp?.getRootPad(true) ?? create(clTPad), pw: rect.width, ph: rect.height, fX1NDC: 0.1, fX2NDC: 0.9, fY1NDC: 0.1, fY2NDC: 0.9, getFrameWidth() { return this.pw; }, getFrameHeight() { return this.ph; }, grx(value) { if (this.pad.fLogx) value = (value > 0) ? Math.log10(value) : this.pad.fUxmin; else value = (value - this.pad.fX1) / (this.pad.fX2 - this.pad.fX1); return value * this.pw; }, gry(value) { if (this.pad.fLogv ?? this.pad.fLogy) value = (value > 0) ? Math.log10(value) : this.pad.fUymin; else value = (value - this.pad.fY1) / (this.pad.fY2 - this.pad.fY1); return (1 - value) * this.ph; }, revertAxis(name, v) { if (name === 'x') return v / this.pw * (this.pad.fX2 - this.pad.fX1) + this.pad.fX1; if (name === 'y') return (1 - v / this.ph) * (this.pad.fY2 - this.pad.fY1) + this.pad.fY1; return v; }, getGrFuncs() { return this; } }; return fp; } /** @summary append exclusion area to created path */ appendExclusion(is_curve, path, drawbins, excl_width) { const extrabins = []; for (let n = drawbins.length - 1; n >= 0; --n) { const bin = drawbins[n], dlen = Math.sqrt(bin.dgrx ** 2 + bin.dgry ** 2); if (dlen > 1e-10) { // shift point bin.grx += excl_width * bin.dgry / dlen; bin.gry -= excl_width * bin.dgrx / dlen; } extrabins.push(bin); } const path2 = buildSvgCurve(extrabins, { cmd: 'L', line: !is_curve }); this.appendPath(path + path2 + 'Z') .call(this.fillatt.func) .style('opacity', 0.75); } /** @summary draw TGraph bins with specified options * @desc Can be called several times */ drawBins(funcs, options, draw_g, w, h, lineatt, fillatt, main_block) { const graph = this.getGraph(); if (!graph?.fNpoints) return; let excl_width = 0, drawbins = null; // if markers or errors drawn - no need handle events for line drawing // this improves interactivity like zooming around graph points const line_events_handling = !this.isBatchMode() && (options.Line || options.Errors) ? 'none' : null; if (main_block && lineatt.excl_side) { excl_width = lineatt.excl_width; if ((lineatt.width > 0) && !options.Line && !options.Curve) options.Line = 1; } if (options.EF) { drawbins = this.optimizeBins((options.EF > 1) ? 20000 : 0); // build lower part for (let n = 0; n < drawbins.length; ++n) { const bin = drawbins[n]; bin.grx = funcs.grx(bin.x); bin.gry = funcs.gry(bin.y - bin.eylow); } const path1 = buildSvgCurve(drawbins, { line: options.EF < 2, qubic: true }), bins2 = []; for (let n = drawbins.length - 1; n >= 0; --n) { const bin = drawbins[n]; bin.gry = funcs.gry(bin.y + bin.eyhigh); bins2.push(bin); } // build upper part (in reverse direction) const path2 = buildSvgCurve(bins2, { line: options.EF < 2, cmd: 'L', qubic: true }), area = draw_g.append('svg:path') .attr('d', path1 + path2 + 'Z') .call(fillatt.func); // Let behaves as ROOT - see JIRA ROOT-8131 if (fillatt.empty() && fillatt.colorindx) area.style('stroke', this.getColor(fillatt.colorindx)); if (main_block) this.#draw_kind = 'lines'; } if (options.Line || options.Fill) { let close_symbol = ''; if (this.#cutg) { close_symbol = 'Z'; if (!options.original) options.Fill = 1; } if (options.Fill) { close_symbol = 'Z'; // always close area if we want to fill it excl_width = 0; } if (!drawbins) drawbins = this.optimizeBins(0); for (let n = 0; n < drawbins.length; ++n) { const bin = drawbins[n]; bin.grx = funcs.grx(bin.x); bin.gry = funcs.gry(bin.y); } const path = buildSvgCurve(drawbins, { line: true, calc: excl_width }); if (excl_width) this.appendExclusion(false, path, drawbins, excl_width); const elem = draw_g.append('svg:path') .attr('d', path + close_symbol) .style('pointer-events', line_events_handling); if (options.Line) elem.call(lineatt.func); if (options.Fill) elem.call(fillatt.func); else elem.style('fill', 'none'); if (main_block) this.#draw_kind = 'lines'; } if (options.Curve) { let curvebins = drawbins; if ((this.#draw_kind !== 'lines') || !curvebins || ((options.Curve === 1) && (curvebins.length > 20000))) { curvebins = this.optimizeBins((options.Curve === 1) ? 20000 : 0); for (let n = 0; n < curvebins.length; ++n) { const bin = curvebins[n]; bin.grx = funcs.grx(bin.x); bin.gry = funcs.gry(bin.y); } } const path = buildSvgCurve(curvebins, { qubic: !excl_width }); if (excl_width) this.appendExclusion(true, path, curvebins, excl_width); draw_g.append('svg:path') .attr('d', path) .call(lineatt.func) .style('fill', 'none') .style('pointer-events', line_events_handling); if (main_block) this.#draw_kind = 'lines'; // handled same way as lines } let nodes = null; if (options.Errors || options.Rect || options.Bar) { drawbins = this.optimizeBins(5000, (pnt, i) => { const grx = funcs.grx(pnt.x); // when drawing bars, take all points if (!options.Bar && ((grx < 0) || (grx > w))) return true; const gry = funcs.gry(pnt.y); if (!options.Bar && !options.OutRange && ((gry < 0) || (gry > h))) return true; pnt.grx1 = Math.round(grx); pnt.gry1 = Math.round(gry); if (this.#has_errors) { pnt.grx0 = Math.round(funcs.grx(pnt.x - options.ScaleErrX * pnt.exlow) - grx); pnt.grx2 = Math.round(funcs.grx(pnt.x + options.ScaleErrX * pnt.exhigh) - grx); pnt.gry0 = Math.round(funcs.gry(pnt.y - pnt.eylow) - gry); pnt.gry2 = Math.round(funcs.gry(pnt.y + pnt.eyhigh) - gry); if (this.#is_bent) { pnt.grdx0 = Math.round(funcs.gry(pnt.y + graph.fEXlowd[i]) - gry); pnt.grdx2 = Math.round(funcs.gry(pnt.y + graph.fEXhighd[i]) - gry); pnt.grdy0 = Math.round(funcs.grx(pnt.x + graph.fEYlowd[i]) - grx); pnt.grdy2 = Math.round(funcs.grx(pnt.x + graph.fEYhighd[i]) - grx); } else pnt.grdx0 = pnt.grdx2 = pnt.grdy0 = pnt.grdy2 = 0; } return false; }); if (main_block) this.#draw_kind = 'nodes'; nodes = draw_g.selectAll('.grpoint') .data(drawbins) .enter() .append('svg:g') .attr('class', 'grpoint') .attr('transform', d => makeTranslate(d.grx1, d.gry1)); } if (options.Bar) { // calculate bar width let xmin = 0, xmax = 0; for (let i = 0; i < drawbins.length; ++i) { if (i === 0) xmin = xmax = drawbins[i].grx1; else { xmin = Math.min(xmin, drawbins[i].grx1); xmax = Math.max(xmax, drawbins[i].grx1); } } const sz0 = drawbins.length < 2 ? w / 4 : (xmax - xmin) / drawbins.length, bw = sz0 * gStyle.fBarWidth, boff = sz0 * gStyle.fBarOffset, yy0 = Math.round(funcs.gry(0)); let usefill = fillatt; if (main_block) { const fp = this.getFramePainter(), fpcol = !fp?.fillatt?.empty() ? fp.fillatt.getFillColor() : -1; if (fpcol === fillatt.getFillColor()) usefill = this.createAttFill({ color: fpcol === 'white' ? kBlack : kWhite, pattern: 1001, std: false }); } nodes.append('svg:path') .attr('d', d => { d.bar = true; // element drawn as bar const dx = bw > 1 ? Math.round(boff - bw / 2) : 0, dw = bw > 1 ? Math.round(bw) : 1, dy = (options.Bar !== 1) ? 0 : ((d.gry1 > yy0) ? yy0 - d.gry1 : 0), dh = (options.Bar !== 1) ? (h > d.gry1 ? h - d.gry1 : 0) : Math.abs(yy0 - d.gry1); return `M${dx},${dy}h${dw}v${dh}h${-dw}z`; }) .call(usefill.func); this.#barwidth = bw; this.#baroffset = boff; } if (options.Rect) { nodes.filter(d => (d.exlow > 0) && (d.exhigh > 0) && (d.eylow > 0) && (d.eyhigh > 0)) .append('svg:path') .attr('d', d => { d.rect = true; return `M${d.grx0},${d.gry0}H${d.grx2}V${d.gry2}H${d.grx0}Z`; }) .call(fillatt.func) .call(options.Rect === 2 ? lineatt.func : () => {}); } this.error_size = 0; if (options.Errors) { // to show end of error markers, use line width attribute let lw = lineatt.width + gStyle.fEndErrorSize; const vv = options.Ends ? `m0,${lw}v${-2 * lw}` : '', hh = options.Ends ? `m${lw},0h${-2 * lw}` : ''; let vleft = vv, vright = vv, htop = hh, hbottom = hh, bb; const mainLine = (dx, dy) => { if (!options.MainError) return `M${dx},${dy}`; const res = 'M0,0'; if (dx) return res + (dy ? `L${dx},${dy}` : `H${dx}`); return dy ? res + `V${dy}` : res; }; switch (options.Ends) { case 2: // option [] bb = Math.max(lineatt.width + 1, Math.round(lw * 0.66)); vleft = `m${bb},${lw}h${-bb}v${-2 * lw}h${bb}`; vright = `m${-bb},${lw}h${bb}v${-2 * lw}h${-bb}`; htop = `m${-lw},${bb}v${-bb}h${2 * lw}v${bb}`; hbottom = `m${-lw},${-bb}v${bb}h${2 * lw}v${-bb}`; break; case 3: // option |> lw = Math.max(lw, Math.round(graph.fMarkerSize * 8 * 0.66)); bb = Math.max(lineatt.width + 1, Math.round(lw * 0.66)); vleft = `l${bb},${lw}v${-2 * lw}l${-bb},${lw}`; vright = `l${-bb},${lw}v${-2 * lw}l${bb},${lw}`; htop = `l${-lw},${bb}h${2 * lw}l${-lw},${-bb}`; hbottom = `l${-lw},${-bb}h${2 * lw}l${-lw},${bb}`; break; case 4: // option > lw = Math.max(lw, Math.round(graph.fMarkerSize * 8 * 0.66)); bb = Math.max(lineatt.width + 1, Math.round(lw * 0.66)); vleft = `l${bb},${lw}m0,${-2 * lw}l${-bb},${lw}`; vright = `l${-bb},${lw}m0,${-2 * lw}l${bb},${lw}`; htop = `l${-lw},${bb}m${2 * lw},0l${-lw},${-bb}`; hbottom = `l${-lw},${-bb}m${2 * lw},0l${-lw},${bb}`; break; } this.error_size = lw; lw = Math.floor((lineatt.width - 1) / 2); // one should take into account half of end-cup line width let visible = nodes.filter(d => (d.exlow > 0) || (d.exhigh > 0) || (d.eylow > 0) || (d.eyhigh > 0)); if (options.skip_errors_x0 || options.skip_errors_y0) visible = visible.filter(d => (d.x || !options.skip_errors_x0) && (d.y || !options.skip_errors_y0)); if (!this.isBatchMode() && settings.Tooltip && main_block) { visible.append('svg:path') .attr('d', d => `M${d.grx0},${d.gry0}h${d.grx2 - d.grx0}v${d.gry2 - d.gry0}h${d.grx0 - d.grx2}z`) .style('fill', 'none') .style('pointer-events', 'visibleFill'); } visible.append('svg:path') .attr('d', d => { d.error = true; return ((d.exlow > 0) ? mainLine(d.grx0 + lw, d.grdx0) + vleft : '') + ((d.exhigh > 0) ? mainLine(d.grx2 - lw, d.grdx2) + vright : '') + ((d.eylow > 0) ? mainLine(d.grdy0, d.gry0 - lw) + hbottom : '') + ((d.eyhigh > 0) ? mainLine(d.grdy2, d.gry2 + lw) + htop : ''); }) .style('fill', 'none') .call(lineatt.func); } if (options.Mark) { // for tooltips use markers only if nodes were not created this.createAttMarker({ attr: graph, style: options.Mark - 100 }); this.#marker_size = this.markeratt.getFullSize(); this.markeratt.resetPos(); const want_tooltip = !this.isBatchMode() && settings.Tooltip && (!this.markeratt.fill || (this.#marker_size < 7)) && !nodes && main_block, hsz = Math.max(5, Math.round(this.#marker_size * 0.7)), maxnummarker = 1000000 / (this.markeratt.getMarkerLength() + 7); // let produce SVG at maximum 1MB let path = '', pnt, grx, gry, hints_marker = '', step = 1; if (!drawbins) drawbins = this.optimizeBins(maxnummarker); else if (this.canOptimize() && (drawbins.length > 1.5 * maxnummarker)) step = Math.min(2, Math.round(drawbins.length / maxnummarker)); for (let n = 0; n < drawbins.length; n += step) { pnt = drawbins[n]; grx = funcs.grx(pnt.x); if ((grx > -this.#marker_size) && (grx < w + this.#marker_size)) { gry = funcs.gry(pnt.y); if ((gry > -this.#marker_size) && (gry < h + this.#marker_size)) { path += this.markeratt.create(grx, gry); if (want_tooltip) hints_marker += `M${grx - hsz},${gry - hsz}h${2 * hsz}v${2 * hsz}h${-2 * hsz}z`; } } } if (path) { draw_g.append('svg:path') .attr('d', path) .call(this.markeratt.func); if ((nodes === null) && (this.#draw_kind === 'none') && main_block) this.#draw_kind = (options.Mark === 101) ? 'path' : 'mark'; } if (want_tooltip && hints_marker) { draw_g.append('svg:path') .attr('d', hints_marker) .style('fill', 'none') .style('pointer-events', 'visibleFill'); } } } /** @summary append TGraphQQ part */ appendQQ(funcs, graph) { const xqmin = Math.max(funcs.scale_xmin, graph.fXq1), xqmax = Math.min(funcs.scale_xmax, graph.fXq2), yqmin = Math.max(funcs.scale_ymin, graph.fYq1), yqmax = Math.min(funcs.scale_ymax, graph.fYq2), makeLine = (x1, y1, x2, y2) => `M${funcs.grx(x1)},${funcs.gry(y1)}L${funcs.grx(x2)},${funcs.gry(y2)}`, yxmin = (graph.fYq2 - graph.fYq1) * (funcs.scale_xmin - graph.fXq1) / (graph.fXq2 - graph.fXq1) + graph.fYq1, yxmax = (graph.fYq2 - graph.fYq1) * (funcs.scale_xmax - graph.fXq1) / (graph.fXq2 - graph.fXq1) + graph.fYq1; let path2; if (yxmin < funcs.scale_ymin) { const xymin = (graph.fXq2 - graph.fXq1) * (funcs.scale_ymin - graph.fYq1) / (graph.fYq2 - graph.fYq1) + graph.fXq1; path2 = makeLine(xymin, funcs.scale_ymin, xqmin, yqmin); } else path2 = makeLine(funcs.scale_xmin, yxmin, xqmin, yqmin); if (yxmax > funcs.scale_ymax) { const xymax = (graph.fXq2 - graph.fXq1) * (funcs.scale_ymax - graph.fYq1) / (graph.fYq2 - graph.fYq1) + graph.fXq1; path2 += makeLine(xqmax, yqmax, xymax, funcs.scale_ymax); } else path2 += makeLine(xqmax, yqmax, funcs.scale_xmax, yxmax); const latt1 = this.createAttLine({ style: 1, width: 1, color: kBlack, std: false }), latt2 = this.createAttLine({ style: 2, width: 1, color: kBlack, std: false }); this.appendPath(makeLine(xqmin, yqmin, xqmax, yqmax)) .call(latt1.func) .style('fill', 'none'); this.appendPath(path2) .call(latt2.func) .style('fill', 'none'); } drawBins3D(/* fp, graph */) { console.log('Load ./hist/TGraphPainter.mjs to draw graph in 3D'); } /** @summary Create necessary histogram draw attributes */ createGraphDrawAttributes(only_check_auto) { const graph = this.getGraph(), o = this.getOptions(); if (o._pfc > 1 || o._plc > 1 || o._pmc > 1) { const pp = this.getPadPainter(); if (isFunc(pp?.getAutoColor)) { const icolor = pp.getAutoColor(graph.$num_graphs); this.#auto_exec = ''; // can be reused when sending option back to server if (o._pfc > 1) { o._pfc = 1; graph.fFillColor = icolor; this.#auto_exec += `SetFillColor(${icolor});;`; this.deleteAttr('fill'); } if (o._plc > 1) { o._plc = 1; graph.fLineColor = icolor; this.#auto_exec += `SetLineColor(${icolor});;`; this.deleteAttr('line'); } if (o._pmc > 1) { o._pmc = 1; graph.fMarkerColor = icolor; this.#auto_exec += `SetMarkerColor(${icolor});;`; this.deleteAttr('marker'); } } } if (only_check_auto) this.deleteAttr(); else { this.createAttLine({ attr: graph, can_excl: true, color0: o.graphLineColor, width: o.graphLineWidth }); this.createAttFill({ attr: graph, color: o.graphFillColor, pattern: o.graphFillPattern, }); } } /** @summary draw TGraph */ drawGraph() { const fp = this.get_fp(), graph = this.getGraph(), o = this.getOptions(); if (!fp) return; // special mode for TMultiGraph 3d drawing if (o.pos3d) return this.drawBins3D(fp, graph); const is_gme = Boolean(this.get_gme()), funcs = fp.getGrFuncs(o.second_x, o.second_y), w = funcs.getFrameWidth(), h = funcs.getFrameHeight(), g = this.createG(fp.pad_layer ? false : this.#frame_layer); this.createGraphDrawAttributes(); this.fillatt.used = false; // mark used only when really used this.#draw_kind = 'none'; // indicate if special svg:g were created for each bin this.#marker_size = 0; // indicate if markers are drawn const draw_g = is_gme ? g.append('svg:g') : g; this.drawBins(funcs, o, draw_g, w, h, this.lineatt, this.fillatt, true); if (graph._typename === 'TGraphQQ') this.appendQQ(funcs, graph); if (is_gme) { const extract_gme_errors = nblock => { this.#bins?.forEach(bin => { bin.eylow = graph.fEyL[nblock][bin.indx]; bin.eyhigh = graph.fEyH[nblock][bin.indx]; }); }; for (let k = 0; k < graph.fNYErrors; ++k) { const lineatt = !o.individual_styles ? this.lineatt : this.createAttLine({ attr: graph.fAttLine[k], std: false }), fillatt = !o.individual_styles ? this.fillatt : this.createAttFill({ attr: graph.fAttFill[k], std: false }), sub_g = g.append('svg:g'), options = k < o.blocks.length ? o.blocks[k] : o; extract_gme_errors(k); this.drawBins(funcs, options, sub_g, w, h, lineatt, fillatt); } extract_gme_errors(0); // ensure that first block kept at the end } if (!this.isBatchMode()) { addMoveHandler(this, this.testEditable()); assignContextMenu(this, kNoReorder); } } /** @summary Provide tooltip at specified point */ extractTooltip(pnt) { if (!pnt) return null; if ((this.#draw_kind === 'lines') || (this.#draw_kind === 'path') || (this.#draw_kind === 'mark')) return this.extractTooltipForPath(pnt); if (this.#draw_kind !== 'nodes') return null; const fp = this.get_fp(), o = this.getOptions(), height = fp.getFrameHeight(), bw = this.#barwidth, boff = this.#baroffset, esz = this.error_size, isbar1 = (o.Bar === 1), funcs = isbar1 ? fp.getGrFuncs(o.second_x, o.second_y) : null, msize = this.#marker_size ? Math.round(this.#marker_size / 2 + 1.5) : 0; let findbin = null, best_dist2 = 1e10, best = null; this.getG().selectAll('.grpoint').each(function() { const d = d3_select(this).datum(); if (d === undefined) return; let dist2 = (pnt.x - d.grx1) ** 2; if (pnt.nproc === 1) dist2 += (pnt.y - d.gry1) ** 2; if (dist2 >= best_dist2) return; let rect; if (d.error || d.rect || d.marker) { rect = { x1: Math.min(-esz, d.grx0, -msize), x2: Math.max(esz, d.grx2, msize), y1: Math.min(-esz, d.gry2, -msize), y2: Math.max(esz, d.gry0, msize) }; } else if (d.bar) { rect = { x1: boff - bw / 2, x2: boff + bw / 2, y1: 0, y2: height - d.gry1 }; if (isbar1) { const yy0 = funcs.gry(0); rect.y1 = (d.gry1 > yy0) ? yy0 - d.gry1 : 0; rect.y2 = (d.gry1 > yy0) ? 0 : yy0 - d.gry1; } } else rect = { x1: -5, x2: 5, y1: -5, y2: 5 }; const matchx = (pnt.x >= d.grx1 + rect.x1) && (pnt.x <= d.grx1 + rect.x2), matchy = (pnt.y >= d.gry1 + rect.y1) && (pnt.y <= d.gry1 + rect.y2); if (matchx && (matchy || (pnt.nproc > 1))) { best_dist2 = dist2; findbin = this; best = rect; best.exact = /* matchx && */ matchy; } }); if (findbin === null) return null; const d = d3_select(findbin).datum(), gr = this.getGraph(), res = { name: gr.fName, title: gr.fTitle, x: d.grx1, y: d.gry1, color1: this.lineatt.color, lines: this.getTooltips(d), rect: best, d3bin: findbin }; res.user_info = { obj: gr, name: gr.fName, bin: d.indx, cont: d.y, grx: d.grx1, gry: d.gry1 }; if (this.fillatt?.used && !this.fillatt?.empty()) res.color2 = this.fillatt.getFillColor(); if (best.exact) res.exact = true; res.menu = res.exact; // activate menu only when exactly locate bin res.menu_dist = 3; // distance always fixed res.bin = d; res.binindx = d.indx; return res; } /** @summary Show tooltip */ showTooltip(hint) { let ttrect = this.getG()?.selectChild('.tooltip_bin'); if (!hint || !this.getG()) { ttrect?.remove(); return; } if (hint.usepath) return this.showTooltipForPath(hint); const d = d3_select(hint.d3bin).datum(); if (ttrect.empty()) { ttrect = this.getG().append('svg:rect') .attr('class', 'tooltip_bin') .style('pointer-events', 'none') .call(addHighlightStyle); } hint.changed = ttrect.property('current_bin') !== hint.d3bin; if (hint.changed) { ttrect.attr('x', d.grx1 + hint.rect.x1) .attr('width', hint.rect.x2 - hint.rect.x1) .attr('y', d.gry1 + hint.rect.y1) .attr('height', hint.rect.y2 - hint.rect.y1) .style('opacity', '0.3') .property('current_bin', hint.d3bin); } } /** @summary Process tooltip event */ processTooltipEvent(pnt) { const hint = this.extractTooltip(pnt); if (!pnt || !pnt.disabled) this.showTooltip(hint); return hint; } /** @summary Find best bin index for specified point */ findBestBin(pnt) { if (!this.#bins) return null; const islines = (this.#draw_kind === 'lines'), o = this.getOptions(), funcs = this.get_fp().getGrFuncs(o.second_x, o.second_y); let bestindx = -1, bestbin = null, bestdist = 1e10, dist, grx, gry, n, bin; for (n = 0; n < this.#bins.length; ++n) { bin = this.#bins[n]; grx = funcs.grx(bin.x); gry = funcs.gry(bin.y); dist = (pnt.x - grx) ** 2 + (pnt.y - gry) ** 2; if (dist < bestdist) { bestdist = dist; bestbin = bin; bestindx = n; } } // check last point if ((bestdist > 100) && islines) bestbin = null; const radius = Math.max(this.lineatt.width + 3, 4, this.#marker_size); if (bestbin) bestdist = Math.sqrt((pnt.x - funcs.grx(bestbin.x)) ** 2 + (pnt.y - funcs.gry(bestbin.y)) ** 2); if (!islines && (bestdist > radius)) bestbin = null; if (!bestbin) bestindx = -1; const res = { bin: bestbin, indx: bestindx, dist: bestdist, radius: Math.round(radius) }; if (!bestbin && islines) { bestdist = 1e10; const is_inside = (x, x1, x2) => ((x1 >= x) && (x >= x2)) || ((x1 <= x) && (x <= x2)); let bin0 = this.#bins[0], grx0 = funcs.grx(bin0.x), gry0, posy; for (n = 1; n < this.#bins.length; ++n) { bin = this.#bins[n]; grx = funcs.grx(bin.x); if (is_inside(pnt.x, grx0, grx)) { // if inside interval, check Y distance gry0 = funcs.gry(bin0.y); gry = funcs.gry(bin.y); if (Math.abs(grx - grx0) < 1) { // very close x - check only y posy = pnt.y; dist = is_inside(pnt.y, gry0, gry) ? 0 : Math.min(Math.abs(pnt.y - gry0), Math.abs(pnt.y - gry)); } else { posy = gry0 + (pnt.x - grx0) / (grx - grx0) * (gry - gry0); dist = Math.abs(posy - pnt.y); } if (dist < bestdist) { bestdist = dist; res.linex = pnt.x; res.liney = posy; } } bin0 = bin; grx0 = grx; } if (bestdist < radius * 0.5) { res.linedist = bestdist; res.closeline = true; } } return res; } /** @summary Check editable flag for TGraph * @desc if arg specified changes or toggles editable flag */ testEditable(arg) { const obj = this.getGraph(); if (!isFunc(obj?.TestBit)) return false; if ((arg === 'toggle') || (arg !== undefined)) obj.SetBit(kNotEditable, !arg); return !obj.TestBit(kNotEditable); } /** @summary Provide tooltip at specified point for path-based drawing */ extractTooltipForPath(pnt) { if (!this.#bins) return null; const best = this.findBestBin(pnt); if (!best || (!best.bin && !best.closeline)) return null; const islines = (this.#draw_kind === 'lines'), ismark = (this.#draw_kind === 'mark'), fp = this.get_fp(), o = this.getOptions(), funcs = fp.getGrFuncs(o.second_x, o.second_y), gr = this.getGraph(), res = { name: gr.fName, title: gr.fTitle, x: best.bin ? funcs.grx(best.bin.x) : best.linex, y: best.bin ? funcs.gry(best.bin.y) : best.liney, color1: this.lineatt.color, lines: this.getTooltips(best.bin), usepath: true, ismark, islines }; res.user_info = { obj: gr, name: gr.fName, bin: 0, cont: 0, grx: res.x, gry: res.y }; if (best.closeline) { res.menu = res.exact = true; res.menu_dist = best.linedist; } else if (best.bin) { if (o.EF && islines) { res.gry1 = funcs.gry(best.bin.y - best.bin.eylow); res.gry2 = funcs.gry(best.bin.y + best.bin.eyhigh); } else res.gry1 = res.gry2 = funcs.gry(best.bin.y); res.binindx = best.indx; res.bin = best.bin; res.radius = best.radius; res.user_info.bin = best.indx; res.user_info.cont = best.bin.y; res.exact = (Math.abs(pnt.x - res.x) <= best.radius) && ((Math.abs(pnt.y - res.gry1) <= best.radius) || (Math.abs(pnt.y - res.gry2) <= best.radius)); res.menu = res.exact; res.menu_dist = Math.sqrt((pnt.x - res.x) ** 2 + Math.min(Math.abs(pnt.y - res.gry1), Math.abs(pnt.y - res.gry2)) ** 2); } if (this.fillatt?.used && !this.fillatt?.em