UNPKG

jsroot

Version:
1,016 lines (809 loc) 35.9 kB
import { gStyle, settings, constants, kInspect } from '../core.mjs'; import { rgb as d3_rgb } from '../d3.mjs'; import { floatToString, DrawOptions, buildSvgCurve, addHighlightStyle } from '../base/BasePainter.mjs'; import { RHistPainter } from './RHistPainter.mjs'; import { ensureRCanvas } from '../gpad/RCanvasPainter.mjs'; /** * @summary Painter for RH1 classes * * @private */ class RH1Painter extends RHistPainter { /** @summary Constructor * @param {object|string} dom - DOM element or id * @param {object} histo - histogram object */ constructor(dom, histo) { super(dom, histo); this.wheel_zoomy = false; } /** @summary Scan content */ scanContent(when_axis_changed) { // if when_axis_changed === true specified, content will be scanned after axis zoom changed const histo = this.getHisto(); if (!histo) return; if (!this.nbinsx && when_axis_changed) when_axis_changed = false; if (!when_axis_changed) this.extractAxesProperties(1); let hmin = 0, hmin_nz = 0, hmax = 0, hsum = 0; if (this.isDisplayItem()) { // take min/max values from the display item hmin = histo.fContMin; hmin_nz = histo.fContMinPos; hmax = histo.fContMax; hsum = hmax; } else { const left = this.getSelectIndex('x', 'left'), right = this.getSelectIndex('x', 'right'); if (when_axis_changed) if ((left === this.scan_xleft) && (right === this.scan_xright)) return; this.scan_xleft = left; this.scan_xright = right; let first = true, value, err; for (let i = 0; i < this.nbinsx; ++i) { value = histo.getBinContent(i+1); hsum += value; if ((i<left) || (i>=right)) continue; if (value > 0) if ((hmin_nz === 0) || (value<hmin_nz)) hmin_nz = value; if (first) { hmin = hmax = value; first = false; } err = 0; hmin = Math.min(hmin, value - err); hmax = Math.max(hmax, value + err); } } this.stat_entries = hsum; this.hmin = hmin; this.hmax = hmax; this.ymin_nz = hmin_nz; // value can be used to show optimal log scale if ((this.nbinsx === 0) || ((Math.abs(hmin) < 1e-300) && (Math.abs(hmax) < 1e-300))) this.draw_content = false; else this.draw_content = true; if (this.draw_content) { if (hmin >= hmax) { if (hmin === 0) { this.ymin = 0; this.ymax = 1; } else if (hmin < 0) { this.ymin = 2 * hmin; this.ymax = 0; } else { this.ymin = 0; this.ymax = hmin * 2; } } else { const dy = (hmax - hmin) * 0.05; this.ymin = hmin - dy; if ((this.ymin < 0) && (hmin >= 0)) this.ymin = 0; this.ymax = hmax + dy; } } } /** @summary Count statistic */ countStat(cond) { const histo = this.getHisto(), xaxis = this.getAxis('x'), left = this.getSelectIndex('x', 'left'), right = this.getSelectIndex('x', 'right'), stat_sumwy = 0, stat_sumwy2 = 0, res = { name: 'histo', meanx: 0, meany: 0, rmsx: 0, rmsy: 0, integral: 0, entries: this.stat_entries, xmax: 0, wmax: 0 }; let stat_sumw = 0, stat_sumwx = 0, stat_sumwx2 = 0, i, xmax = null, wmax = null; for (i = left; i < right; ++i) { const xx = xaxis.GetBinCoord(i+0.5); if (cond && !cond(xx)) continue; const w = histo.getBinContent(i + 1); if ((xmax === null) || (w > wmax)) { xmax = xx; wmax = w; } stat_sumw += w; stat_sumwx += w * xx; stat_sumwx2 += w * xx**2; } res.integral = stat_sumw; if (Math.abs(stat_sumw) > 1e-300) { res.meanx = stat_sumwx / stat_sumw; res.meany = stat_sumwy / stat_sumw; res.rmsx = Math.sqrt(Math.abs(stat_sumwx2 / stat_sumw - res.meanx**2)); res.rmsy = Math.sqrt(Math.abs(stat_sumwy2 / stat_sumw - res.meany**2)); } if (xmax !== null) { res.xmax = xmax; res.wmax = wmax; } return res; } /** @summary Fill statistic */ fillStatistic(stat, dostat /* , dofit */) { const histo = this.getHisto(), data = this.countStat(), print_name = dostat % 10, print_entries = Math.floor(dostat / 10) % 10, print_mean = Math.floor(dostat / 100) % 10, print_rms = Math.floor(dostat / 1000) % 10, print_under = Math.floor(dostat / 10000) % 10, print_over = Math.floor(dostat / 100000) % 10, print_integral = Math.floor(dostat / 1000000) % 10, print_skew = Math.floor(dostat / 10000000) % 10, print_kurt = Math.floor(dostat / 100000000) % 10; // make empty at the beginning stat.clearStat(); if (print_name > 0) stat.addText(data.name); if (print_entries > 0) stat.addText('Entries = ' + stat.format(data.entries, 'entries')); if (print_mean > 0) stat.addText('Mean = ' + stat.format(data.meanx)); if (print_rms > 0) stat.addText('Std Dev = ' + stat.format(data.rmsx)); if (print_under > 0) stat.addText('Underflow = ' + stat.format(histo.getBinContent(0), 'entries')); if (print_over > 0) stat.addText('Overflow = ' + stat.format(histo.getBinContent(this.nbinsx+1), 'entries')); if (print_integral > 0) stat.addText('Integral = ' + stat.format(data.integral, 'entries')); if (print_skew > 0) stat.addText('Skew = <not avail>'); if (print_kurt > 0) stat.addText('Kurt = <not avail>'); return true; } /** @summary Get baseline for bar drawings * @private */ getBarBaseline(funcs, height) { let gry = funcs.swap_xy ? 0 : height; if (Number.isFinite(this.options.BaseLine) && (this.options.BaseLine >= funcs.scale_ymin)) gry = Math.round(funcs.gry(this.options.BaseLine)); return gry; } /** @summary Draw histogram as bars */ async drawBars(handle, funcs, width, height) { this.createG(true); const left = handle.i1, right = handle.i2, di = handle.stepi, pmain = this.getFramePainter(), histo = this.getHisto(), xaxis = this.getAxis('x'); let i, x1, x2, grx1, grx2, y, gry1, w, bars = '', barsl = '', barsr = ''; const gry2 = this.getBarBaseline(funcs, height); for (i = left; i < right; i += di) { x1 = xaxis.GetBinCoord(i); x2 = xaxis.GetBinCoord(i+di); if (funcs.logx && (x2 <= 0)) continue; grx1 = Math.round(funcs.grx(x1)); grx2 = Math.round(funcs.grx(x2)); y = histo.getBinContent(i+1); if (funcs.logy && (y < funcs.scale_ymin)) continue; gry1 = Math.round(funcs.gry(y)); w = grx2 - grx1; grx1 += Math.round(this.options.BarOffset*w); w = Math.round(this.options.BarWidth*w); if (pmain.swap_xy) bars += `M${gry2},${grx1}h${gry1-gry2}v${w}h${gry2-gry1}z`; else bars += `M${grx1},${gry1}h${w}v${gry2-gry1}h${-w}z`; if (this.options.BarStyle > 0) { grx2 = grx1 + w; w = Math.round(w / 10); if (pmain.swap_xy) { barsl += `M${gry2},${grx1}h${gry1-gry2}v${w}h${gry2-gry1}z`; barsr += `M${gry2},${grx2}h${gry1-gry2}v${-w}h${gry2-gry1}z`; } else { barsl += `M${grx1},${gry1}h${w}v${gry2-gry1}h${-w}z`; barsr += `M${grx2},${gry1}h${-w}v${gry2-gry1}h${w}z`; } } } if (this.fillatt.empty()) this.fillatt.setSolidColor('blue'); if (bars) { this.draw_g.append('svg:path') .attr('d', bars) .call(this.fillatt.func); } if (barsl) { this.draw_g.append('svg:path') .attr('d', barsl) .call(this.fillatt.func) .style('fill', d3_rgb(this.fillatt.color).brighter(0.5).formatRgb()); } if (barsr) { this.draw_g.append('svg:path') .attr('d', barsr) .call(this.fillatt.func) .style('fill', d3_rgb(this.fillatt.color).darker(0.5).formatRgb()); } return true; } /** @summary Draw histogram as filled errors */ async drawFilledErrors(handle, funcs /* , width, height */) { this.createG(true); const left = handle.i1, right = handle.i2, di = handle.stepi, histo = this.getHisto(), xaxis = this.getAxis('x'), bins1 = [], bins2 = []; let i, x, grx, y, yerr, gry; for (i = left; i < right; i += di) { x = xaxis.GetBinCoord(i+0.5); if (funcs.logx && (x <= 0)) continue; grx = Math.round(funcs.grx(x)); y = histo.getBinContent(i+1); yerr = histo.getBinError(i+1); if (funcs.logy && (y-yerr < funcs.scale_ymin)) continue; gry = Math.round(funcs.gry(y + yerr)); bins1.push({ grx, gry }); gry = Math.round(funcs.gry(y - yerr)); bins2.unshift({ grx, gry }); } const path1 = buildSvgCurve(bins1, { line: this.options.ErrorKind !== 4 }), path2 = buildSvgCurve(bins2, { line: this.options.ErrorKind !== 4, cmd: 'L' }); if (this.fillatt.empty()) this.fillatt.setSolidColor('blue'); this.draw_g.append('svg:path') .attr('d', path1 + path2 + 'Z') .call(this.fillatt.func); return true; } /** @summary Draw 1D histogram as SVG */ async draw1DBins() { const pmain = this.getFramePainter(), rect = pmain.getFrameRect(); if (!this.draw_content || (rect.width <= 0) || (rect.height <= 0)) { this.removeG(); return false; } this.createHistDrawAttributes(); const handle = this.prepareDraw({ extra: 1, only_indexes: true }), funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y); if (this.options.Bar) return this.drawBars(handle, funcs, rect.width, rect.height); if ((this.options.ErrorKind === 3) || (this.options.ErrorKind === 4)) return this.drawFilledErrors(handle, funcs, rect.width, rect.height); return this.drawHistBins(handle, funcs, rect.width, rect.height); } /** @summary Draw histogram bins */ async drawHistBins(handle, funcs, width, height) { this.createG(true); const options = this.options, left = handle.i1, right = handle.i2, di = handle.stepi, histo = this.getHisto(), want_tooltip = !this.isBatchMode() && settings.Tooltip, xaxis = this.getAxis('x'), exclude_zero = !options.Zero, show_errors = options.Error, show_line = options.Line, show_text = options.Text; let show_markers = options.Mark, res = '', lastbin = false, startx, currx, curry, x, grx, y, gry, curry_min, curry_max, prevy, prevx, i, bestimin, bestimax, path_fill = null, path_err = null, path_marker = null, path_line = null, hints_err = null, endx = '', endy = '', dend = 0, my, yerr1, yerr2, bincont, binerr, mx1, mx2, midx, text_font, pr = Promise.resolve(); if (show_errors && !show_markers && (this.v7EvalAttr('marker_style', 1) > 1)) show_markers = true; if (options.ErrorKind === 2) { if (this.fillatt.empty()) show_markers = true; else path_fill = ''; } else if (options.Error) { path_err = ''; hints_err = want_tooltip ? '' : null; } if (show_line) path_line = ''; if (show_markers) { // draw markers also when e2 option was specified this.createv7AttMarker(); if (this.markeratt.size > 0) { // simply use relative move from point, can optimize in the future path_marker = ''; this.markeratt.resetPos(); } else show_markers = false; } if (show_text) { text_font = this.v7EvalFont('text', { size: 20, color: 'black', align: 22 }); if (!text_font.angle && !options.TextKind) { const space = width / (right - left + 1); if (space < 3 * text_font.size) { text_font.setAngle(270); text_font.setSize(Math.round(space*0.7)); } } pr = this.startTextDrawingAsync(text_font, 'font'); } return pr.then(() => { // if there are too many points, exclude many vertical drawings at the same X position // instead define min and max value and made min-max drawing let use_minmax = ((right-left) > 3*width); if (options.ErrorKind === 1) { const lw = this.lineatt.width + gStyle.fEndErrorSize; endx = `m0,${lw}v${-2*lw}m0,${lw}`; endy = `m${lw},0h${-2*lw}m${lw},0`; dend = Math.floor((this.lineatt.width-1)/2); } const draw_markers = show_errors || show_markers; if (draw_markers || show_text || show_line) use_minmax = true; const draw_bin = besti => { bincont = histo.getBinContent(besti+1); if (!exclude_zero || (bincont !== 0)) { mx1 = Math.round(funcs.grx(xaxis.GetBinCoord(besti))); mx2 = Math.round(funcs.grx(xaxis.GetBinCoord(besti+di))); midx = Math.round((mx1+mx2)/2); my = Math.round(funcs.gry(bincont)); yerr1 = yerr2 = 20; if (show_errors) { binerr = histo.getBinError(besti+1); yerr1 = Math.round(my - funcs.gry(bincont + binerr)); // up yerr2 = Math.round(funcs.gry(bincont - binerr) - my); // down } if (show_text && (bincont !== 0)) { const lbl = (bincont === Math.round(bincont)) ? bincont.toString() : floatToString(bincont, gStyle.fPaintTextFormat); if (text_font.angle) this.drawText({ align: 12, x: midx, y: Math.round(my - 2 - text_font.size / 5), text: lbl, latex: 0 }); else this.drawText({ x: Math.round(mx1 + (mx2 - mx1) * 0.1), y: Math.round(my - 2 - text_font.size), width: Math.round((mx2 - mx1) * 0.8), height: text_font.size, text: lbl, latex: 0 }); } if (show_line && (path_line !== null)) path_line += ((path_line.length === 0) ? 'M' : 'L') + midx + ',' + my; if (draw_markers) { if ((my >= -yerr1) && (my <= height + yerr2)) { if (path_fill !== null) path_fill += `M${mx1},${my-yerr1}h${mx2-mx1}v${yerr1+yerr2+1}h${mx1-mx2}z`; if (path_marker !== null) path_marker += this.markeratt.create(midx, my); if (path_err !== null) { let edx = 5; if (this.options.errorX > 0) { edx = Math.round((mx2-mx1)*this.options.errorX); const mmx1 = midx - edx, mmx2 = midx + edx; path_err += `M${mmx1+dend},${my}${endx}h${mmx2-mmx1-2*dend}${endx}`; } path_err += `M${midx},${my-yerr1+dend}${endy}v${yerr1+yerr2-2*dend}${endy}`; if (hints_err !== null) hints_err += `M${midx-edx},${my-yerr1}h${2*edx}v${yerr1+yerr2}h${-2*edx}z`; } } } } }; for (i = left; i <= right; i += di) { x = xaxis.GetBinCoord(i); if (funcs.logx && (x <= 0)) continue; grx = Math.round(funcs.grx(x)); lastbin = (i > right - di); if (lastbin && (left < right)) gry = curry; else { y = histo.getBinContent(i+1); gry = Math.round(funcs.gry(y)); } if (res.length === 0) { bestimin = bestimax = i; prevx = startx = currx = grx; prevy = curry_min = curry_max = curry = gry; res = 'M'+currx+','+curry; } else if (use_minmax) { if ((grx === currx) && !lastbin) { if (gry < curry_min) bestimax = i; else if (gry > curry_max) bestimin = i; curry_min = Math.min(curry_min, gry); curry_max = Math.max(curry_max, gry); curry = gry; } else { if (draw_markers || show_text || show_line) { if (bestimin === bestimax) draw_bin(bestimin); else if (bestimin < bestimax) { draw_bin(bestimin); draw_bin(bestimax); } else { draw_bin(bestimax); draw_bin(bestimin); } } // when several points as same X differs, need complete logic if (!draw_markers && ((curry_min !== curry_max) || (prevy !== curry_min))) { if (prevx !== currx) res += 'h'+(currx-prevx); if (curry === curry_min) { if (curry_max !== prevy) res += 'v' + (curry_max - prevy); if (curry_min !== curry_max) res += 'v' + (curry_min - curry_max); } else { if (curry_min !== prevy) res += 'v' + (curry_min - prevy); if (curry_max !== curry_min) res += 'v' + (curry_max - curry_min); if (curry !== curry_max) res += 'v' + (curry - curry_max); } prevx = currx; prevy = curry; } if (lastbin && (prevx !== grx)) res += 'h'+(grx-prevx); bestimin = bestimax = i; curry_min = curry_max = curry = gry; currx = grx; } } else if ((gry !== curry) || lastbin) { if (grx !== currx) res += 'h'+(grx-currx); if (gry !== curry) res += 'v'+(gry-curry); curry = gry; currx = grx; } } const fill_for_interactive = !this.isBatchMode() && this.fillatt.empty() && options.Hist && settings.Tooltip && !draw_markers && !show_line; let h0 = height + 3; if (!fill_for_interactive) { const gry0 = Math.round(funcs.gry(0)); if (gry0 <= 0) h0 = -3; else if (gry0 < height) h0 = gry0; } const close_path = `L${currx},${h0}H${startx}Z`; if (draw_markers || show_line) { if (path_fill) { this.draw_g.append('svg:path') .attr('d', path_fill) .call(this.fillatt.func); } if (path_err) { this.draw_g.append('svg:path') .attr('d', path_err) .call(this.lineatt.func); } if (hints_err) { this.draw_g.append('svg:path') .attr('d', hints_err) .style('fill', 'none') .style('pointer-events', this.isBatchMode() ? null : 'visibleFill'); } if (path_line) { if (!this.fillatt.empty() && !options.Hist) { this.draw_g.append('svg:path') .attr('d', path_line + close_path) .call(this.fillatt.func); } this.draw_g.append('svg:path') .attr('d', path_line) .style('fill', 'none') .call(this.lineatt.func); } if (path_marker) { this.draw_g.append('svg:path') .attr('d', path_marker) .call(this.markeratt.func); } } else if (res && options.Hist) { this.draw_g.append('svg:path') .attr('d', res + ((!this.fillatt.empty() || fill_for_interactive) ? close_path : '')) .style('stroke-linejoin', 'miter') .call(this.lineatt.func) .call(this.fillatt.func); } return show_text ? this.finishTextDrawing() : true; }); } /** @summary Provide text information (tooltips) for histogram bin */ getBinTooltips(bin) { const tips = [], name = this.getObjectHint(), pmain = this.getFramePainter(), histo = this.getHisto(), xaxis = this.getAxis('x'), di = this.isDisplayItem() ? histo.stepx : 1, x1 = xaxis.GetBinCoord(bin), x2 = xaxis.GetBinCoord(bin+di), xlbl = this.getAxisBinTip('x', bin, di); let cont = histo.getBinContent(bin+1); if (name) tips.push(name); if (this.options.Error || this.options.Mark) { tips.push(`x = ${xlbl}`, `y = ${pmain.axisAsText('y', cont)}`); if (this.options.Error) { if (xlbl[0] === '[') tips.push('error x = ' + ((x2 - x1) / 2).toPrecision(4)); tips.push('error y = ' + histo.getBinError(bin + 1).toPrecision(4)); } } else { tips.push(`bin = ${bin+1}`, `x = ${xlbl}`); if (histo.$baseh) cont -= histo.$baseh.getBinContent(bin+1); const lbl = 'entries = ' + (di > 1 ? '~' : ''); if (cont === Math.round(cont)) tips.push(lbl + cont); else tips.push(lbl + floatToString(cont, gStyle.fStatFormat)); } return tips; } /** @summary Process tooltip event */ processTooltipEvent(pnt) { let ttrect = this.draw_g?.selectChild('.tooltip_bin'); if (!pnt || !this.draw_content || this.options.Mode3D || !this.draw_g) { ttrect?.remove(); return null; } const pmain = this.getFramePainter(), funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y), width = pmain.getFrameWidth(), height = pmain.getFrameHeight(), histo = this.getHisto(), xaxis = this.getAxis('x'), left = this.getSelectIndex('x', 'left', -1), right = this.getSelectIndex('x', 'right', 2); let show_rect, grx1, grx2, gry1, gry2, gapx = 2, l = left, r = right; function GetBinGrX(i) { const xx = xaxis.GetBinCoord(i); return (funcs.logx && (xx <= 0)) ? null : funcs.grx(xx); } function GetBinGrY(i) { const yy = histo.getBinContent(i + 1); if (funcs.logy && (yy < funcs.scale_ymin)) return funcs.swap_xy ? -1000 : 10*height; return Math.round(funcs.gry(yy)); } const pnt_x = funcs.swap_xy ? pnt.y : pnt.x, pnt_y = funcs.swap_xy ? pnt.x : pnt.y; while (l < r-1) { const m = Math.round((l+r)*0.5), xx = GetBinGrX(m); if ((xx === null) || (xx < pnt_x - 0.5)) if (funcs.swap_xy) r = m; else l = m; else if (xx > pnt_x + 0.5) if (funcs.swap_xy) l = m; else r = m; else { l++; r--; } } let findbin = r = l; grx1 = GetBinGrX(findbin); if (funcs.swap_xy) { while ((l > left) && (GetBinGrX(l-1) < grx1 + 2)) --l; while ((r < right) && (GetBinGrX(r+1) > grx1 - 2)) ++r; } else { while ((l > left) && (GetBinGrX(l-1) > grx1 - 2)) --l; while ((r < right) && (GetBinGrX(r+1) < grx1 + 2)) ++r; } if (l < r) { // many points can be assigned with the same cursor position // first try point around mouse y let best = height; for (let m = l; m <= r; m++) { const dist = Math.abs(GetBinGrY(m) - pnt_y); if (dist < best) { best = dist; findbin = m; } } // if best distance still too far from mouse position, just take from between if (best > height/10) findbin = Math.round(l + (r-l) / height * pnt_y); grx1 = GetBinGrX(findbin); } grx1 = Math.round(grx1); grx2 = Math.round(GetBinGrX(findbin+1)); if (this.options.Bar) { const w = grx2 - grx1; grx1 += Math.round(this.options.BarOffset*w); grx2 = grx1 + Math.round(this.options.BarWidth*w); } if (grx1 > grx2) [grx1, grx2] = [grx2, grx1]; if (this.isDisplayItem() && ((findbin <= histo.dx) || (findbin >= histo.dx + histo.nx))) { // special case when zoomed out of scale and bin is not available ttrect.remove(); return null; } const midx = Math.round((grx1 + grx2)/2), midy = gry1 = gry2 = GetBinGrY(findbin); if (this.options.Bar) { show_rect = true; gapx = 0; gry1 = this.getBarBaseline(funcs, height); if (gry1 > gry2) [gry1, gry2] = [gry2, gry1]; if (!pnt.touch && (pnt.nproc === 1)) if ((pnt_y < gry1) || (pnt_y > gry2)) findbin = null; } else if (this.options.Error || this.options.Mark) { show_rect = true; let msize = 3; if (this.markeratt) msize = Math.max(msize, this.markeratt.getFullSize()); if (this.options.Error) { const cont = histo.getBinContent(findbin+1), binerr = histo.getBinError(findbin+1); gry1 = Math.round(funcs.gry(cont + binerr)); // up gry2 = Math.round(funcs.gry(cont - binerr)); // down const dx = (grx2-grx1)*this.options.errorX; grx1 = Math.round(midx - dx); grx2 = Math.round(midx + dx); } // show at least 6 pixels as tooltip rect if (grx2 - grx1 < 2*msize) { grx1 = midx-msize; grx2 = midx+msize; } gry1 = Math.min(gry1, midy - msize); gry2 = Math.max(gry2, midy + msize); if (!pnt.touch && (pnt.nproc === 1)) if ((pnt_y < gry1) || (pnt_y > gry2)) findbin = null; } else if (this.options.Line) show_rect = false; else { // if histogram alone, use old-style with rects // if there are too many points at pixel, use circle show_rect = (pnt.nproc === 1) && (right-left < width); if (show_rect) { gry2 = height; if (!this.fillatt.empty()) { gry2 = Math.min(height, Math.max(0, Math.round(funcs.gry(0)))); if (gry2 < gry1) [gry1, gry2] = [gry2, gry1]; } // for mouse events pointer should be between y1 and y2 if (((pnt.y < gry1) || (pnt.y > gry2)) && !pnt.touch) findbin = null; } } if (findbin !== null) { // if bin on boundary found, check that x position is ok if ((findbin === left) && (grx1 > pnt_x + gapx)) findbin = null; else if ((findbin === right-1) && (grx2 < pnt_x - gapx)) findbin = null; else // if bars option used check that bar is not match if ((pnt_x < grx1 - gapx) || (pnt_x > grx2 + gapx)) findbin = null; else // exclude empty bin if empty bins suppressed if (!this.options.Zero && (histo.getBinContent(findbin+1) === 0)) findbin = null; } if ((findbin === null) || ((gry2 <= 0) || (gry1 >= height))) { ttrect.remove(); return null; } const res = { name: 'histo', title: histo.fTitle, x: midx, y: midy, exact: true, color1: this.lineatt?.color ?? 'green', color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue', lines: this.getBinTooltips(findbin) }; if (pnt.disabled) { // case when tooltip should not highlight bin ttrect.remove(); res.changed = true; } else if (show_rect) { if (ttrect.empty()) { ttrect = this.draw_g.append('svg:rect') .attr('class', 'tooltip_bin') .style('pointer-events', 'none') .call(addHighlightStyle); } res.changed = ttrect.property('current_bin') !== findbin; if (res.changed) { ttrect.attr('x', pmain.swap_xy ? gry1 : grx1) .attr('width', pmain.swap_xy ? gry2-gry1 : grx2-grx1) .attr('y', pmain.swap_xy ? grx1 : gry1) .attr('height', pmain.swap_xy ? grx2-grx1 : gry2-gry1) .style('opacity', '0.3') .property('current_bin', findbin); } res.exact = (Math.abs(midy - pnt_y) <= 5) || ((pnt_y>=gry1) && (pnt_y<=gry2)); res.menu = res.exact; // one could show context menu // distance to middle point, use to decide which menu to activate res.menu_dist = Math.sqrt((midx-pnt_x)**2 + (midy-pnt_y)**2); } else { const radius = this.lineatt.width + 3; if (ttrect.empty()) { ttrect = this.draw_g.append('svg:circle') .attr('class', 'tooltip_bin') .style('pointer-events', 'none') .attr('r', radius) .call(this.lineatt.func) .call(this.fillatt.func); } res.exact = (Math.abs(midx - pnt.x) <= radius) && (Math.abs(midy - pnt.y) <= radius); res.menu = res.exact; // show menu only when mouse pointer exactly over the histogram res.menu_dist = Math.sqrt((midx-pnt.x)**2 + (midy-pnt.y)**2); res.changed = ttrect.property('current_bin') !== findbin; if (res.changed) { ttrect.attr('cx', midx) .attr('cy', midy) .property('current_bin', findbin); } } if (res.changed) { res.user_info = { obj: histo, name: 'histo', bin: findbin, cont: histo.getBinContent(findbin+1), grx: midx, gry: midy }; } return res; } /** @summary Fill histogram context menu */ fillHistContextMenu(menu) { menu.add('Auto zoom-in', () => this.autoZoom()); const opts = this.getSupportedDrawOptions(); menu.addDrawMenu('Draw with', opts, arg => { if (arg.indexOf(kInspect) === 0) return this.showInspector(arg); this.decodeOptions(arg); // obsolete, should be implemented differently if (this.options.need_fillcol && this.fillatt?.empty()) this.fillatt.change(5, 1001); // redraw all objects this.interactiveRedraw('pad', 'drawopt'); }); } /** @summary Perform automatic zoom inside non-zero region of histogram */ autoZoom() { let left = this.getSelectIndex('x', 'left', -1), right = this.getSelectIndex('x', 'right', 1); const dist = right - left, histo = this.getHisto(), xaxis = this.getAxis('x'); if (dist === 0) return; // first find minimum let min = histo.getBinContent(left + 1); for (let indx = left; indx < right; ++indx) min = Math.min(min, histo.getBinContent(indx+1)); if (min > 0) return; // if all points positive, no chance for auto-scale while ((left < right) && (histo.getBinContent(left+1) <= min)) ++left; while ((left < right) && (histo.getBinContent(right) <= min)) --right; // if singular bin if ((left === right-1) && (left > 2) && (right < this.nbinsx-2)) { --left; ++right; } if ((right - left < dist) && (left < right)) return this.getFramePainter().zoom(xaxis.GetBinCoord(left), xaxis.GetBinCoord(right)); } /** @summary Checks if it makes sense to zoom inside specified axis range */ canZoomInside(axis, min, max) { const xaxis = this.getAxis('x'); if ((axis === 'x') && (xaxis.FindBin(max, 0.5) - xaxis.FindBin(min, 0) > 1)) return true; if ((axis === 'y') && (Math.abs(max-min) > Math.abs(this.ymax-this.ymin)*1e-6)) return true; return false; } /** @summary Call appropriate draw function */ async callDrawFunc(reason) { const main = this.getFramePainter(); if (main && (main.mode3d !== this.options.Mode3D) && !this.isMainPainter()) this.options.Mode3D = main.mode3d; return this.options.Mode3D ? this.draw3D(reason) : this.draw2D(reason); } /** @summary Draw in 2d */ async draw2D(reason) { this.clear3DScene(); return this.drawFrameAxes().then(res => { return res ? this.drawingBins(reason) : false; }).then(res => { if (res) return this.draw1DBins().then(() => this.addInteractivity()); }).then(() => this); } /** @summary Draw in 3d */ async draw3D(reason) { console.log('3D drawing is disabled, load ./hist/RH1Painter.mjs'); return this.draw2D(reason); } /** @summary Redraw histogram */ async redraw(reason) { return this.callDrawFunc(reason); } static async _draw(painter, opt) { return ensureRCanvas(painter).then(() => { painter.setAsMainPainter(); painter.options = { Hist: false, Bar: false, BarStyle: 0, Error: false, ErrorKind: -1, errorX: gStyle.fErrorX, Zero: false, Mark: false, Line: false, Fill: false, Lego: 0, Surf: 0, Text: false, TextAngle: 0, TextKind: '', AutoColor: 0, BarOffset: 0, BarWidth: 1, BaseLine: false, Mode3D: false, FrontBox: false, BackBox: false }; const d = new DrawOptions(opt); if (d.check('R3D_', true)) painter.options.Render3D = constants.Render3D.fromString(d.part.toLowerCase()); const kind = painter.v7EvalAttr('kind', 'hist'), sub = painter.v7EvalAttr('sub', 0), has_main = Boolean(painter.getMainPainter()), o = painter.options; o.Text = painter.v7EvalAttr('drawtext', false); o.BarOffset = painter.v7EvalAttr('baroffset', 0.0); o.BarWidth = painter.v7EvalAttr('barwidth', 1.0); o.second_x = has_main && painter.v7EvalAttr('secondx', false); o.second_y = has_main && painter.v7EvalAttr('secondy', false); switch (kind) { case 'bar': o.Bar = true; o.BarStyle = sub; break; case 'err': o.Error = true; o.ErrorKind = sub; break; case 'p': o.Mark = true; break; case 'l': o.Line = true; break; case 'lego': o.Lego = sub > 0 ? 10+sub : 12; o.Mode3D = true; break; default: o.Hist = true; } painter.scanContent(); return painter.callDrawFunc(); }); } /** @summary draw RH1 object */ static async draw(dom, histo, opt) { return RH1Painter._draw(new RH1Painter(dom, histo), opt); } } // class RH1Painter export { RH1Painter };