UNPKG

jsroot

Version:
1,376 lines (1,145 loc) 95.4 kB
import { gStyle, BIT, settings, constants, create, isObject, isFunc, isStr, getPromise, clTList, clTPaveText, clTPaveStats, clTPaletteAxis, clTProfile, clTProfile2D, clTProfile3D, clTPad, clTAxis, clTF1, clTF2, kNoZoom, clTCutG, kNoStats, kTitle, setHistogramTitle } from '../core.mjs'; import { getColor, getColorPalette } from '../base/colors.mjs'; import { DrawOptions } from '../base/BasePainter.mjs'; import { ObjectPainter, EAxisBits, kAxisTime, kAxisLabels } from '../base/ObjectPainter.mjs'; import { TPavePainter, kPosTitle } from '../hist/TPavePainter.mjs'; import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs'; import { gamma_quantile, gamma_quantile_c } from '../base/math.mjs'; const kCARTESIAN = 1, kPOLAR = 2, kCYLINDRICAL = 3, kSPHERICAL = 4, kRAPIDITY = 5, kNormal = 0, kPoisson = 1, kPoisson2 = 2; /** * @summary Class to decode histograms draw options * @desc All options started from capital letter are major drawing options * any other draw options are internal settings. * @private */ class THistDrawOptions { constructor() { this.reset(); } /** @summary Reset hist draw options */ reset() { Object.assign(this, { Axis: 0, RevX: false, RevY: false, SymlogX: 0, SymlogY: 0, Bar: false, BarStyle: 0, Curve: false, Hist: 1, Line: false, Fill: false, Error: 0, ErrorKind: -1, errorX: gStyle.fErrorX, Mark: false, Same: false, Scat: false, ScatCoef: 1.0, Func: true, AllFunc: false, Arrow: false, Box: false, BoxStyle: 0, Text: false, TextAngle: 0, TextKind: '', Char: 0, Color: false, Contour: 0, Cjust: false, Lego: 0, Surf: 0, Off: 0, Tri: 0, Proj: 0, AxisPos: 0, Ortho: gStyle.fOrthoCamera, Spec: false, Pie: false, List: false, Zscale: false, Zvert: true, PadPalette: false, Candle: '', Violin: '', Scaled: null, Circular: 0, Poisson: kNormal, GLBox: 0, GLColor: false, Project: '', ProfileProj: '', Profile2DProj: '', System: kCARTESIAN, AutoColor: false, NoStat: false, ForceStat: false, PadStats: false, PadTitle: false, AutoZoom: false, HighRes: 0, Zero: 1, Palette: 0, BaseLine: false, ShowEmpty: false, Optimize: settings.OptimizeDraw, Mode3D: false, x3dscale: 1, y3dscale: 1, SwapXY: false, Render3D: constants.Render3D.Default, FrontBox: true, BackBox: true, need_fillcol: false, minimum: kNoZoom, maximum: kNoZoom, ymin: 0, ymax: 0, cutg: null, IgnoreMainScale: false, IgnorePalette: false }); } isCartesian() { return this.System === kCARTESIAN; } is3d() { return this.Lego || this.Surf; } /** @summary Base on sumw2 values (re)set some basic draw options, only for 1dim hist */ decodeSumw2(histo, force) { const len = histo.fSumw2?.length ?? 0; let isany = false; for (let n = 0; n < len; ++n) if (histo.fSumw2[n] > 0) { isany = true; break; } if (Number.isInteger(this.Error) || force) this.Error = isany ? 1 : 0; if (Number.isInteger(this.Hist) || force) this.Hist = isany ? 0 : 1; if (Number.isInteger(this.Zero) || force) this.Zero = isany ? 0 : 1; } /** @summary Is palette can be used with current draw options */ canHavePalette() { if (this.ndim === 3) return this.BoxStyle === 12 || this.BoxStyle === 13 || this.GLBox === 12; if (this.ndim === 1) return this.Lego === 12 || this.Lego === 14; if (this.Mode3D) return this.Lego === 12 || this.Lego === 14 || this.Surf === 11 || this.Surf === 12; if (this.Color || this.Contour || this.Hist || this.Axis) return true; return !this.Scat && !this.Box && !this.Arrow && !this.Proj && !this.Candle && !this.Violin && !this.Text; } /** @summary Decode histogram draw options */ decode(opt, hdim, histo, pp, pad, painter) { this.orginal = opt; // will be overwritten by storeDrawOpt call this.cutg_name = ''; if (isStr(opt) && (hdim === 2)) { const p1 = opt.lastIndexOf('['), p2 = opt.lastIndexOf(']'); if ((p1 >= 0) && (p2 > p1+1)) { this.cutg_name = opt.slice(p1+1, p2); opt = opt.slice(0, p1) + opt.slice(p2+1); this.cutg = pp?.findInPrimitives(this.cutg_name, clTCutG); if (this.cutg) this.cutg.$redraw_pad = true; } } const d = new DrawOptions(opt); if (hdim === 1) this.decodeSumw2(histo, true); this.ndim = hdim || 1; // keep dimensions, used for now in GED // for old web canvas json // TODO: remove in version 8 d.check('USE_PAD_TITLE'); d.check('USE_PAD_PALETTE'); d.check('USE_PAD_STATS'); if (d.check('IGNORE_PALETTE')) this.IgnorePalette = true; if (d.check('PAL', true)) this.Palette = d.partAsInt(); // this is zooming of histogram content if (d.check('MINIMUM:', true)) { this.ominimum = true; this.minimum = parseFloat(d.part); } else { this.ominimum = false; this.minimum = histo.fMinimum; } if (d.check('MAXIMUM:', true)) { this.omaximum = true; this.maximum = parseFloat(d.part); } else { this.omaximum = false; this.maximum = histo.fMaximum; } if (!this.ominimum && !this.omaximum && this.minimum === this.maximum) this.minimum = this.maximum = kNoZoom; if (d.check('HMIN:', true)) { this.ohmin = true; this.hmin = parseFloat(d.part); } else { this.ohmin = false; delete this.hmin; } if (d.check('HMAX:', true)) { this.ohmax = true; this.hmax = parseFloat(d.part); } else { this.ohmax = false; delete this.hmax; } this.zoom_min_max = d.check('ZOOM_MIN_MAX'); // let configure histogram titles - only for debug purposes if (d.check('HTITLE:', true)) histo.fTitle = decodeURIComponent(d.part.toLowerCase()); if (d.check('XTITLE:', true)) histo.fXaxis.fTitle = decodeURIComponent(d.part.toLowerCase()); if (d.check('YTITLE:', true)) histo.fYaxis.fTitle = decodeURIComponent(d.part.toLowerCase()); if (d.check('ZTITLE:', true)) histo.fZaxis.fTitle = decodeURIComponent(d.part.toLowerCase()); if (d.check('POISSON2')) this.Poisson = kPoisson2; if (d.check('POISSON')) this.Poisson = kPoisson; if (d.check('SHOWEMPTY')) this.ShowEmpty = true; if (d.check('NOOPTIMIZE')) this.Optimize = 0; if (d.check('OPTIMIZE')) this.Optimize = 2; if (d.check('AUTOCOL')) this.AutoColor = true; if (d.check('AUTOZOOM')) this.AutoZoom = true; if (d.check('OPTSTAT', true)) this.optstat = d.partAsInt(); if (d.check('OPTFIT', true)) this.optfit = d.partAsInt(); if (this.optstat || this.optfit) histo?.SetBit(kNoStats, false); if (d.check('ALLBINS') && histo) { histo.fXaxis.fFirst = 0; histo.fXaxis.fLast = histo.fXaxis.fNbins + 1; histo.fXaxis.SetBit(EAxisBits.kAxisRange); if (this.ndim > 1) { histo.fYaxis.fFirst = 0; histo.fYaxis.fLast = histo.fYaxis.fNbins + 1; histo.fYaxis.SetBit(EAxisBits.kAxisRange); } if (this.ndim > 2) { histo.fZaxis.fFirst = 0; histo.fZaxis.fLast = histo.fZaxis.fNbins + 1; histo.fZaxis.SetBit(EAxisBits.kAxisRange); } } if (d.check('NOSTAT')) this.NoStat = true; if (d.check('STAT')) this.ForceStat = true; if (d.check('NOTOOLTIP')) painter?.setTooltipAllowed(false); if (d.check('TOOLTIP')) painter?.setTooltipAllowed(true); if (d.check('SYMLOGX', true)) this.SymlogX = d.partAsInt(0, 3); if (d.check('SYMLOGY', true)) this.SymlogY = d.partAsInt(0, 3); if (d.check('X3DSC', true)) this.x3dscale = d.partAsInt(0, 100) / 100; if (d.check('Y3DSC', true)) this.y3dscale = d.partAsInt(0, 100) / 100; if (d.check('PERSPECTIVE') || d.check('PERSP')) this.Ortho = false; if (d.check('ORTHO')) this.Ortho = true; let lx = 0, ly = 0, check3dbox = ''; if (d.check('LOG2XY')) lx = ly = 2; if (d.check('LOGXY')) lx = ly = 1; if (d.check('LOG2X')) lx = 2; if (d.check('LOGX')) lx = 1; if (d.check('LOG2Y')) ly = 2; if (d.check('LOGY')) ly = 1; if (lx && pad) { pad.fLogx = lx; pad.fUxmin = 0; pad.fUxmax = 1; pad.fX1 = 0; pad.fX2 = 1; } if (ly && pad) { pad.fLogy = ly; pad.fUymin = 0; pad.fUymax = 1; pad.fY1 = 0; pad.fY2 = 1; } if (d.check('LOG2Z') && pad) pad.fLogz = 2; if (d.check('LOGZ') && pad) pad.fLogz = 1; if (d.check('LOGV') && pad) pad.fLogv = 1; // fictional member, can be introduced in ROOT if (d.check('GRIDXY') && pad) pad.fGridx = pad.fGridy = 1; if (d.check('GRIDX') && pad) pad.fGridx = 1; if (d.check('GRIDY') && pad) pad.fGridy = 1; if (d.check('TICKXY') && pad) pad.fTickx = pad.fTicky = 1; if (d.check('TICKX') && pad) pad.fTickx = 1; if (d.check('TICKY') && pad) pad.fTicky = 1; if (d.check('TICKZ') && pad) pad.fTickz = 1; if (d.check('GRAYSCALE')) pp?.setGrayscale(true); if (d.check('FILL_', 'color')) { this.histoFillColor = d.color; this.histoFillPattern = 1001; } if (d.check('LINE_', 'color')) this.histoLineColor = getColor(d.color); if (d.check('WIDTH_', true)) this.histoLineWidth = d.partAsInt(); if (d.check('XAXIS_', 'color')) histo.fXaxis.fAxisColor = histo.fXaxis.fLabelColor = histo.fXaxis.fTitleColor = d.color; if (d.check('YAXIS_', 'color')) histo.fYaxis.fAxisColor = histo.fYaxis.fLabelColor = histo.fYaxis.fTitleColor = d.color; if (d.check('X+')) { this.AxisPos = 10; this.second_x = Boolean(painter?.getMainPainter()); } if (d.check('Y+')) { this.AxisPos += 1; this.second_y = Boolean(painter?.getMainPainter()); } if (d.check('SAME0')) { this.Same = true; this.IgnoreMainScale = true; } if (d.check('SAMES')) { this.Same = true; this.ForceStat = true; } if (d.check('SAME')) { this.Same = true; this.Func = true; } if (d.check('SPEC')) this.Spec = true; // not used if (d.check('BASE0') || d.check('MIN0')) this.BaseLine = 0; else if (gStyle.fHistMinimumZero) this.BaseLine = 0; if (d.check('PIE')) this.Pie = true; // not used if (d.check('CANDLE', true)) this.Candle = d.part || '1'; if (d.check('VIOLIN', true)) { this.Violin = d.part || '1'; delete this.Candle; } if (d.check('NOSCALED')) this.Scaled = false; if (d.check('SCALED')) this.Scaled = true; if (d.check('GLBOX', true)) this.GLBox = 10 + d.partAsInt(); if (d.check('GLCOL')) this.GLColor = true; d.check('GL'); // suppress GL if (d.check('CIRCULAR', true) || d.check('CIRC', true)) { this.Circular = 11; if (d.part.indexOf('0') >= 0) this.Circular = 10; // black and white if (d.part.indexOf('1') >= 0) this.Circular = 11; // color if (d.part.indexOf('2') >= 0) this.Circular = 12; // color and width } this.Chord = d.check('CHORD'); if (d.check('LEGO', true)) { this.Lego = 1; if (d.part.indexOf('0') >= 0) this.Zero = false; if (d.part.indexOf('1') >= 0) this.Lego = 11; if (d.part.indexOf('2') >= 0) this.Lego = 12; if (d.part.indexOf('3') >= 0) this.Lego = 13; if (d.part.indexOf('4') >= 0) this.Lego = 14; check3dbox = d.part; if (d.part.indexOf('Z') >= 0) this.Zscale = true; if (d.part.indexOf('H') >= 0) this.Zvert = false; } if (d.check('R3D_', true)) this.Render3D = constants.Render3D.fromString(d.part.toLowerCase()); if (d.check('POL')) this.System = kPOLAR; if (d.check('CYL')) this.System = kCYLINDRICAL; if (d.check('SPH')) this.System = kSPHERICAL; if (d.check('PSR')) this.System = kRAPIDITY; if (d.check('SURF', true)) { this.Surf = d.partAsInt(10, 1); check3dbox = d.part; if (d.part.indexOf('Z') >= 0) this.Zscale = true; if (d.part.indexOf('H') >= 0) this.Zvert = false; } if (d.check('TF3', true)) check3dbox = d.part; if (d.check('ISO', true)) check3dbox = d.part; if (d.check('LIST')) this.List = true; // not used if (d.check('CONT', true) && (hdim > 1)) { this.Contour = 1; if (d.part.indexOf('Z') >= 0) this.Zscale = true; if (d.part.indexOf('H') >= 0) this.Zvert = false; if (d.part.indexOf('1') >= 0) this.Contour = 11; else if (d.part.indexOf('2') >= 0) this.Contour = 12; else if (d.part.indexOf('3') >= 0) this.Contour = 13; else if (d.part.indexOf('4') >= 0) this.Contour = 14; } // decode bar/hbar option if (d.check('HBAR', true)) this.BarStyle = 20; else if (d.check('BAR', true)) this.BarStyle = 10; if (this.BarStyle > 0) { this.Hist = false; this.need_fillcol = true; this.BarStyle += d.partAsInt(); } if (d.check('ARR')) this.Arrow = true; if (d.check('BOX', true)) { this.BoxStyle = 10; if (d.part.indexOf('1') >= 0) this.BoxStyle = 11; else if (d.part.indexOf('2') >= 0) this.BoxStyle = 12; else if (d.part.indexOf('3') >= 0) this.BoxStyle = 13; if (d.part.indexOf('Z') >= 0) this.Zscale = true; if (d.part.indexOf('H') >= 0) this.Zvert = false; } this.Box = this.BoxStyle > 0; if (d.check('CJUST')) this.Cjust = true; if (d.check('COL7')) this.Color = 7; // special color mode with use of bar offset if (d.check('COL')) this.Color = true; if (d.check('CHAR')) this.Char = 1; if (d.check('ALLFUNC')) this.AllFunc = true; if (d.check('FUNC')) { this.Func = true; this.Hist = false; } if (d.check('HAXISG')) { this.Axis = 3; this.SwapXY = 1; } if (d.check('HAXIS')) { this.Axis = 1; this.SwapXY = 1; } if (d.check('HAXIG')) { this.Axis = 2; this.SwapXY = 1; } if (d.check('AXISG')) this.Axis = 3; if (d.check('AXIS')) this.Axis = 1; if (d.check('AXIG')) this.Axis = 2; if (d.check('TEXT', true)) { this.Text = true; this.Hist = false; this.TextAngle = Math.min(d.partAsInt(), 90); if (d.part.indexOf('N') >= 0) this.TextKind = 'N'; if (d.part.indexOf('E0') >= 0) this.TextLine = true; if (d.part.indexOf('E') >= 0) this.TextKind = 'E'; } if (d.check('SCAT=', true)) { this.Scat = true; this.ScatCoef = parseFloat(d.part); if (!Number.isFinite(this.ScatCoef) || (this.ScatCoef <= 0)) this.ScatCoef = 1.0; } if (d.check('SCAT')) this.Scat = true; if (d.check('TRI', true)) { this.Color = false; this.Tri = 1; check3dbox = d.part; if (d.part.indexOf('ERR') >= 0) this.Error = true; } if (d.check('AITOFF')) this.Proj = 1; if (d.check('MERCATOR')) this.Proj = 2; if (d.check('SINUSOIDAL')) this.Proj = 3; if (d.check('PARABOLIC')) this.Proj = 4; if (d.check('MOLLWEIDE')) this.Proj = 5; if (this.Proj > 0) this.Contour = 14; if (d.check('PROJXY', true)) { let flag = true; if ((histo?._typename === clTProfile2D) && d.part && !Number.isInteger(Number.parseInt(d.part))) { this.Profile2DProj = d.part; flag = d.check('PROJXY', true); // allow projxy with projected profile2d } if (flag) this.Project = 'XY' + d.partAsInt(0, 1); } if (d.check('PROJX', true)) { if (histo?._typename === clTProfile) this.ProfileProj = d.part || 'B'; else this.Project = 'X' + d.part; } if (d.check('PROJY', true)) this.Project = 'Y' + d.part; if (d.check('PROJ')) this.Project = 'Y1'; if (check3dbox) { if (check3dbox.indexOf('FB') >= 0) this.FrontBox = false; if (check3dbox.indexOf('BB') >= 0) this.BackBox = false; } if ((hdim === 3) && d.check('FB')) this.FrontBox = false; if ((hdim === 3) && d.check('BB')) this.BackBox = false; if (d.check('PFC') && !this._pfc) this._pfc = 2; if ((d.check('PLC') || this.AutoColor) && !this._plc) this._plc = 2; if (d.check('PMC') && !this._pmc) this._pmc = 2; const check_axis_bit = (aopt, axis, bit) => { // ignore Z scale options for 2D plots if ((axis === 'fZaxis') && (hdim < 3) && !this.Lego && !this.Surf) return; let flag = d.check(aopt); if (pad && pad['$'+aopt]) { flag = true; pad['$'+aopt] = undefined; } if (flag && histo) histo[axis].SetBit(bit, true); }; check_axis_bit('OTX', 'fXaxis', EAxisBits.kOppositeTitle); check_axis_bit('OTY', 'fYaxis', EAxisBits.kOppositeTitle); check_axis_bit('OTZ', 'fZaxis', EAxisBits.kOppositeTitle); check_axis_bit('CTX', 'fXaxis', EAxisBits.kCenterTitle); check_axis_bit('CTY', 'fYaxis', EAxisBits.kCenterTitle); check_axis_bit('CTZ', 'fZaxis', EAxisBits.kCenterTitle); check_axis_bit('MLX', 'fXaxis', EAxisBits.kMoreLogLabels); check_axis_bit('MLY', 'fYaxis', EAxisBits.kMoreLogLabels); check_axis_bit('MLZ', 'fZaxis', EAxisBits.kMoreLogLabels); check_axis_bit('NOEX', 'fXaxis', EAxisBits.kNoExponent); check_axis_bit('NOEY', 'fYaxis', EAxisBits.kNoExponent); check_axis_bit('NOEZ', 'fZaxis', EAxisBits.kNoExponent); if (d.check('RX') || pad?.$RX) this.RevX = true; if (d.check('RY') || pad?.$RY) this.RevY = true; if (d.check('L')) { this.Line = true; this.Hist = false; } if (d.check('F')) { this.Fill = true; this.need_fillcol = true; } if (d.check('A')) this.Axis = -1; if (pad?.$ratio_pad === 'up') { if (!this.Same) this.Axis = 0; // draw both axes histo.fXaxis.fLabelSize = 0; histo.fXaxis.fTitle = ''; histo.fYaxis.$use_top_pad = true; } else if (pad?.$ratio_pad === 'low') { if (!this.Same) this.Axis = 0; // draw both axes histo.fXaxis.$use_top_pad = true; histo.fYaxis.$use_top_pad = true; histo.fXaxis.fTitle = 'x'; const fp = painter?.getCanvPainter().findPainterFor(null, 'upper_pad', clTPad)?.getFramePainter(); if (fp) { painter.zoom_xmin = fp.scale_xmin; painter.zoom_xmax = fp.scale_xmax; } } if (d.check('B1')) { this.BarStyle = 1; this.BaseLine = 0; this.Hist = false; this.need_fillcol = true; } if (d.check('B')) { this.BarStyle = 1; this.Hist = false; this.need_fillcol = true; } if (d.check('C')) { this.Curve = true; this.Hist = false; } if (d.check('][')) { this.Off = 1; this.Hist = true; } if (d.check('HIST')) { this.Hist = true; this.Func = true; this.Error = false; } this.Bar = (this.BarStyle > 0); delete this.MarkStyle; // remove mark style if any if (d.check('P0')) { this.Mark = true; this.Hist = false; this.Zero = true; } if (d.check('P')) { this.Mark = true; this.Hist = false; this.Zero = false; } if (d.check('HZ')) { this.Zscale = true; this.Zvert = false; } if (d.check('Z')) this.Zscale = true; if (d.check('*')) { this.Mark = true; this.MarkStyle = 3; this.Hist = false; } if (d.check('H')) this.Hist = true; if (d.check('E', true)) { this.Error = true; if (hdim === 1) { this.Zero = false; // do not draw empty bins with errors if (this.Hist === 1) this.Hist = false; if (Number.isInteger(parseInt(d.part[0]))) this.ErrorKind = parseInt(d.part[0]); if ((this.ErrorKind === 3) || (this.ErrorKind === 4)) this.need_fillcol = true; if (this.ErrorKind === 0) this.Zero = true; // enable drawing of empty bins if (d.part.indexOf('X0') >= 0) this.errorX = 0; } } if (d.check('9')) this.HighRes = 1; if (d.check('0')) this.Zero = false; if (this.Color && d.check('1')) this.Zero = false; // flag identifies 3D drawing mode for histogram if ((this.Lego > 0) || (hdim === 3) || (((this.Surf > 0) || this.Error) && (hdim === 2))) this.Mode3D = true; // default draw options for TF1 is line and fill if (painter?.isTF1() && (hdim === 1) && (this.Hist === 1) && !this.Line && !this.Fill && !this.Curve && !this.Mark) { this.Hist = false; this.Curve = settings.FuncAsCurve; this.Line = !this.Curve; this.Fill = true; } if ((this.Surf === 15) && (this.System === kPOLAR || this.System === kCARTESIAN)) this.Surf = 13; } /** @summary Is X/Y swap is configured */ swap_xy() { return this.BarStyle >= 20 || this.SwapXY; } /** @summary Tries to reconstruct string with hist draw options */ asString(is_main_hist, pad) { let res = '', zopt = ''; if (this.Zscale) zopt = this.Zvert ? 'Z' : 'HZ'; if (this.Mode3D) { if (this.Lego) { res = 'LEGO'; if (!this.Zero) res += '0'; if (this.Lego > 10) res += (this.Lego-10); res += zopt; } else if (this.Surf) { res = 'SURF' + (this.Surf-10); res += zopt; } if (!this.FrontBox) res += 'FB'; if (!this.BackBox) res += 'BB'; if (this.x3dscale !== 1) res += `_X3DSC${Math.round(this.x3dscale * 100)}`; if (this.y3dscale !== 1) res += `_Y3DSC${Math.round(this.y3dscale * 100)}`; } else { if (this.Candle) res = 'CANDLE' + this.Candle; else if (this.Violin) res = 'VIOLIN' + this.Violin; else if (this.Scat) res = 'SCAT'; else if (this.Color) { res = 'COL'; if (!this.Zero) res += '0'; res += zopt; if (this.Axis < 0) res += 'A'; } else if (this.Contour) { res = 'CONT'; if (this.Contour > 10) res += (this.Contour-10); res += zopt; } else if (this.Bar) res = (this.BaseLine === false) ? 'B' : 'B1'; else if (this.Mark) res = this.Zero ? 'P0' : 'P'; // here invert logic with 0 else if (this.Line) { res += 'L'; if (this.Fill) res += 'F'; } else if (this.Off) res = ']['; if (this.Error) { res += 'E'; if (this.ErrorKind >= 0) res += this.ErrorKind; if (this.errorX === 0) res += 'X0'; } if (this.Cjust) res += ' CJUST'; if (this.Hist === true) res += 'HIST'; if (this.Text) { res += 'TEXT'; if (this.TextAngle) res += this.TextAngle; res += this.TextKind; } } if (this.Palette && this.canHavePalette()) res += `_PAL${this.Palette}`; if (this.is3d() && this.Ortho && is_main_hist) res += '_ORTHO'; if (this.ProfileProj) res += '_PROJX' + this.ProfileProj; if (this.Profile2DProj) res += '_PROJXY' + this.Profile2DProj; if (this.Proj) res += '_PROJ' + this.Proj; if (this.ShowEmpty) res += '_SHOWEMPTY'; if (this.Same) res += this.ForceStat ? 'SAMES' : 'SAME'; else if (is_main_hist && res) { if (this.ForceStat || (this.StatEnabled === true)) res += '_STAT'; else if (this.NoStat || (this.StatEnabled === false)) res += '_NOSTAT'; } if (is_main_hist && pad && res) { if (pad.fLogx === 2) res += '_LOG2X'; else if (pad.fLogx) res += '_LOGX'; if (pad.fLogy === 2) res += '_LOG2Y'; else if (pad.fLogy) res += '_LOGY'; if (pad.fLogz === 2) res += '_LOG2Z'; else if (pad.fLogz) res += '_LOGZ'; if (pad.fGridx) res += '_GRIDX'; if (pad.fGridy) res += '_GRIDY'; if (pad.fTickx) res += '_TICKX'; if (pad.fTicky) res += '_TICKY'; if (pad.fTickz) res += '_TICKZ'; } if (this.cutg_name) res += ` [${this.cutg_name}]`; return res; } } // class THistDrawOptions /** * @summary Handle for histogram contour * * @private */ class HistContour { constructor(zmin, zmax) { this.arr = []; this.colzmin = zmin; this.colzmax = zmax; this.below_min_indx = -1; this.exact_min_indx = 0; } /** @summary Returns contour levels */ getLevels() { return this.arr; } /** @summary Create normal contour levels */ createNormal(nlevels, log_scale, zminpositive) { if (log_scale) { if (this.colzmax <= 0) this.colzmax = 1.0; if (this.colzmin <= 0) { if ((zminpositive === undefined) || (zminpositive <= 0)) this.colzmin = 0.0001*this.colzmax; else this.colzmin = ((zminpositive < 3) || (zminpositive > 100)) ? 0.3*zminpositive : 1; } if (this.colzmin >= this.colzmax) this.colzmin = 0.0001*this.colzmax; const logmin = Math.log(this.colzmin)/Math.log(10), logmax = Math.log(this.colzmax)/Math.log(10), dz = (logmax-logmin)/nlevels; this.arr.push(this.colzmin); for (let level = 1; level < nlevels; level++) this.arr.push(Math.exp((logmin + dz*level)*Math.log(10))); this.arr.push(this.colzmax); this.custom = true; } else { if ((this.colzmin === this.colzmax) && (this.colzmin !== 0)) { this.colzmax += 0.01*Math.abs(this.colzmax); this.colzmin -= 0.01*Math.abs(this.colzmin); } const dz = (this.colzmax-this.colzmin)/nlevels; for (let level = 0; level <= nlevels; level++) this.arr.push(this.colzmin + dz*level); } } /** @summary Create custom contour levels */ createCustom(levels) { this.custom = true; for (let n = 0; n < levels.length; ++n) this.arr.push(levels[n]); if (this.colzmax > this.arr.at(-1)) this.arr.push(this.colzmax); } /** @summary Configure indices */ configIndicies(below_min, exact_min) { this.below_min_indx = below_min; this.exact_min_indx = exact_min; } /** @summary Get index based on z value */ getContourIndex(zc) { // bins less than zmin not drawn if (zc < this.colzmin) return this.below_min_indx; // if bin content exactly zmin, draw it when col0 specified or when content is positive if (zc === this.colzmin) return this.exact_min_indx; if (!this.custom) return Math.floor(0.01 + (zc - this.colzmin) * (this.arr.length - 1) / (this.colzmax - this.colzmin)); let l = 0, r = this.arr.length - 1; if (zc < this.arr[0]) return -1; if (zc >= this.arr[r]) return r; while (l < r-1) { const mid = Math.round((l+r)/2); if (this.arr[mid] > zc) r = mid; else l = mid; } return l; } /** @summary Get palette color */ getPaletteColor(palette, zc) { const zindx = this.getContourIndex(zc); if (zindx < 0) return null; const pindx = palette.calcColorIndex(zindx, this.arr.length); return palette.getColor(pindx); } /** @summary Get palette index */ getPaletteIndex(palette, zc) { const zindx = this.getContourIndex(zc); return (zindx < 0) ? null : palette.calcColorIndex(zindx, this.arr.length); } } // class HistContour /** * @summary Handle for updating of secondary functions * * @private */ class FunctionsHandler { constructor(painter, pp, funcs, statpainter) { this.painter = painter; this.pp = pp; const painters = [], update_painters = [], only_draw = (statpainter === true); this.newfuncs = []; this.newopts = []; // find painters associated with histogram/graph/... if (!only_draw) { pp?.forEachPainterInPad(objp => { if (objp.isSecondary(painter) && objp.getSecondaryId()?.match(/^func_|^indx_/)) painters.push(objp); }, 'objects'); } for (let n = 0; n < funcs?.arr.length; ++n) { const func = funcs.arr[n], fopt = funcs.opt[n]; if (!func?._typename) continue; if (isFunc(painter.needDrawFunc) && !painter.needDrawFunc(painter.getObject(), func)) continue; let funcpainter = null, func_indx = -1; if (!only_draw) { // try to find matching object in associated list of painters for (let i = 0; i < painters.length; ++i) { if (painters[i].matchObjectType(func._typename) && (painters[i].getObjectName() === func.fName)) { funcpainter = painters[i]; func_indx = i; break; } } // or just in generic list of painted objects if (!funcpainter && func.fName) funcpainter = pp?.findPainterFor(null, func.fName, func._typename); } if (funcpainter) { funcpainter.updateObject(func, fopt); if (func_indx >= 0) { painters.splice(func_indx, 1); update_painters.push(funcpainter); } } else { // use arrays index while index is important this.newfuncs[n] = func; this.newopts[n] = fopt; } } // stat painter has to be kept even when no object exists in the list if (isObject(statpainter)) { const indx = painters.indexOf(statpainter); if (indx >= 0) painters.splice(indx, 1); } // remove all function which are not found in new list of functions if (painters.length > 0) pp?.cleanPrimitives(p => painters.indexOf(p) >= 0); if (update_painters.length > 0) this._extraPainters = update_painters; } /** @summary Draw/update functions selected before */ drawNext(indx) { if (this._extraPainters) { const p = this._extraPainters.shift(); if (this._extraPainters.length === 0) delete this._extraPainters; return getPromise(p.redraw()).then(() => this.drawNext(0)); } if (!this.newfuncs || (indx >= this.newfuncs.length)) { delete this.newfuncs; delete this.newopts; return Promise.resolve(this.painter); // simplify drawing } const func = this.newfuncs[indx], fopt = this.newopts[indx]; if (!func || this.pp?.findPainterFor(func)) return this.drawNext(indx+1); const func_id = func?.fName ? `func_${func.fName}` : `indx_${indx}`; // Required to correctly draw multiple stats boxes // TODO: set reference via weak pointer func.$main_painter = this.painter; const promise = TPavePainter.canDraw(func) ? TPavePainter.draw(this.pp, func, fopt) : this.pp.drawObject(this.pp, func, fopt); return promise.then(fpainter => { fpainter.setSecondaryId(this.painter, func_id); return this.drawNext(indx+1); }); } } // class FunctionsHandler // TH1 bits // kNoStats = BIT(9), don't draw stats box const kUserContour = BIT(10), // user specified contour levels // kCanRebin = BIT(11), // can rebin axis // kLogX = BIT(15), // X-axis in log scale kIsZoomed = BIT(16), // bit set when zooming on Y axis kNoTitle = BIT(17); // don't draw the histogram title // kIsAverage = BIT(18); // Bin contents are average (used by Add) /** * @summary Basic painter for histogram classes * @private */ class THistPainter extends ObjectPainter { /** @summary Constructor * @param {object|string} dom - DOM element for drawing or element id * @param {object} histo - TH1 derived histogram object */ constructor(dom, histo) { super(dom, histo); this.draw_content = true; this.nbinsx = this.nbinsy = 0; this.mode3d = false; } /** @summary Returns histogram object */ getHisto() { return this.getObject(); } /** @summary Returns histogram axis */ getAxis(name) { const histo = this.getObject(); switch (name) { case 'x': return histo?.fXaxis; case 'y': return histo?.fYaxis; case 'z': return histo?.fZaxis; } return null; } /** @summary Returns true if TProfile */ isTProfile() { return this.matchObjectType(clTProfile); } /** @summary Returns true if histogram drawn instead of TF1/TF2 object */ isTF1() { return false; } /** @summary Returns true if TH1K */ isTH1K() { return this.matchObjectType('TH1K'); } /** @summary Returns true if TH2Poly */ isTH2Poly() { return this.matchObjectType(/^TH2Poly/) || this.matchObjectType(/^TProfile2Poly/); } /** @summary Clear 3d drawings - if any */ clear3DScene() { const fp = this.getFramePainter(); if (isFunc(fp?.create3DScene)) fp.create3DScene(-1); this.mode3d = false; } /** @summary Cleanup histogram painter */ cleanup() { this.clear3DScene(); delete this._color_palette; delete this.fContour; delete this.options; super.cleanup(); } /** @summary Returns number of histogram dimensions */ getDimension() { const histo = this.getHisto(); if (!histo) return 0; if (histo._typename.match(/^TH2/)) return 2; if (histo._typename === clTProfile2D) return 2; if (histo._typename.match(/^TH3/)) return 3; if (histo._typename === clTProfile3D) return 3; if (this.isTH2Poly()) return 2; return 1; } /** @summary Decode options string opt and fill the option structure */ decodeOptions(opt) { const histo = this.getHisto(), hdim = this.getDimension(), pp = this.getPadPainter(), pad = pp?.getRootPad(true); if (!this.options) this.options = new THistDrawOptions(); else this.options.reset(); // when changing draw option, reset attributes usage this.lineatt?.setUsed(false); this.fillatt?.setUsed(false); this.markeratt?.setUsed(false); this.options.decode(opt || histo.fOption, hdim, histo, pp, pad, this); this.storeDrawOpt(opt); // opt will be return as default draw option, used in web canvas } /** @summary Copy draw options from other painter */ copyOptionsFrom(src) { if (src === this) return; const o = this.options, o0 = src.options; o.Mode3D = o0.Mode3D; o.Zero = o0.Zero; if (o0.Mode3D) { o.Lego = o0.Lego; o.Surf = o0.Surf; } else { o.Color = o0.Color; o.Contour = o0.Contour; } } /** @summary copy draw options to all other histograms in the pad */ copyOptionsToOthers() { this.forEachPainter(painter => { if ((painter !== this) && isFunc(painter.copyOptionsFrom)) painter.copyOptionsFrom(this); }, 'objects'); } /** @summary Scan histogram content * @abstract */ scanContent(/* when_axis_changed */) { // function will be called once new histogram or // new histogram content is assigned // one should find min, max, bins number, content min/max values // if when_axis_changed === true specified, content will be scanned after axis zoom changed } /** @summary Check pad ranges when drawing of frame axes will be performed * @desc Only if histogram is main painter and drawn with SAME option, pad range can be used * In all other cases configured range must be derived from histogram itself */ checkPadRange() { if (this.isMainPainter()) this.check_pad_range = this.options.Same ? 'pad_range' : true; } /** @summary Create necessary histogram draw attributes */ createHistDrawAttributes(only_check_auto) { const histo = this.getHisto(), o = this.options; if (o._pfc > 1 || o._plc > 1 || o._pmc > 1) { const pp = this.getPadPainter(); if (isFunc(pp?.getAutoColor)) { const icolor = pp.getAutoColor(histo.$num_histos); this._auto_exec = ''; // can be reused when sending option back to server if (o._pfc > 1) { o._pfc = 1; histo.fFillColor = icolor; this._auto_exec += `SetFillColor(${icolor});;`; delete this.fillatt; } if (o._plc > 1) { o._plc = 1; histo.fLineColor = icolor; this._auto_exec += `SetLineColor(${icolor});;`; delete this.lineatt; } if (o._pmc > 1) { o._pmc = 1; histo.fMarkerColor = icolor; this._auto_exec += `SetMarkerColor(${icolor});;`; delete this.markeratt; } } } if (only_check_auto) this.deleteAttr(); else { this.createAttFill({ attr: histo, color: this.options.histoFillColor, pattern: this.options.histoFillPattern, kind: 1 }); this.createAttLine({ attr: histo, color0: this.options.histoLineColor, width: this.options.histoLineWidth }); } } /** @summary Update axes attributes in target histogram * @private */ updateAxes(tgt_histo, src_histo, fp) { const copyTAxisMembers = (tgt, src, copy_zoom) => { tgt.fTitle = src.fTitle; tgt.fLabels = src.fLabels; tgt.fXmin = src.fXmin; tgt.fXmax = src.fXmax; tgt.fTimeDisplay = src.fTimeDisplay; tgt.fTimeFormat = src.fTimeFormat; tgt.fAxisColor = src.fAxisColor; tgt.fLabelColor = src.fLabelColor; tgt.fLabelFont = src.fLabelFont; tgt.fLabelOffset = src.fLabelOffset; tgt.fLabelSize = src.fLabelSize; tgt.fNdivisions = src.fNdivisions; tgt.fTickLength = src.fTickLength; tgt.fTitleColor = src.fTitleColor; tgt.fTitleFont = src.fTitleFont; tgt.fTitleOffset = src.fTitleOffset; tgt.fTitleSize = src.fTitleSize; if (copy_zoom) { tgt.fFirst = src.fFirst; tgt.fLast = src.fLast; tgt.fBits = src.fBits; } }; copyTAxisMembers(tgt_histo.fXaxis, src_histo.fXaxis, this.snapid && !fp?.zoomChangedInteractive('x')); copyTAxisMembers(tgt_histo.fYaxis, src_histo.fYaxis, this.snapid && !fp?.zoomChangedInteractive('y')); copyTAxisMembers(tgt_histo.fZaxis, src_histo.fZaxis, this.snapid && !fp?.zoomChangedInteractive('z')); } /** @summary Update histogram object * @param obj - new histogram instance * @param opt - new drawing option (optional) * @return {Boolean} - true if histogram was successfully updated */ updateObject(obj, opt) { const histo = this.getHisto(), fp = this.getFramePainter(), pp = this.getPadPainter(), o = this.options; if (obj !== histo) { if (!this.matchObjectType(obj)) return false; // simple replace of object does not help - one can have different // complex relations between histogram and stat box, histogram and colz axis, // one could have THStack or TMultiGraph object // The only that could be done is update of content const statpainter = pp?.findPainterFor(this.findStat()); // copy histogram bits if (histo.TestBit(kNoStats) !== obj.TestBit(kNoStats)) { histo.SetBit(kNoStats, obj.TestBit(kNoStats)); // here check only stats bit if (statpainter) { statpainter.Enabled = !histo.TestBit(kNoStats) && !this.options.NoStat; // && (!this.options.Same || this.options.ForceStat) // remove immediately when redraw not called for disabled stats if (!statpainter.Enabled) statpainter.removeG(); } } histo.SetBit(kIsZoomed, obj.TestBit(kIsZoomed)); // special treatment for web canvas - also name can be changed if (this.snapid !== undefined) { histo.fName = obj.fName; o._pfc = o._plc = o._pmc = 0; // auto colors should be processed in web canvas } if (!o._pfc) histo.fFillColor = obj.fFillColor; histo.fFillStyle = obj.fFillStyle; if (!o._plc) histo.fLineColor = obj.fLineColor; histo.fLineStyle = obj.fLineStyle; histo.fLineWidth = obj.fLineWidth; if (!o._pmc) histo.fMarkerColor = obj.fMarkerColor; histo.fMarkerSize = obj.fMarkerSize; histo.fMarkerStyle = obj.fMarkerStyle; histo.fEntries = obj.fEntries; histo.fTsumw = obj.fTsumw; histo.fTsumwx = obj.fTsumwx; histo.fTsumwx2 = obj.fTsumwx2; histo.fXaxis.fNbins = obj.fXaxis.fNbins; if (this.getDimension() > 1) { histo.fTsumwy = obj.fTsumwy; histo.fTsumwy2 = obj.fTsumwy2; histo.fTsumwxy = obj.fTsumwxy; histo.fYaxis.fNbins = obj.fYaxis.fNbins; if (this.getDimension() > 2) { histo.fTsumwz = obj.fTsumwz; histo.fTsumwz2 = obj.fTsumwz2; histo.fTsumwxz = obj.fTsumwxz; histo.fTsumwyz = obj.fTsumwyz; histo.fZaxis.fNbins = obj.fZaxis.fNbins; } } this.updateAxes(histo, obj, fp); histo.fArray = obj.fArray; histo.fNcells = obj.fNcells; histo.fTitle = obj.fTitle; histo.fMinimum = obj.fMinimum; histo.fMaximum = obj.fMaximum; histo.fSumw2 = obj.fSumw2; if (!o.ominimum) o.minimum = histo.fMinimum; if (!o.omaximum) o.maximum = histo.fMaximum; if (this.getDimension() === 1) o.decodeSumw2(histo); if (this.isTProfile()) histo.fBinEntries = obj.fBinEntries; else if (this.isTH1K()) { histo.fNIn = obj.fNIn; histo.fReady = 0; } else if (this.isTH2Poly()) histo.fBins = obj.fBins; // remove old functions, update existing, prepare to draw new one this._funcHandler = new FunctionsHandler(this, pp, obj.fFunctions, statpainter); const changed_opt = (histo.fOption !== obj.fOption); histo.fOption = obj.fOption; if (((opt !== undefined) && (o.original !== opt)) || changed_opt) this.decodeOptions(opt || histo.fOption); } if (!o.ominimum) o.minimum = histo.fMinimum; if (!o.omaximum) o.maximum = histo.fMaximum; if (!o.ominimum && !o.omaximum && o.minimum === o.maximum) o.minimum = o.maximum = kNoZoom; if (!fp || !fp.zoomChangedInteractive()) this.checkPadRange(); this.scanContent(); this.histogram_updated = true; // indicate that object updated return true; } /** @summary Access or modify histogram min/max * @private */ accessMM(ismin, v) { const name = ismin ? 'minimum' : 'maximum'; if (v === undefined) return this.options[name]; this.options[name] = v; this.interactiveRedraw('pad', ismin ? `exec:SetMinimum(${v})` : `exec:SetMaximum(${v})`); } /** @summary Extract axes bins and ranges * @desc here functions are defined to convert index to axis value and back * was introduced to support non-equidistant bins */ extractAxesProperties(ndim) { const assignTAxisFuncs = axis => { if (axis.fXbins.length >= axis.fNbins) { axis.GetBinCoord = function(bin) { const indx = Math.round(bin); if (indx <= 0) return this.fXmin; if (indx > this.fNbins) return this.fXmax; if (indx === bin) return this.fXbins[indx]; const indx2 = (bin < indx) ? indx - 1 : indx + 1; return this.fXbins[indx] * Math.abs(bin-indx2) + this.fXbins[indx2] * Math.abs(bin-indx); }; axis.FindBin = function(x, add) { for (let k = 1; k < this.fXbins.length; ++k) if (x < this.fXbins[k]) return Math.floor(k-1+add); return this.fNbins; }; } else { axis.$binwidth = (axis.fXmax - axis.fXmin) / (axis.fNbins || 1); axis.GetBinCoord = function(bin) { return this.fXmin + bin*this.$binwidth; }; axis.FindBin = function(x, add) { return Math.floor((x - this.fXmin) / this.$binwidth + add); }; } }; this.nbinsx = this.nbinsy = this.nbinsz = 0; const histo = this.getHisto(); this.nbinsx = histo.fXaxis.fNbins; this.xmin = histo.fXaxis.fXmin; this.xmax = histo.fXaxis.fXmax; if (histo.fXaxis.TestBit(EAxisBits.kAxisRange) && (histo.fXaxis.fFirst !== histo.fXaxis.fLast)) { if (histo.fXaxis.fFirst === 0) this.xmin = histo.fXaxis.GetBinLowEdge(0); if (histo.fXaxis.fLast === this.nbinsx + 1) this.xmax = histo.fXaxis.GetBinLowEdge(this.nbinsx + 2); } assignTAxisFuncs(histo.fXaxis); this.ymin = histo.fYaxis.fXmin; this.ymax = histo.fYaxis.fXmax; this._exact_y_range = (ndim === 1) && this.options.ohmin && this.options.ohmax; if (this._exact_y_range) { this.ymin = this.options.hmin; this.ymax = this.options.hmax; } if (ndim > 1) { this.nbinsy = histo.fYaxis.fNbins; if (histo.fYaxis.TestBit(EAxisBits.kAxisRange) && (histo.fYaxis.fFirst !== histo.fYaxis.fLast)) { if (histo.fYaxis.fFirst === 0) this.ymin = histo.fYaxis.GetBinLowEdge(0); if (histo.fYaxis.fLast === this.nbinsy + 1) this.ymax = histo.fYaxis.GetBinLowEdge(this.nbinsy + 2); } assignTAxisFuncs(histo.fYaxis); this.zmin = histo.fZaxis.fXmin; this.zmax = histo.fZaxis.fXmax; if ((ndim === 2) && this.options.ohmin && this.options.ohmax) { this.zmin = this.options.hmin; this.zmax = this.options.hmax; } } if (ndim > 2) { this.nbinsz = histo.fZaxis.fNbins; if (histo.fZaxis.TestBit(EAxisBits.kAxisRange) && (histo.fZaxis.fFirst !== histo.fZaxis.fLast)) { if (histo.fZaxis.fFirst === 0) this.zmin = histo.fZaxis.GetBinLowEdge(0); if (histo.fZaxis.fLast === this.nbinsz + 1) this.zmax = histo.fZaxis.GetBinLowEdge(this.nbinsz + 2); } assignTAxisFuncs(histo.fZaxis); } } /** @summary Draw axes for histogram * @desc axes can be drawn only for main histogram */ async drawAxes() { const fp = this.getFramePainter(); if (!fp) return false; const histo = this.getHisto(); // artificially add y range to display axes if (this.ymin === this.ymax) this.ymax += 1; if (!this.isMainPainter()) { const opts = { second_x: (this.options.AxisPos >= 10), second_y: (this.options.AxisPos % 10) === 1, hist_painter: this }; if ((!opts.second_x && !opts.second_y) || fp.hasDrawnAxes(opts.second_x, opts.second_y)) return false; fp.setAxes2Ranges(opts.second_x, histo.fXaxis, this.xmin, this.xmax, opts.second_y, histo.fYaxis, this.ymin, this.ymax); fp.createXY2(opts); return fp.drawAxes2(opts.second_x, opts.second_y); } fp.setAxesRanges(histo.fXaxis, this.xmin, this.xmax, histo.fYaxis, this.ymin, this.ymax, histo.fZaxis, 0, 0); fp.createXY({ ndim: this.getDimension(), check_pad_range: this.check_pad_range, zoom_xmin: this.zoom_xmin, zoom_xmax: this.zoom_xmax, zoom_ymin: this.zoom_ymin, zoom_ymax: this.zoom_ymax, xmin_nz: histo.$xmin_nz, ymin_nz: this.ymin_nz ?? histo.$ymin_nz, swap_xy: this.options.swap_xy(), reverse_x: this.options.RevX, reverse_y: this.options.RevY, symlog_x: this.options.SymlogX, symlog_y: this.options.SymlogY, Proj: this.options.Proj, extra_y_space: this.options.Text && (this.options.BarStyle > 0), hist_painter: this }); delete this.check_pad_range; delete this.zoom_xmin; delete this.zoom_xmax; delete this.zoom_ymin; delete this.zoom_ymax; if (this.options.Same) return false; const disable_axis_draw = (this.options.Axis < 0) || (this.options.Axis === 2); return fp.drawAxes(false, disable_axis_draw, disable_axis_draw, this.options.AxisPos, this.options.Zscale && this.options.Zvert, this.options.Zscale && !this.options.Zvert, this.options.Axis !== 1); } /** @summary Inform web canvas that something changed in the histogram */ processOnlineChange(kind) { const cp = this.getCanvPainter(); if (isFunc(cp?.processChanges)) cp.processChanges(kind, this); } /** @summary Fill option object used in TWebCanvas */ fillW