UNPKG

jsroot

Version:
1,339 lines (1,105 loc) 50.5 kB
import { gStyle, settings, clTF1, clTProfile, kNoZoom, kInspect, isFunc } from '../core.mjs'; import { rgb as d3_rgb } from '../d3.mjs'; import { floatToString, buildSvgCurve, addHighlightStyle } from '../base/BasePainter.mjs'; import { THistPainter } from './THistPainter.mjs'; import { getTF1Value } from '../base/func.mjs'; const PadDrawOptions = ['LOGXY', 'LOGX', 'LOGY', 'LOGZ', 'LOGV', 'LOG', 'LOG2X', 'LOG2Y', 'LOG2', 'LNX', 'LNY', 'LN', 'GRIDXY', 'GRIDX', 'GRIDY', 'TICKXY', 'TICKX', 'TICKY', 'TICKZ', 'FB', 'GRAYSCALE']; /** * @summary Painter for TH1 classes * @private */ class TH1Painter extends THistPainter { /** @summary Returns histogram * @desc Also assigns custom getBinContent method for TProfile if PROJX options specified */ getHisto() { const histo = super.getHisto(); if (histo?._typename === clTProfile) { if (!histo.$getBinContent) histo.$getBinContent = histo.getBinContent; switch (this.options?.ProfileProj) { case 'B': histo.getBinContent = histo.getBinEntries; break; case 'C=E': histo.getBinContent = histo.getBinError; break; case 'W': histo.getBinContent = function(i) { return this.$getBinContent(i) * this.getBinEntries(i); }; break; default: histo.getBinContent = histo.$getBinContent; break; } } return histo; } /** @summary Convert TH1K into normal binned histogram */ convertTH1K() { const histo = this.getObject(); if (histo.fReady) return; const arr = histo.fArray, entries = histo.fEntries; // array of values histo.fNcells = histo.fXaxis.fNbins + 2; histo.fArray = new Float64Array(histo.fNcells).fill(0); for (let n = 0; n < histo.fNIn; ++n) histo.Fill(arr[n]); histo.fReady = 1; histo.fEntries = entries; } /** @summary Scan content of 1-D histogram * @desc Detect min/max values for x and y axis * @param {boolean} when_axis_changed - true when zooming was changed, some checks may be skipped */ scanContent(when_axis_changed) { if (when_axis_changed && !this.nbinsx) when_axis_changed = false; if (this.isTH1K()) this.convertTH1K(); const histo = this.getHisto(); if (!when_axis_changed) this.extractAxesProperties(1); const left = this.getSelectIndex('x', 'left'), right = this.getSelectIndex('x', 'right'), pad_logy = this.getPadPainter()?.getPadLog(this.options.swap_xy() ? 'x' : 'y'), f1 = this.options.Func ? this.findFunction(clTF1) : null; if (when_axis_changed && (left === this.scan_xleft) && (right === this.scan_xright)) return; // Paint histogram axis only this.draw_content = !(this.options.Axis > 0); this.scan_xleft = left; this.scan_xright = right; const is_profile = this.isTProfile(), imin = Math.min(0, left), imax = Math.max(this.nbinsx, right); let hmin = 0, hmin_nz = 0, hmax = 0, hsum = 0, first = true, value, errs = { low: 0, up: 0 }; for (let i = imin; i < imax; ++i) { value = histo.getBinContent(i + 1); hsum += is_profile ? histo.fBinEntries[i + 1] : value; if ((i < left) || (i >= right)) continue; if ((value > 0) && ((hmin_nz === 0) || (value < hmin_nz))) hmin_nz = value; if (first) { hmin = hmax = value; first = false; } if (this.options.Error) errs = this.getBinErrors(histo, i + 1, value); hmin = Math.min(hmin, value - errs.low); hmax = Math.max(hmax, value + errs.up); if (f1) { // similar code as in THistPainter, line 7196 const x = histo.fXaxis.GetBinCenter(i + 1), v = getTF1Value(f1, x); if (v !== undefined) { hmax = Math.max(hmax, v); if (pad_logy && (value > 0) && (v > 0.3 * value)) hmin_nz = Math.min(hmin_nz, v); } } } // account overflow/underflow bins if (is_profile) hsum += histo.fBinEntries[0] + histo.fBinEntries[this.nbinsx + 1]; else hsum += histo.getBinContent(0) + histo.getBinContent(this.nbinsx + 1); 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; let set_zoom = false; if (this.draw_content || (this.isMainPainter() && (this.options.Axis > 0) && !this.options.ohmin && !this.options.ohmax && (histo.fMinimum === kNoZoom) && (histo.fMaximum === kNoZoom))) { 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 if (pad_logy) { this.ymin = (hmin_nz || hmin) * 0.5; this.ymax = hmax*2*(0.9/0.95); } else { this.ymin = hmin; this.ymax = hmax; } } hmin = this.options.minimum; hmax = this.options.maximum; if ((hmin === hmax) && (hmin !== kNoZoom)) { if (hmin < 0) { hmin *= 2; hmax = 0; } else { hmin = 0; hmax *= 2; if (!hmax) hmax = 1; } } let fix_min = false, fix_max = false; if (this.options.ohmin && this.options.ohmax && !this.draw_content) { // case of hstack drawing, zooming allowed only when flag is provided if (this.options.zoom_min_max) { if ((hmin !== kNoZoom) && (hmin <= this.ymin)) hmin = kNoZoom; if ((hmax !== kNoZoom) && (hmax >= this.ymax)) hmax = kNoZoom; set_zoom = true; } else hmin = hmax = kNoZoom; } else if ((hmin !== kNoZoom) && (hmax !== kNoZoom) && !this.draw_content && ((this.ymin === this.ymax) || (this.ymin > hmin) || (this.ymax < hmax))) { // often appears with TF1 painter where Y range is not set properly this.ymin = hmin; this.ymax = hmax; fix_min = fix_max = true; } else { if (hmin !== kNoZoom) { fix_min = true; if (hmin < this.ymin) this.ymin = hmin; set_zoom = true; } if (hmax !== kNoZoom) { fix_max = true; if (hmax > this.ymax) this.ymax = hmax; set_zoom = true; } } // final adjustment like in THistPainter.cxx line 7309 if (!this._exact_y_range && !pad_logy) { if (!fix_min) { if ((this.options.BaseLine !== false) && (this.ymin >= 0)) this.ymin = 0; else { const positive = (this.ymin >= 0); this.ymin -= gStyle.fHistTopMargin*(this.ymax - this.ymin); if (positive && (this.ymin < 0)) this.ymin = 0; } } if (!fix_max) this.ymax += gStyle.fHistTopMargin*(this.ymax - this.ymin); } // always set zoom when hmin/hmax is configured // fMinimum/fMaximum values is a way how ROOT handles Y scale zooming for TH1 if (!when_axis_changed) { if (set_zoom && ((hmin !== kNoZoom) || (hmax !== kNoZoom))) { this.zoom_ymin = (hmin === kNoZoom) ? this.ymin : hmin; this.zoom_ymax = (hmax === kNoZoom) ? this.ymax : hmax; } else { delete this.zoom_ymin; delete this.zoom_ymax; } } // used in FramePainter.isAllowedDefaultYZooming this.wheel_zoomy = (this.getDimension() > 1) || !this.draw_content; } /** @summary Count histogram statistic */ countStat(cond, count_skew) { const profile = this.isTProfile(), histo = this.getHisto(), xaxis = histo.fXaxis, left = this.getSelectIndex('x', 'left'), right = this.getSelectIndex('x', 'right'), fp = this.getFramePainter(), res = { name: histo.fName, meanx: 0, meany: 0, rmsx: 0, rmsy: 0, integral: 0, entries: (histo.fEntries > 0) ? histo.fEntries : this.stat_entries, eff_entries: 0, xmax: 0, wmax: 0, skewx: 0, skewd: 0, kurtx: 0, kurtd: 0 }, has_counted_stat = !fp.isAxisZoomed('x') && (Math.abs(histo.fTsumw) > 1e-300); let stat_sumw = 0, stat_sumw2 = 0, stat_sumwx = 0, stat_sumwx2 = 0, stat_sumwy = 0, stat_sumwy2 = 0, i, xx, w, xmax = null, wmax = null; if (!isFunc(cond)) cond = null; for (i = left; i < right; ++i) { xx = xaxis.GetBinCoord(i + 0.5); if (cond && !cond(xx)) continue; if (profile) { w = histo.fBinEntries[i + 1]; stat_sumwy += histo.fArray[i + 1]; stat_sumwy2 += histo.fSumw2[i + 1]; } else w = histo.getBinContent(i + 1); if ((xmax === null) || (w > wmax)) { xmax = xx; wmax = w; } if (!has_counted_stat) { stat_sumw += w; stat_sumw2 += w * w; stat_sumwx += w * xx; stat_sumwx2 += w * xx**2; } } // when no range selection done, use original statistic from histogram if (has_counted_stat) { stat_sumw = histo.fTsumw; stat_sumw2 = histo.fTsumw2; stat_sumwx = histo.fTsumwx; stat_sumwx2 = histo.fTsumwx2; } res.integral = stat_sumw; res.eff_entries = stat_sumw2 ? stat_sumw*stat_sumw/stat_sumw2 : Math.abs(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; } if (count_skew) { let sum3 = 0, sum4 = 0, np = 0; for (i = left; i < right; ++i) { xx = xaxis.GetBinCoord(i + 0.5); if (cond && !cond(xx)) continue; w = profile ? histo.fBinEntries[i + 1] : histo.getBinContent(i + 1); np += w; sum3 += w * Math.pow(xx - res.meanx, 3); sum4 += w * Math.pow(xx - res.meanx, 4); } const stddev3 = Math.pow(res.rmsx, 3), stddev4 = Math.pow(res.rmsx, 4); if (np * stddev3 !== 0) res.skewx = sum3 / (np * stddev3); res.skewd = res.eff_entries > 0 ? Math.sqrt(6/res.eff_entries) : 0; if (np * stddev4 !== 0) res.kurtx = sum4 / (np * stddev4) - 3; res.kurtd = res.eff_entries > 0 ? Math.sqrt(24/res.eff_entries) : 0; } return res; } /** @summary Fill stat box */ fillStatistic(stat, dostat, dofit) { // no need to refill statistic if histogram is dummy if (this.isIgnoreStatsFill()) return false; if (dostat === 1) dostat = 1111; if (dofit === 1) dofit = 111; const histo = this.getHisto(), 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, data = this.countStat(undefined, (print_skew > 0) || (print_kurt > 0)); // make empty at the beginning stat.clearPave(); if (print_name > 0) stat.addText(data.name); if (this.isTProfile()) { if (print_entries > 0) stat.addText('Entries = ' + stat.format(data.entries, 'entries')); if (print_mean > 0) { stat.addText('Mean = ' + stat.format(data.meanx)); stat.addText('Mean y = ' + stat.format(data.meany)); } if (print_rms > 0) { stat.addText('Std Dev = ' + stat.format(data.rmsx)); stat.addText('Std Dev y = ' + stat.format(data.rmsy)); } } else { 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.fArray.length > 0) ? histo.fArray[0] : 0, 'entries')); if (print_over > 0) stat.addText('Overflow = ' + stat.format((histo.fArray.length > 0) ? histo.fArray.at(-1) : 0, 'entries')); if (print_integral > 0) stat.addText('Integral = ' + stat.format(data.integral, 'entries')); if (print_skew === 2) stat.addText(`Skewness = ${stat.format(data.skewx)} #pm ${stat.format(data.skewd)}`); else if (print_skew > 0) stat.addText(`Skewness = ${stat.format(data.skewx)}`); if (print_kurt === 2) stat.addText(`Kurtosis = ${stat.format(data.kurtx)} #pm ${stat.format(data.kurtd)}`); else if (print_kurt > 0) stat.addText(`Kurtosis = ${stat.format(data.kurtx)}`); } if (dofit) stat.fillFunctionStat(this.findFunction(clTF1), dofit, 1); return true; } /** @summary Get baseline for bar drawings */ 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(funcs, height) { const left = this.getSelectIndex('x', 'left', -1), right = this.getSelectIndex('x', 'right', 1), histo = this.getHisto(), xaxis = histo.fXaxis, show_text = this.options.Text; let text_col, text_angle, text_size, side = (this.options.BarStyle > 10) ? this.options.BarStyle % 10 : 0, pr = Promise.resolve(); if (side > 4) side = 4; const gry2 = this.getBarBaseline(funcs, height); if (show_text) { text_col = this.getColor(histo.fMarkerColor); text_angle = -1*this.options.TextAngle; text_size = 20; if ((histo.fMarkerSize !== 1) && text_angle) text_size = 0.02*height*histo.fMarkerSize; pr = this.startTextDrawingAsync(42, text_size, this.draw_g, text_size); } return pr.then(() => { let bars = '', barsl = '', barsr = ''; for (let i = left; i < right; ++i) { const x1 = xaxis.GetBinLowEdge(i + 1), x2 = xaxis.GetBinLowEdge(i + 2); if (funcs.logx && (x2 <= 0)) continue; let grx1 = Math.round(funcs.grx(x1)), grx2 = Math.round(funcs.grx(x2)), w = grx2 - grx1; const y = histo.getBinContent(i+1); if (funcs.logy && (y < funcs.scale_ymin)) continue; const gry1 = Math.round(funcs.gry(y)); grx1 += Math.round(histo.fBarOffset/1000*w); w = Math.round(histo.fBarWidth/1000*w); if (funcs.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 (side > 0) { grx2 = grx1 + w; w = Math.round(w * side / 10); if (funcs.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 (show_text && y) { const text = (y === Math.round(y)) ? y.toString() : floatToString(y, gStyle.fPaintTextFormat); if (funcs.swap_xy) this.drawText({ align: 12, x: Math.round(gry1 + text_size/2), y: Math.round(grx1+0.1), height: Math.round(w*0.8), text, color: text_col, latex: 0 }); else if (text_angle) this.drawText({ align: 12, x: grx1+w/2, y: Math.round(gry1 - 2 - text_size/5), width: 0, height: 0, rotate: text_angle, text, color: text_col, latex: 0 }); else this.drawText({ align: 22, x: Math.round(grx1 + w*0.1), y: Math.round(gry1 - 2 - text_size), width: Math.round(w*0.8), height: text_size, text, color: text_col, latex: 0 }); } } 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()); } if (show_text) return this.finishTextDrawing(); }); } /** @summary Draw histogram as filled errors */ drawFilledErrors(funcs) { const left = this.getSelectIndex('x', 'left', 0), right = this.getSelectIndex('x', 'right', 0), histo = this.getHisto(), bins1 = [], bins2 = []; for (let i = left; i < right; ++i) { const x = histo.fXaxis.GetBinCoord(i+0.5); if (funcs.logx && (x <= 0)) continue; const grx = Math.round(funcs.grx(x)), y = histo.getBinContent(i+1), yerrs = this.getBinErrors(histo, i + 1, y); if (funcs.logy && (y - yerrs.low < funcs.scale_ymin)) continue; bins1.push({ grx, gry: Math.round(funcs.gry(y + yerrs.up)) }); bins2.unshift({ grx, gry: Math.round(funcs.gry(y - yerrs.low)) }); } const line = this.options.ErrorKind !== 4, path1 = buildSvgCurve(bins1, { line }), path2 = buildSvgCurve(bins2, { line, cmd: 'L' }); this.draw_g.append('svg:path') .attr('d', path1 + path2 + 'Z') .call(this.fillatt.func); } /** @summary Draw TH1 as hist/line/curve * @return Promise or scalar value */ async drawNormal(funcs, width, height) { const left = this.getSelectIndex('x', 'left', -1), right = this.getSelectIndex('x', 'right', 2), histo = this.getHisto(), want_tooltip = !this.isBatchMode() && settings.Tooltip, xaxis = histo.fXaxis, exclude_zero = !this.options.Zero, show_errors = this.options.Error, show_curve = this.options.Curve, show_text = this.options.Text, text_profile = show_text && (this.options.TextKind === 'E') && this.isTProfile() && histo.fBinEntries, grpnts = []; let res = '', lastbin = false, show_markers = this.options.Mark, show_line = this.options.Line, startx, startmidx, 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 = '', hints_err = null, hints_marker = null, hsz = 5, do_marker = false, do_err = false, dend = 0, dlw = 0, my, yerr1, yerr2, bincont, binerr, mx1, mx2, midx, lx, ly, mmx1, mmx2, text_col, text_angle, text_size, pr = Promise.resolve(); if (show_errors && !show_markers && (histo.fMarkerStyle > 1)) show_markers = true; if (this.options.ErrorKind === 2) { if (this.fillatt.empty()) show_markers = true; else path_fill = ''; } else if (show_errors) { show_line = false; path_err = ''; hints_err = want_tooltip ? '' : null; do_err = true; } dlw = this.lineatt.width + gStyle.fEndErrorSize; if (this.options.ErrorKind === 1) dend = Math.floor((this.lineatt.width-1)/2); if (show_markers) { // draw markers also when e2 option was specified this.createAttMarker({ attr: histo, style: this.options.MarkStyle }); // when style not configured, it will be ignored if (this.markeratt.size > 0) { // simply use relative move from point, can optimize in the future path_marker = ''; do_marker = true; this.markeratt.resetPos(); if ((hints_err === null) && want_tooltip && (!this.markeratt.fill || (this.markeratt.getFullSize() < 7))) { hints_marker = ''; hsz = Math.max(5, Math.round(this.markeratt.getFullSize()*0.7)); } } else show_markers = false; } const draw_markers = show_errors || show_markers, draw_any_but_hist = draw_markers || show_text || show_line || show_curve, draw_hist = this.options.Hist && (!this.lineatt.empty() || !this.fillatt.empty()), check_sumw2 = show_errors && histo.fSumw2?.length, // 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 use_minmax = draw_any_but_hist || ((right - left) > 3*width); if (!draw_hist && !draw_any_but_hist) return this.removeG(); if (show_text) { text_col = this.getColor(histo.fMarkerColor); text_angle = -1*this.options.TextAngle; text_size = 20; if ((histo.fMarkerSize !== 1) && text_angle) text_size = 0.02*height*histo.fMarkerSize; if (!text_angle && !this.options.TextKind) { const space = width / (right - left + 1); if (space < 3 * text_size) { text_angle = 270; text_size = Math.round(space*0.7); } } pr = this.startTextDrawingAsync(42, text_size, this.draw_g, text_size); } return pr.then(() => { // just to get correct values for the specified bin const extract_bin = bin => { bincont = histo.getBinContent(bin+1); if (exclude_zero && (bincont === 0) && (!check_sumw2 || !histo.fSumw2[bin+1])) return false; mx1 = Math.round(funcs.grx(xaxis.GetBinLowEdge(bin+1))); mx2 = Math.round(funcs.grx(xaxis.GetBinLowEdge(bin+2))); midx = Math.round((mx1 + mx2) / 2); if (startmidx === undefined) startmidx = midx; my = Math.round(funcs.gry(bincont)); if (show_errors) { binerr = this.getBinErrors(histo, bin + 1, bincont); yerr1 = Math.round(my - funcs.gry(bincont + binerr.up)); // up yerr2 = Math.round(funcs.gry(bincont - binerr.low) - my); // low } else yerr1 = yerr2 = 20; return true; }, draw_errbin = () => { let edx = 5; if (this.options.errorX > 0) { edx = Math.round((mx2 - mx1) * this.options.errorX); mmx1 = midx - edx; mmx2 = midx + edx; if (this.options.ErrorKind === 1) path_err += `M${mmx1+dend},${my-dlw}v${2*dlw}m0,-${dlw}h${mmx2-mmx1-2*dend}m0,-${dlw}v${2*dlw}`; else path_err += `M${mmx1+dend},${my}h${mmx2-mmx1-2*dend}`; } if (this.options.ErrorKind === 1) path_err += `M${midx-dlw},${my-yerr1+dend}h${2*dlw}m${-dlw},0v${yerr1+yerr2-2*dend}m${-dlw},0h${2*dlw}`; else path_err += `M${midx},${my-yerr1+dend}v${yerr1+yerr2-2*dend}`; if (hints_err !== null) { const he1 = Math.max(yerr1, 5), he2 = Math.max(yerr2, 5); hints_err += `M${midx-edx},${my-he1}h${2*edx}v${he1+he2}h${-2*edx}z`; } }, draw_marker = () => { if (funcs.swap_xy) { path_marker += this.markeratt.create(my, midx); if (hints_marker !== null) hints_marker += `M${my-hsz},${midx-hsz}v${2*hsz}h${2*hsz}v${-2*hsz}z`; } else { path_marker += this.markeratt.create(midx, my); if (hints_marker !== null) hints_marker += `M${midx-hsz},${my-hsz}h${2*hsz}v${2*hsz}h${-2*hsz}z`; } }, draw_bin = bin => { if (extract_bin(bin)) { if (show_text) { const cont = text_profile ? histo.fBinEntries[bin+1] : bincont; if (cont !== 0) { const arg = text_angle ? { align: 12, x: midx, y: Math.round(my - 2 - text_size / 5), width: 0, height: 0, rotate: text_angle } : { align: 22, x: Math.round(mx1 + (mx2 - mx1) * 0.1), y: Math.round(my - 2 - text_size), width: Math.round((mx2 - mx1) * 0.8), height: text_size }; arg.text = (cont === Math.round(cont)) ? cont.toString() : floatToString(cont, gStyle.fPaintTextFormat); arg.color = text_col; arg.latex = 0; if (funcs.swap_xy) { arg.x = my; arg.y = Math.round(midx - text_size/2); } this.drawText(arg); } } if (show_line) { if (funcs.swap_xy) path_line += (path_line ? 'L' : 'M') + `${my},${midx}`; // no optimization else if (path_line.length === 0) path_line = `M${midx},${my}`; else if (lx === midx) path_line += `v${my-ly}`; else if (ly === my) path_line += `h${midx-lx}`; else path_line += `l${midx-lx},${my-ly}`; lx = midx; ly = my; } else if (show_curve) grpnts.push({ grx: (mx1 + mx2) / 2, gry: funcs.gry(bincont) }); 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) && do_marker) draw_marker(); if ((path_err !== null) && do_err) draw_errbin(); } } } }; // check if we should draw markers or error marks directly, skipping optimization if (do_marker || do_err) { if (!settings.OptimizeDraw || ((right-left < 50000) && (settings.OptimizeDraw === 1))) { for (i = left; i < right; ++i) { if (extract_bin(i)) { if (path_marker !== null) draw_marker(); if (path_err !== null) draw_errbin(); } } do_err = do_marker = false; } } for (i = left; i <= right; ++i) { x = xaxis.GetBinLowEdge(i+1); if (this.logx && (x <= 0)) continue; grx = Math.round(funcs.grx(x)); lastbin = (i === right); 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_any_but_hist) { 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 at same X differs, need complete logic if (draw_hist && ((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; } // end of use_minmax } 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 = want_tooltip && this.fillatt.empty() && draw_hist && !draw_markers && !show_line && !show_curve && !this._ignore_frame; 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`, add_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); }; if (res && draw_hist && !this.fillatt.empty()) { add_hist(); res = ''; } if (draw_markers || show_line || show_curve) { if (!path_line && grpnts.length) { if (funcs.swap_xy) grpnts.forEach(pnt => { const d = pnt.grx; pnt.grx = pnt.gry; pnt.gry = d; }); path_line = buildSvgCurve(grpnts); } if (path_fill) { this.draw_g.append('svg:path') .attr('d', path_fill) .call(this.fillatt.func); } else if (path_line && !this.fillatt.empty() && !draw_hist) { this.draw_g.append('svg:path') .attr('d', path_line + `L${midx},${h0}H${startmidx}Z`) .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) { 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); } if (hints_marker) { this.draw_g.append('svg:path') .attr('d', hints_marker) .style('fill', 'none') .style('pointer-events', this.isBatchMode() ? null : 'visibleFill'); } } if (res && draw_hist) add_hist(); if (show_text) return this.finishTextDrawing(); }); } /** @summary Draw TH1 bins in SVG element * @return Promise or scalar value */ draw1DBins() { if (this.options.Same && this._ignore_frame) this.getFrameSvg().style('display', 'none'); this.createHistDrawAttributes(); const pmain = this.getFramePainter(), funcs = this.getHistGrFuncs(pmain), width = pmain.getFrameWidth(), height = pmain.getFrameHeight(); if (!this.draw_content || (width <= 0) || (height <= 0)) return this.removeG(); this.createG(!this._ignore_frame); if (this.options.Bar) { return this.drawBars(funcs, height).then(() => { if (this.options.ErrorKind === 1) return this.drawNormal(funcs, width, height); }); } if ((this.options.ErrorKind === 3) || (this.options.ErrorKind === 4)) return this.drawFilledErrors(funcs); return this.drawNormal(funcs, width, height); } /** @summary Provide text information (tooltips) for histogram bin */ getBinTooltips(bin) { const tips = [], name = this.getObjectHint(), pmain = this.getFramePainter(), funcs = this.getHistGrFuncs(pmain), histo = this.getHisto(), x1 = histo.fXaxis.GetBinLowEdge(bin+1), x2 = histo.fXaxis.GetBinLowEdge(bin+2), xlbl = this.getAxisBinTip('x', histo.fXaxis, bin); let cont = histo.getBinContent(bin+1); if (name) tips.push(name); if (this.options.Error || this.options.Mark || this.isTF1()) { tips.push(`x = ${xlbl}`, `y = ${funcs.axisAsText('y', cont)}`); if (this.options.Error) { if (xlbl[0] === '[') tips.push(`error x = ${((x2 - x1) / 2).toPrecision(4)}`); const errs = this.getBinErrors(histo, bin + 1, cont); if (errs.poisson) tips.push(`error low = ${errs.low.toPrecision(4)}`, `error up = ${errs.up.toPrecision(4)}`); else tips.push(`error y = ${errs.up.toPrecision(4)}`); } } else { tips.push(`bin = ${bin+1}`, `x = ${xlbl}`); if (histo.$baseh) cont -= histo.$baseh.getBinContent(bin+1); if (cont === Math.round(cont)) tips.push(`entries = ${cont}`); else tips.push(`entries = ${floatToString(cont, gStyle.fStatFormat)}`); } return tips; } /** @summary Process tooltip event */ processTooltipEvent(pnt) { if (!pnt || !this.draw_content || !this.draw_g || this.options.Mode3D) { this.draw_g?.selectChild('.tooltip_bin').remove(); return null; } const pmain = this.getFramePainter(), funcs = this.getHistGrFuncs(pmain), histo = this.getHisto(), left = this.getSelectIndex('x', 'left', -1), right = this.getSelectIndex('x', 'right', 2); let width = pmain.getFrameWidth(), height = pmain.getFrameHeight(), show_rect, grx1, grx2, gry1, gry2, gapx = 2, l = left, r = right, pnt_x = pnt.x, pnt_y = pnt.y; const GetBinGrX = i => { const xx = histo.fXaxis.GetBinLowEdge(i+1); return (funcs.logx && (xx <= 0)) ? null : funcs.grx(xx); }, 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)); }; if (funcs.swap_xy) [pnt_x, pnt_y, width, height] = [pnt_y, pnt_x, height, width]; const descent_order = funcs.swap_xy !== pmain.x_handle.reverse; while (l < r-1) { const m = Math.round((l+r)*0.5), xx = GetBinGrX(m); if ((xx === null) || (xx < pnt_x - 0.5)) if (descent_order) r = m; else l = m; else if (xx > pnt_x + 0.5) if (descent_order) l = m; else r = m; else { l++; r--; } } let findbin = r = l; grx1 = GetBinGrX(findbin); if (descent_order) { 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(histo.fBarOffset / 1000 * w); grx2 = grx1 + Math.round(histo.fBarWidth / 1000 * w); } if (grx1 > grx2) [grx1, grx2] = [grx2, grx1]; 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.Hist !== true)) || this.options.Mark || this.options.Line || this.options.Curve) { show_rect = !this.isTF1(); let msize = 3; if (this.markeratt) msize = Math.max(msize, this.markeratt.getFullSize()); if (this.options.Error) { const cont = histo.getBinContent(findbin + 1), binerrs = this.getBinErrors(histo, findbin + 1, cont); gry1 = Math.round(funcs.gry(cont + binerrs.up)); // up gry2 = Math.round(funcs.gry(cont - binerrs.low)); // low if ((cont === 0) && this.isTProfile()) findbin = null; 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 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 ((pnt_x < grx1 - gapx) || (pnt_x > grx2 + gapx)) findbin = null; // if bars option used check that bar is not match else if (!this.options.Zero && (histo.getBinContent(findbin+1) === 0) && (histo.getBinError(findbin+1) === 0)) findbin = null; // exclude empty bin if empty bins suppressed } let ttrect = this.draw_g.selectChild('.tooltip_bin'); if ((findbin === null) || ((gry2 <= 0) || (gry1 >= height))) { ttrect.remove(); return null; } const res = { name: this.getObjectName(), 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', funcs.swap_xy ? gry1 : grx1) .attr('width', funcs.swap_xy ? gry2-gry1 : grx2-grx1) .attr('y', funcs.swap_xy ? grx1 : gry1) .attr('height', funcs.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 when histogram is selected // 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.fName, 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); if (this.options.need_fillcol && this.fillatt?.empty()) this.fillatt.change(5, 1001); // redraw all objects in pad, inform dependent objects this.interactiveRedraw('pad', 'drawopt'); }); if (!this.snapid && !this.isTProfile() && !this.isTF1()) menu.addRebinMenu(sz => this.rebinHist(sz)); } /** @summary Rebin histogram, used via context menu */ rebinHist(sz) { const histo = this.getHisto(), xaxis = histo.fXaxis, nbins = Math.floor(xaxis.fNbins/ sz); if (nbins < 2) return; const arr = new Array(nbins+2), xbins = (xaxis.fXbins.length > 0) ? new Array(nbins) : null; arr[0] = histo.fArray[0]; let indx = 1; for (let i = 1; i <= nbins; ++i) { if (xbins) xbins[i-1] = xaxis.fXbins[indx-1]; let sum = 0; for (let k = 0; k < sz; ++k) sum += histo.fArray[indx++]; arr[i] = sum; } if (xbins) { if (indx <= xaxis.fXbins.length) xaxis.fXmax = xaxis.fXbins[indx-1]; xaxis.fXbins = xbins; } else xaxis.fXmax = xaxis.fXmin + (xaxis.fXmax - xaxis.fXmin) / xaxis.fNbins * nbins * sz; xaxis.fNbins = nbins; let overflow = 0; while (indx < histo.fArray.length) overflow += histo.fArray[indx++]; arr[nbins+1] = overflow; histo.fArray = arr; histo.fSumw2 = []; this.scanContent(); this.interactiveRedraw('pad'); } /** @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(); if ((dist === 0) || !histo) 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(histo.fXaxis.GetBinLowEdge(left+1), histo.fXaxis.GetBinLowEdge(right+1)); } /** @summary Checks if it makes sense to zoom inside specified axis range */ canZoomInside(axis, min, max) { const histo = this.getHisto(); if ((axis === 'x') && histo && (histo.fXaxis.FindBin(max, 0.5) - histo.fXaxis.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 Performs 2D drawing of histogram * @return {Promise} when ready */ async draw2D(reason) { this.clear3DScene(); this.scanContent(reason === 'zoom'); const pr = this.isMainPainter() ? this.drawColorPalette(false) : Promise.resolve(true); return pr.then(() => this.drawAxes()) .then(() => this.draw1DBins()) .then(() => this.updateFunctions()) .then(() => this.updateHistTitle()) .then(() => { this.updateStatWebCanvas(); return this.addInteractivity(); }); } /** @summary Should performs 3D drawing of histogram * @desc Disable in 2D case, just draw with default options * @return {Promise} when ready */ async draw3D(reason) { console.log('3D drawing is disabled, load ./hist/TH1Painter.mjs'); return this.draw2D(reason); } /** @summary Call drawing function depending from 3D mode */ async callDrawFunc(reason) { const main = this.getMainPainter(), fp = this.getFramePainter(); if ((main !== this) && fp && (fp.mode3d !== this.options.Mode3D)) this.copyOptionsFrom(main); if (!this.options.Mode3D) return this.draw2D(reason); return this.draw3D(reason).catch(err => { const cp = this.getCanvPainter(); if (isFunc(cp?.showConsoleError)) cp.showConsoleError(err);