import { gStyle, settings, createHistogram, createTPolyLine, isFunc, isStr, clTMultiGraph, clTH1D, clTF2, clTProfile2D, kInspect } from '../core.mjs'; import { pointer as d3_pointer, rgb as d3_rgb, chord as d3_chord, arc as d3_arc, ribbon as d3_ribbon } from '../d3.mjs'; import { kBlack } from '../base/colors.mjs'; import { TRandom, floatToString, makeTranslate, addHighlightStyle, getBoxDecorations } from '../base/BasePainter.mjs'; import { EAxisBits } from '../base/ObjectPainter.mjs'; import { THistPainter, kPOLAR } from './THistPainter.mjs'; import { assignContextMenu } from '../gui/menu.mjs'; /** @summary Build histogram contour lines * @private */ function buildHist2dContour(histo, handle, levels, palette, contour_func) { const kMAXCONTOUR = 2004, kMAXCOUNT = 2000, // arguments used in the PaintContourLine xarr = new Float32Array(2*kMAXCONTOUR), yarr = new Float32Array(2*kMAXCONTOUR), itarr = new Int32Array(2*kMAXCONTOUR), nlevels = levels.length, first_level = levels[0], last_level = levels[nlevels - 1], polys = [], x = [0, 0, 0, 0], y = [0, 0, 0, 0], zc = [0, 0, 0, 0], ir = [0, 0, 0, 0], arrx = handle.grx, arry = handle.gry; let lj = 0; const LinearSearch = zvalue => { if (zvalue >= last_level) return nlevels - 1; for (let kk = 0; kk < nlevels; ++kk) { if (zvalue < levels[kk]) return kk-1; } return nlevels - 1; }, BinarySearch = zvalue => { if (zvalue < first_level) return -1; if (zvalue >= last_level) return nlevels - 1; let l = 0, r = nlevels - 1, m; while (r - l > 1) { m = Math.round((r + l) / 2); if (zvalue < levels[m]) r = m; else l = m; } return l; }, LevelSearch = nlevels < 10 ? LinearSearch : BinarySearch, PaintContourLine = (elev1, icont1, x1, y1, elev2, icont2, x2, y2) => { /* Double_t *xarr, Double_t *yarr, Int_t *itarr, Double_t *levels */ const vert = (x1 === x2), tlen = vert ? (y2 - y1) : (x2 - x1), tdif = elev2 - elev1; let n = icont1 + 1, ii = lj-1, icount = 0, xlen, pdif, diff, elev; const maxii = ii + kMAXCONTOUR/2 -3; while (n <= icont2 && ii <= maxii) { // elev = fH->GetContourLevel(n); elev = levels[n]; diff = elev - elev1; pdif = diff/tdif; xlen = tlen*pdif; if (vert) { xarr[ii] = x1; yarr[ii] = y1 + xlen; } else { xarr[ii] = x1 + xlen; yarr[ii] = y1; } itarr[ii] = n; icount++; ii += 2; n++; } return icount; }; let ipoly, poly, npmax = 0, i, j, k, m, n, ljfill, count, xsave, ysave, itars, ix, jx; for (j = handle.j1; j < handle.j2-1; ++j) { y[1] = y[0] = (arry[j] + arry[j+1])/2; y[3] = y[2] = (arry[j+1] + arry[j+2])/2; for (i = handle.i1; i < handle.i2-1; ++i) { zc[0] = histo.getBinContent(i+1, j+1); zc[1] = histo.getBinContent(i+2, j+1); zc[2] = histo.getBinContent(i+2, j+2); zc[3] = histo.getBinContent(i+1, j+2); for (k = 0; k < 4; k++) ir[k] = LevelSearch(zc[k]); if ((ir[0] !== ir[1]) || (ir[1] !== ir[2]) || (ir[2] !== ir[3]) || (ir[3] !== ir[0])) { x[3] = x[0] = (arrx[i] + arrx[i+1])/2; x[2] = x[1] = (arrx[i+1] + arrx[i+2])/2; if (zc[0] <= zc[1]) n = 0; else n = 1; if (zc[2] <= zc[3]) m = 2; else m = 3; if (zc[n] > zc[m]) n = m; n++; lj=1; for (ix=1; ix<=4; ix++) { m = n%4 + 1; ljfill = PaintContourLine(zc[n-1], ir[n-1], x[n-1], y[n-1], zc[m-1], ir[m-1], x[m-1], y[m-1]); lj += 2*ljfill; n = m; } if (zc[0] <= zc[1]) n = 0; else n = 1; if (zc[2] <= zc[3]) m = 2; else m = 3; if (zc[n] > zc[m]) n = m; n++; lj=2; for (ix=1; ix<=4; ix++) { m = (n === 1) ? 4 : n-1; ljfill = PaintContourLine(zc[n-1], ir[n-1], x[n-1], y[n-1], zc[m-1], ir[m-1], x[m-1], y[m-1]); lj += 2*ljfill; n = m; } // Re-order endpoints count = 0; for (ix = 1; ix <= lj - 5; ix += 2) { // count = 0; while (itarr[ix-1] !== itarr[ix]) { xsave = xarr[ix]; ysave = yarr[ix]; itars = itarr[ix]; for (jx=ix; jx<=lj-5; jx +=2) { xarr[jx] = xarr[jx+2]; yarr[jx] = yarr[jx+2]; itarr[jx] = itarr[jx+2]; } xarr[lj-3] = xsave; yarr[lj-3] = ysave; itarr[lj-3] = itars; if (count > kMAXCOUNT) break; count++; } } if (count > 100) continue; for (ix = 1; ix <= lj - 2; ix += 2) { ipoly = itarr[ix-1]; if ((ipoly >= 0) && (ipoly < levels.length)) { poly = polys[ipoly]; if (!poly) poly = polys[ipoly] = createTPolyLine(kMAXCONTOUR*4, true); const np = poly.fLastPoint; if (np < poly.fN-2) { poly.fX[np+1] = Math.round(xarr[ix-1]); poly.fY[np+1] = Math.round(yarr[ix-1]); poly.fX[np+2] = Math.round(xarr[ix]); poly.fY[np+2] = Math.round(yarr[ix]); poly.fLastPoint = np+2; npmax = Math.max(npmax, poly.fLastPoint+1); } else { // console.log(`reject point ${poly.fLastPoint}`); } } } } // end of if (ir[0] } // end of j } // end of i const polysort = new Int32Array(levels.length); let first = 0; // find first positive contour for (ipoly = 0; ipoly < levels.length; ipoly++) if (levels[ipoly] >= 0) { first = ipoly; break; } // store negative contours from 0 to minimum, then all positive contours k = 0; for (ipoly = first-1; ipoly >= 0; ipoly--) { polysort[k] = ipoly; k++; } for (ipoly = first; ipoly < levels.length; ipoly++) { polysort[k] = ipoly; k++; } const xp = new Float32Array(2*npmax), yp = new Float32Array(2*npmax), has_func = isFunc(palette.calcColorIndex); // rcanvas for v7 for (k = 0; k < levels.length; ++k) { ipoly = polysort[k]; poly = polys[ipoly]; if (!poly) continue; const colindx = has_func ? palette.calcColorIndex(ipoly, levels.length) : ipoly, xx = poly.fX, yy = poly.fY, np2 = poly.fLastPoint+1, xmin = 0, ymin = 0; let istart = 0, iminus, iplus, nadd; while (true) { iminus = npmax; iplus = iminus+1; xp[iminus]= xx[istart]; yp[iminus] = yy[istart]; xp[iplus] = xx[istart+1]; yp[iplus] = yy[istart+1]; xx[istart] = xx[istart+1] = xmin; yy[istart] = yy[istart+1] = ymin; while (true) { nadd = 0; for (i = 2; i < np2; i += 2) { if ((iplus < 2*npmax-1) && (xx[i] === xp[iplus]) && (yy[i] === yp[iplus])) { iplus++; xp[iplus] = xx[i+1]; yp[iplus] = yy[i+1]; xx[i] = xx[i+1] = xmin; yy[i] = yy[i+1] = ymin; nadd++; } if ((iminus > 0) && (xx[i+1] === xp[iminus]) && (yy[i+1] === yp[iminus])) { iminus--; xp[iminus] = xx[i]; yp[iminus] = yy[i]; xx[i] = xx[i+1] = xmin; yy[i] = yy[i+1] = ymin; nadd++; } } if (nadd === 0) break; } if ((iminus+1 < iplus) && (iminus >= 0)) contour_func(colindx, xp, yp, iminus, iplus, ipoly); istart = 0; for (i = 2; i < np2; i += 2) { if (xx[i] !== xmin && yy[i] !== ymin) { istart = i; break; } } if (istart === 0) break; } } } /** @summary Handle 3D triangles with color levels */ class Triangles3DHandler { constructor(ilevels, grz, grz_min, grz_max, dolines, donormals, dogrid) { let levels = [grz_min, grz_max]; // just cut top/bottom parts if (ilevels) { // recalculate levels into graphical coordinates levels = new Float32Array(ilevels.length); for (let ll = 0; ll < ilevels.length; ++ll) levels[ll] = grz(ilevels[ll]); } Object.assign(this, { grz_min, grz_max, dolines, donormals, dogrid }); this.loop = 0; const nfaces = [], posbuf = [], posbufindx = [], // buffers for faces pntbuf = new Float32Array(6*3), // maximal 6 points gridpnts = new Float32Array(2*3), levels_eps = (levels.at(-1) - levels.at(0)) / levels.length / 1e2; let nsegments = 0, lpos = null, lindx = 0, // buffer for lines ngridsegments = 0, grid = null, gindx = 0, // buffer for grid lines segments normindx = [], // buffer to remember place of vertex for each bin pntindx = 0, lastpart = 0, gridcnt = 0; function checkSide(z, level1, level2, eps) { return (z < level1 - eps) ? -1 : (z > level2 + eps ? 1 : 0); } this.createNormIndex = function(handle) { // for each bin maximal 8 points reserved if (handle.donormals) normindx = new Int32Array((handle.i2-handle.i1)*(handle.j2-handle.j1)*8).fill(-1); }; this.createBuffers = function() { if (!this.loop) return; for (let lvl = 1; lvl < levels.length; ++lvl) { if (nfaces[lvl]) { posbuf[lvl] = new Float32Array(nfaces[lvl] * 9); posbufindx[lvl] = 0; } } if (this.dolines && (nsegments > 0)) lpos = new Float32Array(nsegments * 6); if (this.dogrid && (ngridsegments > 0)) grid = new Float32Array(ngridsegments * 6); }; this.addLineSegment = function(x1, y1, z1, x2, y2, z2) { if (!this.dolines) return; const side1 = checkSide(z1, this.grz_min, this.grz_max, 0), side2 = checkSide(z2, this.grz_min, this.grz_max, 0); if ((side1 === side2) && (side1 !== 0)) return; if (!this.loop) return ++nsegments; if (side1 !== 0) { const diff = z2 - z1; z1 = (side1 < 0) ? this.grz_min : this.grz_max; x1 = x2 - (x2 - x1) / diff * (z2 - z1); y1 = y2 - (y2 - y1) / diff * (z2 - z1); } if (side2 !== 0) { const diff = z1 - z2; z2 = (side2 < 0) ? this.grz_min : this.grz_max; x2 = x1 - (x1 - x2) / diff * (z1 - z2); y2 = y1 - (y1 - y2) / diff * (z1 - z2); } lpos[lindx] = x1; lpos[lindx+1] = y1; lpos[lindx+2] = z1; lindx+=3; lpos[lindx] = x2; lpos[lindx+1] = y2; lpos[lindx+2] = z2; lindx+=3; }; function addCrossingPoint(xx1, yy1, zz1, xx2, yy2, zz2, crossz, with_grid) { if (pntindx >= pntbuf.length) console.log('more than 6 points???'); const part = (crossz - zz1) / (zz2 - zz1); let shift = 3; if ((lastpart !== 0) && (Math.abs(part) < Math.abs(lastpart))) { // while second crossing point closer than first to original, move it in memory pntbuf[pntindx] = pntbuf[pntindx-3]; pntbuf[pntindx+1] = pntbuf[pntindx-2]; pntbuf[pntindx+2] = pntbuf[pntindx-1]; pntindx-=3; shift = 6; } pntbuf[pntindx] = xx1 + part*(xx2-xx1); pntbuf[pntindx+1] = yy1 + part*(yy2-yy1); pntbuf[pntindx+2] = crossz; if (with_grid && grid) { gridpnts[gridcnt] = pntbuf[pntindx]; gridpnts[gridcnt+1] = pntbuf[pntindx+1]; gridpnts[gridcnt+2] = pntbuf[pntindx+2]; gridcnt += 3; } pntindx += shift; lastpart = part; } function rememberVertex(indx, handle, ii, jj) { const bin = ((ii-handle.i1) * (handle.j2-handle.j1) + (jj-handle.j1))*8; if (normindx[bin] >= 0) return console.error('More than 8 vertexes for the bin'); const pos = bin + 8 + normindx[bin]; // position where write index normindx[bin]--; normindx[pos] = indx; // at this moment index can be overwritten, means all 8 position are there } this.addMainTriangle = function(x1, y1, z1, x2, y2, z2, x3, y3, z3, is_first, handle, i, j) { for (let lvl = 1; lvl < levels.length; ++lvl) { let side1 = checkSide(z1, levels[lvl-1], levels[lvl], levels_eps), side2 = checkSide(z2, levels[lvl-1], levels[lvl], levels_eps), side3 = checkSide(z3, levels[lvl-1], levels[lvl], levels_eps), side_sum = side1 + side2 + side3; // always show top segments if ((lvl > 1) && (lvl === levels.length - 1) && (side_sum === 3) && (z1 <= this.grz_max)) side1 = side2 = side3 = side_sum = 0; if (side_sum === 3) continue; if (side_sum === -3) return; if (!this.loop) { let npnts = Math.abs(side2-side1) + Math.abs(side3-side2) + Math.abs(side1-side3); if (side1 === 0) ++npnts; if (side2 === 0) ++npnts; if (side3 === 0) ++npnts; if ((npnts === 1) || (npnts === 2)) console.error(`FOUND npnts = ${npnts}`); if (npnts > 2) { if (nfaces[lvl] === undefined) nfaces[lvl] = 0; nfaces[lvl] += npnts-2; } // check if any(contours for given level exists if (((side1 > 0) || (side2 > 0) || (side3 > 0)) && ((side1 !== side2) || (side2 !== side3) || (side3 !== side1))) ++ngridsegments; continue; } gridcnt = 0; pntindx = 0; if (side1 === 0) { pntbuf[pntindx] = x1; pntbuf[pntindx+1] = y1; pntbuf[pntindx+2] = z1; pntindx += 3; } if (side1 !== side2) { // order is important, should move from 1->2 point, checked via lastpart lastpart = 0; if ((side1 < 0) || (side2 < 0)) addCrossingPoint(x1, y1, z1, x2, y2, z2, levels[lvl-1]); if ((side1 > 0) || (side2 > 0)) addCrossingPoint(x1, y1, z1, x2, y2, z2, levels[lvl], true); } if (side2 === 0) { pntbuf[pntindx] = x2; pntbuf[pntindx+1] = y2; pntbuf[pntindx+2] = z2; pntindx += 3; } if (side2 !== side3) { // order is important, should move from 2->3 point, checked via lastpart lastpart = 0; if ((side2 < 0) || (side3 < 0)) addCrossingPoint(x2, y2, z2, x3, y3, z3, levels[lvl-1]); if ((side2 > 0) || (side3 > 0)) addCrossingPoint(x2, y2, z2, x3, y3, z3, levels[lvl], true); } if (side3 === 0) { pntbuf[pntindx] = x3; pntbuf[pntindx+1] = y3; pntbuf[pntindx+2] = z3; pntindx += 3; } if (side3 !== side1) { // order is important, should move from 3->1 point, checked via lastpart lastpart = 0; if ((side3 < 0) || (side1 < 0)) addCrossingPoint(x3, y3, z3, x1, y1, z1, levels[lvl-1]); if ((side3 > 0) || (side1 > 0)) addCrossingPoint(x3, y3, z3, x1, y1, z1, levels[lvl], true); } if (pntindx === 0) continue; if (pntindx < 9) { console.log(`found ${pntindx/3} points, must be at least 3`); continue; } if (grid && (gridcnt === 6)) { for (let jj = 0; jj < 6; ++jj) grid[gindx+jj] = gridpnts[jj]; gindx += 6; } // if three points and surf === 14, remember vertex for each point const buf = posbuf[lvl]; let s = posbufindx[lvl]; if (this.donormals && (pntindx === 9)) { rememberVertex(s, handle, i, j); rememberVertex(s+3, handle, i+1, is_first ? j+1 : j); rememberVertex(s+6, handle, is_first ? i : i+1, j+1); } for (let k1 = 3; k1 < pntindx - 3; k1 += 3) { buf[s] = pntbuf[0]; buf[s+1] = pntbuf[1]; buf[s+2] = pntbuf[2]; s+=3; buf[s] = pntbuf[k1]; buf[s+1] = pntbuf[k1+1]; buf[s+2] = pntbuf[k1+2]; s+=3; buf[s] = pntbuf[k1+3]; buf[s+1] = pntbuf[k1+4]; buf[s+2] = pntbuf[k1+5]; s+=3; } posbufindx[lvl] = s; } }; this.callFuncs = function(meshFunc, linesFunc) { for (let lvl = 1; lvl < levels.length; ++lvl) { if (posbuf[lvl] && meshFunc) meshFunc(lvl, posbuf[lvl], normindx); } if (lpos && linesFunc) { if (nsegments*6 !== lindx) console.error(`SURF lines mismmatch nsegm=${nsegments} lindx=${lindx} diff=${nsegments*6 - lindx}`); linesFunc(false, lpos); } if (grid && linesFunc) { if (ngridsegments*6 !== gindx) console.error(`SURF grid draw mismatch ngridsegm=${ngridsegments} gindx=${gindx} diff=${ngridsegments*6 - gindx}`); linesFunc(true, grid); } }; } } /** @summary Build 3d surface * @desc Make it independent from three.js to be able reuse it for 2D case * @private */ function buildSurf3D(histo, handle, ilevels, meshFunc, linesFunc) { const main_grz = handle.grz, arrx = handle.original ? handle.origx : handle.grx, arry = handle.original ? handle.origy : handle.gry, triangles = new Triangles3DHandler(ilevels, handle.grz, handle.grz_min, handle.grz_max, handle.dolines, handle.donormals, handle.dogrid); let i, j, x1, x2, y1, y2, z11, z12, z21, z22; triangles.createNormIndex(handle); for (triangles.loop = 0; triangles.loop < 2; ++triangles.loop) { triangles.createBuffers(); for (i = handle.i1; i < handle.i2-1; ++i) { x1 = handle.original ? 0.5 * (arrx[i] + arrx[i+1]) : arrx[i]; x2 = handle.original ? 0.5 * (arrx[i+1] + arrx[i+2]) : arrx[i+1]; for (j = handle.j1; j < handle.j2-1; ++j) { y1 = handle.original ? 0.5 * (arry[j] + arry[j+1]) : arry[j]; y2 = handle.original ? 0.5 * (arry[j+1] + arry[j+2]) : arry[j+1]; z11 = main_grz(histo.getBinContent(i+1, j+1)); z12 = main_grz(histo.getBinContent(i+1, j+2)); z21 = main_grz(histo.getBinContent(i+2, j+1)); z22 = main_grz(histo.getBinContent(i+2, j+2)); triangles.addMainTriangle(x1, y1, z11, x2, y2, z22, x1, y2, z12, true, handle, i, j); triangles.addMainTriangle(x1, y1, z11, x2, y1, z21, x2, y2, z22, false, handle, i, j); triangles.addLineSegment(x1, y2, z12, x1, y1, z11); triangles.addLineSegment(x1, y1, z11, x2, y1, z21); if (i === handle.i2 - 2) triangles.addLineSegment(x2, y1, z21, x2, y2, z22); if (j === handle.j2 - 2) triangles.addLineSegment(x1, y2, z12, x2, y2, z22); } } } triangles.callFuncs(meshFunc, linesFunc); } /** * @summary Painter for TH2 classes * @private */ class TH2Painter extends THistPainter { /** @summary constructor * @param {object} histo - histogram object */ constructor(dom, histo) { super(dom, histo); this.wheel_zoomy = true; } /** @summary cleanup painter */ cleanup() { delete this.tt_handle; super.cleanup(); } /** @summary Returns histogram * @desc Also assigns custom getBinContent method for TProfile2D if PROJXY options specified */ getHisto() { const histo = super.getHisto(); if (histo?._typename === clTProfile2D) { if (!histo.$getBinContent) histo.$getBinContent = histo.getBinContent; switch (this.options?.Profile2DProj) { case 'B': histo.getBinContent = histo.getBinEntries; break; case 'C=E': histo.getBinContent = function(i, j) { return this.getBinError(this.getBin(i, j)); }; break; case 'W': histo.getBinContent = function(i, j) { return this.$getBinContent(i, j) * this.getBinEntries(i, j); }; break; default: histo.getBinContent = histo.$getBinContent; break; } } return histo; } /** @summary Toggle projection */ toggleProjection(kind, width) { if ((kind === 'Projections') || (kind === 'Off')) kind = ''; let widthX = width, widthY = width; if (isStr(kind) && (kind.indexOf('XY') === 0)) { const ws = (kind.length > 2) ? kind.slice(2) : ''; kind = 'XY'; widthX = widthY = parseInt(ws) || 1; } else if (isStr(kind) && (kind.length > 1)) { const ps = kind.indexOf('_'); if ((ps > 0) && (kind[0] === 'X') && (kind[ps+1] === 'Y')) { widthX = parseInt(kind.slice(1, ps)) || 1; widthY = parseInt(kind.slice(ps+2)) || 1; kind = 'XY'; } else if ((ps > 0) && (kind[0] === 'Y') && (kind[ps+1] === 'X')) { widthY = parseInt(kind.slice(1, ps)) || 1; widthX = parseInt(kind.slice(ps+2)) || 1; kind = 'XY'; } else { widthX = widthY = parseInt(kind.slice(1)) || 1; kind = kind[0]; } } if (!widthX && !widthY) widthX = widthY = 1; if (kind && (this.is_projection === kind)) { if ((this.projection_widthX === widthX) && (this.projection_widthY === widthY)) kind = ''; else { this.projection_widthX = widthX; this.projection_widthY = widthY; return; } } delete this.proj_hist; const new_proj = (this.is_projection === kind) ? '' : kind; this.projection_widthX = widthX; this.projection_widthY = widthY; this.is_projection = ''; // avoid projection handling until area is created return this.provideSpecialDrawArea(new_proj).then(() => { this.is_projection = new_proj; return this.redrawProjection(); }); } /** @summary Redraw projection */ async redrawProjection(ii1, ii2, jj1, jj2) { if (!this.is_projection) return false; if (jj2 === undefined) { if (!this.tt_handle) return; ii1 = Math.round((this.tt_handle.i1 + this.tt_handle.i2)/2); ii2 = ii1+1; jj1 = Math.round((this.tt_handle.j1 + this.tt_handle.j2)/2); jj2 = jj1+1; } const canp = this.getCanvPainter(); if (canp && !canp._readonly && (this.snapid !== undefined)) { // this is when projection should be created on the server side if (((this.is_projection === 'X') || (this.is_projection === 'XY')) && !canp.websocketTimeout('projX')) { if (canp.sendWebsocket(`EXECANDSEND:DXPROJ:${this.snapid}:ProjectionX("_projx",${jj1+1},${jj2},"")`)) canp.websocketTimeout('projX', 1000); } if (((this.is_projection === 'Y') || (this.is_projection === 'XY')) && !canp.websocketTimeout('projY')) { if (canp.sendWebsocket(`EXECANDSEND:DYPROJ:${this.snapid}:ProjectionY("_projy",${ii1+1},${ii2},"")`)) canp.websocketTimeout('projY', 1000); } return true; } if (this.doing_projection) return false; this.doing_projection = true; const histo = this.getHisto(), createXProject = () => { const p = createHistogram(clTH1D, this.nbinsx); Object.assign(p.fXaxis, histo.fXaxis); p.fName = 'xproj'; p.fTitle = 'X projection'; return p; }, createYProject = () => { const p = createHistogram(clTH1D, this.nbinsy); Object.assign(p.fXaxis, histo.fYaxis); p.fName = 'yproj'; p.fTitle = 'Y projection'; return p; }, fillProjectHist = (kind, p) => { let first = 0, last = -1; if (kind === 'X') { for (let i = 0; i < this.nbinsx; ++i) { let sum = 0; for (let j = jj1; j < jj2; ++j) sum += histo.getBinContent(i+1, j+1); p.setBinContent(i+1, sum); } p.fTitle = 'X projection ' + (jj1+1 === jj2 ? `bin ${jj2}` : `bins [${jj1+1} .. ${jj2}]`); if (this.tt_handle) { first = this.tt_handle.i1+1; last = this.tt_handle.i2; } } else { for (let j = 0; j < this.nbinsy; ++j) { let sum = 0; for (let i = ii1; i < ii2; ++i) sum += histo.getBinContent(i+1, j+1); p.setBinContent(j+1, sum); } p.fTitle = 'Y projection ' + (ii1+1 === ii2 ? `bin ${ii2}` : `bins [${ii1+1} .. ${ii2}]`); if (this.tt_handle) { first = this.tt_handle.j1+1; last = this.tt_handle.j2; } } if (first < last) { p.fXaxis.fFirst = first; p.fXaxis.fLast = last; p.fXaxis.SetBit(EAxisBits.kAxisRange, (first !== 1) || (last !== p.fXaxis.fNbins)); } // reset statistic before display p.fEntries = 0; p.fTsumw = 0; }; if (!this.proj_hist) { switch (this.is_projection) { case 'X': this.proj_hist = createXProject(); break; case 'XY': this.proj_hist = createXProject(); this.proj_hist2 = createYProject(); break; default: this.proj_hist = createYProject(); } } if (this.is_projection === 'XY') { fillProjectHist('X', this.proj_hist); fillProjectHist('Y', this.proj_hist2); return this.drawInSpecialArea(this.proj_hist, '', 'X') .then(() => this.drawInSpecialArea(this.proj_hist2, '', 'Y')) .then(res => { delete this.doing_projection; return res; }); } fillProjectHist(this.is_projection, this.proj_hist); return this.drawInSpecialArea(this.proj_hist).then(res => { delete this.doing_projection; return res; }); } /** @summary Execute TH2 menu command * @desc Used to catch standard menu items and provide local implementation */ executeMenuCommand(method, args) { if (super.executeMenuCommand(method, args)) return true; if ((method.fName === 'SetShowProjectionX') || (method.fName === 'SetShowProjectionY')) { this.toggleProjection(method.fName[17], args && parseInt(args) ? parseInt(args) : 1); return true; } if (method.fName === 'SetShowProjectionXY') { this.toggleProjection('X' + args.replaceAll(',', '_Y')); return true; } return false; } /** @summary Fill histogram context menu */ fillHistContextMenu(menu) { if (!this.isTH2Poly() && this.getPadPainter()?.iscan) { let kind = this.is_projection || ''; if (kind) kind += this.projection_widthX; if ((this.projection_widthX !== this.projection_widthY) && (this.is_projection === 'XY')) kind = `X${this.projection_widthX}_Y${this.projection_widthY}`; const kinds = ['X1', 'X2', 'X3', 'X5', 'X10', 'Y1', 'Y2', 'Y3', 'Y5', 'Y10', 'XY1', 'XY2', 'XY3', 'XY5', 'XY10']; if (kind) kinds.unshift('Off'); menu.sub('Projections', () => menu.input('Input projection kind X1 or XY2 or X3_Y4', kind, 'string').then(val => this.toggleProjection(val))); for (let k = 0; k < kinds.length; ++k) menu.addchk(kind === kinds[k], kinds[k], kinds[k], arg => this.toggleProjection(arg)); menu.endsub(); } if (!this.isTH2Poly()) 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); const oldProject = this.options.Project; this.decodeOptions(arg); if ((oldProject === this.options.Project) || this.mode3d) this.interactiveRedraw('pad', 'drawopt'); else this.toggleProjection(this.options.Project); }); if (this.options.Color || this.options.Contour || this.options.Hist || this.options.Surf || this.options.Lego === 12 || this.options.Lego === 14) this.fillPaletteMenu(menu, true); } /** @summary Process click on histogram-defined buttons */ clickButton(funcname) { const res = super.clickButton(funcname); if (res) return res; if (this.isMainPainter()) { switch (funcname) { case 'ToggleColor': return this.toggleColor(); case 'Toggle3D': return this.toggleMode3D(); } } // all methods here should not be processed further return false; } /** @summary Fill pad toolbar with histogram-related functions */ fillToolbar() { super.fillToolbar(true); const pp = this.getPadPainter(); if (!pp) return; if (!this.isTH2Poly() && !this.options.Axis) pp.addPadButton('th2color', 'Toggle color', 'ToggleColor'); if (!this.options.Axis) pp.addPadButton('th2colorz', 'Toggle color palette', 'ToggleColorZ'); pp.addPadButton('th2draw3d', 'Toggle 3D mode', 'Toggle3D'); pp.showPadButtons(); } /** @summary Toggle color drawing mode */ toggleColor() { if (this.options.Mode3D) { this.options.Mode3D = false; this.options.Color = true; } else { this.options.Color = !this.options.Color; this.options.Scat = !this.options.Color; } this._can_move_colz = true; // indicate that next redraw can move Z scale this.copyOptionsToOthers(); return this.interactiveRedraw('pad', 'drawopt'); } /** @summary Perform automatic zoom inside non-zero region of histogram */ autoZoom() { if (this.isTH2Poly()) return; // not implemented const i1 = this.getSelectIndex('x', 'left', -1), i2 = this.getSelectIndex('x', 'right', 1), j1 = this.getSelectIndex('y', 'left', -1), j2 = this.getSelectIndex('y', 'right', 1), histo = this.getObject(); if ((i1 === i2) || (j1 === j2)) return; // first find minimum let min = histo.getBinContent(i1 + 1, j1 + 1); for (let i = i1; i < i2; ++i) { for (let j = j1; j < j2; ++j) min = Math.min(min, histo.getBinContent(i + 1, j + 1)); } if (min > 0) return; // if all points positive, no chance for auto-scale let ileft = i2, iright = i1, jleft = j2, jright = j1; for (let i = i1; i < i2; ++i) { for (let j = j1; j < j2; ++j) { if (histo.getBinContent(i + 1, j + 1) > min) { if (i < ileft) ileft = i; if (i >= iright) iright = i + 1; if (j < jleft) jleft = j; if (j >= jright) jright = j + 1; } } } let xmin, xmax, ymin, ymax, isany = false; if ((ileft === iright-1) && (ileft > i1+1) && (iright < i2-1)) { ileft--; iright++; } if ((jleft === jright-1) && (jleft > j1+1) && (jright < j2-1)) { jleft--; jright++; } if ((ileft > i1 || iright < i2) && (ileft < iright - 1)) { xmin = histo.fXaxis.GetBinLowEdge(ileft+1); xmax = histo.fXaxis.GetBinLowEdge(iright+1); isany = true; } if ((jleft > j1 || jright < j2) && (jleft < jright - 1)) { ymin = histo.fYaxis.GetBinLowEdge(jleft+1); ymax = histo.fYaxis.GetBinLowEdge(jright+1); isany = true; } if (isany) return this.getFramePainter().zoom(xmin, xmax, ymin, ymax); } /** @summary Scan TH2 histogram content */ scanContent(when_axis_changed) { // no need to re-scan histogram while result does not depend from axis selection if (when_axis_changed && this.nbinsx && this.nbinsy) return; const histo = this.getObject(); let i, j; this.extractAxesProperties(2); if (this.isTH2Poly()) { this.gminposbin = null; this.gminbin = this.gmaxbin = 0; for (let n = 0, len = histo.fBins.arr.length; n < len; ++n) { const bin_content = histo.fBins.arr[n].fContent; if (n === 0) this.gminbin = this.gmaxbin = bin_content; if (bin_content < this.gminbin) this.gminbin = bin_content; else if (bin_content > this.gmaxbin) this.gmaxbin = bin_content; if ((bin_content > 0) && ((this.gminposbin === null) || (this.gminposbin > bin_content))) this.gminposbin = bin_content; } } else { // global min/max, used at the moment in 3D drawing this.gminbin = this.gmaxbin = histo.getBinContent(1, 1); this.gminposbin = null; for (i = 0; i < this.nbinsx; ++i) { for (j = 0; j < this.nbinsy; ++j) { const bin_content = histo.getBinContent(i+1, j+1); if (bin_content < this.gminbin) this.gminbin = bin_content; else if (bin_content > this.gmaxbin) this.gmaxbin = bin_content; if (bin_content > 0) { if ((this.gminposbin === null) || (this.gminposbin > bin_content)) this.gminposbin = bin_content; } } } } // this value used for logz scale drawing if ((this.gminposbin === null) && (this.gmaxbin > 0)) this.gminposbin = this.gmaxbin*1e-4; let is_content = (this.gmaxbin !== 0) || (this.gminbin !== 0); // for TProfile2D show empty bin if there are entries for it if (!is_content && (histo._typename === clTProfile2D)) { for (i = 0; i < this.nbinsx && !is_content; ++i) { for (j = 0; j < this.nbinsy; ++j) { if (histo.getBinEntries(i + 1, j + 1)) { is_content = true; break; } } } } if (this.options.Axis > 0) { // Paint histogram axis only this.draw_content = false; } else if (this.isTH2Poly()) { this.draw_content = is_content || this.options.Line || this.options.Fill || this.options.Mark; if (!this.draw_content && this.options.Zero) { this.draw_content = true; this.options.Line = 1; } } else this.draw_content = is_content || this.options.ShowEmpty; } /** @summary Count TH2 histogram statistic * @desc Optionally one could provide condition function to select special range */ countStat(cond, count_skew) { if (!isFunc(cond)) cond = this.options.cutg ? (x, y) => this.options.cutg.IsInside(x, y) : null; const histo = this.getHisto(), xaxis = histo.fXaxis, yaxis = histo.fYaxis, fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), res = { name: histo.fName, entries: 0, eff_entries: 0, integral: 0, meanx: 0, meany: 0, rmsx: 0, rmsy: 0, matrix: [0, 0, 0, 0, 0, 0, 0, 0, 0], xmax: 0, ymax: 0, wmax: null, skewx: 0, skewy: 0, skewd: 0, kurtx: 0, kurty: 0, kurtd: 0 }, has_counted_stat = !fp.isAxisZoomed('x') && !fp.isAxisZoomed('y') && (Math.abs(histo.fTsumw) > 1e-300) && !cond; let stat_sum0 = 0, stat_sumw2 = 0, stat_sumx1 = 0, stat_sumy1 = 0, stat_sumx2 = 0, stat_sumy2 = 0, xside, yside, xx, yy, zz, xleft, xright, yleft, yright; if (this.isTH2Poly()) { const len = histo.fBins.arr.length; let i, bin, n, gr, ngr, numgraphs, numpoints; for (i = 0; i < len; ++i) { bin = histo.fBins.arr[i]; xside = (bin.fXmin > funcs.scale_xmax) ? 2 : (bin.fXmax < funcs.scale_xmin ? 0 : 1); yside = (bin.fYmin > funcs.scale_ymax) ? 2 : (bin.fYmax < funcs.scale_ymin ? 0 : 1); xx = yy = numpoints = 0; gr = bin.fPoly; numgraphs = 1; if (gr._typename === clTMultiGraph) { numgraphs = bin.fPoly.fGraphs.arr.length; gr = null; } for (ngr = 0; ngr < numgraphs; ++ngr) { if (!gr || (ngr > 0)) gr = bin.fPoly.fGraphs.arr[ngr]; for (n = 0; n < gr.fNpoints; ++n) { ++numpoints; xx += gr.fX[n]; yy += gr.fY[n]; } } if (numpoints > 1) { xx /= numpoints; yy /= numpoints; } zz = bin.fContent; res.entries += zz; res.matrix[yside * 3 + xside] += zz; if ((xside !== 1) || (yside !== 1) || (cond && !cond(xx, yy))) continue; if ((res.wmax === null) || (zz > res.wmax)) { res.wmax = zz; res.xmax = xx; res.ymax = yy; } if (!has_counted_stat) { stat_sum0 += zz; stat_sumw2 += zz * zz; stat_sumx1 += xx * zz; stat_sumy1 += yy * zz; stat_sumx2 += xx * xx * zz; stat_sumy2 += yy * yy * zz; } } } else { xleft = this.getSelectIndex('x', 'left'); xright = this.getSelectIndex('x', 'right'); yleft = this.getSelectIndex('y', 'left'); yright = this.getSelectIndex('y', 'right'); for (let xi = 0; xi <= this.nbinsx + 1; ++xi) { xside = (xi <= xleft) ? 0 : (xi > xright ? 2 : 1); xx = xaxis.GetBinCoord(xi - 0.5); for (let yi = 0; yi <= this.nbinsy + 1; ++yi) { yside = (yi <= yleft) ? 0 : (yi > yright ? 2 : 1); yy = yaxis.GetBinCoord(yi - 0.5); zz = histo.getBinContent(xi, yi); res.entries += zz; res.matrix[yside * 3 + xside] += zz; if ((xside !== 1) || (yside !== 1) || (cond && !cond(xx, yy))) continue; if ((res.wmax === null) || (zz > res.wmax)) { res.wmax = zz; res.xmax = xx; res.ymax = yy; } if (!has_counted_stat) { stat_sum0 += zz; stat_sumw2 += zz * zz; stat_sumx1 += xx * zz; stat_sumy1 += yy * zz; stat_sumx2 += xx**2 * zz; stat_sumy2 += yy**2 * zz; } } } } if (has_counted_stat) { stat_sum0 = histo.fTsumw; stat_sumw2 = histo.fTsumw2; stat_sumx1 = histo.fTsumwx; stat_sumx2 = histo.fTsumwx2; stat_sumy1 = histo.fTsumwy; stat_sumy2 = histo.fTsumwy2; } if (Math.abs(stat_sum0) > 1e-300) { res.meanx = stat_sumx1 / stat_sum0; res.meany = stat_sumy1 / stat_sum0; res.rmsx = Math.sqrt(Math.abs(stat_sumx2 / stat_sum0 - res.meanx**2)); res.rmsy = Math.sqrt(Math.abs(stat_sumy2 / stat_sum0 - res.meany**2)); } if (res.wmax === null) res.wmax = 0; res.integral = stat_sum0; if (histo.fEntries > 0) res.entries = histo.fEntries; res.eff_entries = stat_sumw2 ? stat_sum0*stat_sum0/stat_sumw2 : Math.abs(stat_sum0); if (count_skew && !this.isTH2Poly()) { let sumx3 = 0, sumy3 = 0, sumx4 = 0, sumy4 = 0, np = 0, w; for (let xi = xleft; xi < xright; ++xi) { xx = xaxis.GetBinCoord(xi + 0.5); for (let yi = yleft; yi < yright; ++yi) { yy = yaxis.GetBinCoord(yi + 0.5); if (cond && !cond(xx, yy)) continue; w = histo.getBinContent(xi + 1, yi + 1); np += w; sumx3 += w * Math.pow(xx - res.meanx, 3); sumy3 += w * Math.pow(yy - res.meany, 3); sumx4 += w * Math.pow(xx - res.meanx, 4); sumy4 += w * Math.pow(yy - res.meany, 4); } } const stddev3x = Math.pow(res.rmsx, 3), stddev3y = Math.pow(res.rmsy, 3), stddev4x = Math.pow(res.rmsx, 4), stddev4y = Math.pow(res.rmsy, 4); if (np * stddev3x !== 0) res.skewx = sumx3 / (np * stddev3x); if (np * stddev3y !== 0) res.skewy = sumy3 / (np * stddev3y); res.skewd = res.eff_entries > 0 ? Math.sqrt(6/res.eff_entries) : 0; if (np * stddev4x !== 0) res.kurtx = sumx4 / (np * stddev4x) - 3; if (np * stddev4y !== 0) res.kurty = sumy4 / (np * stddev4y) - 3; res.kurtd = res.eff_entries > 0 ? Math.sqrt(24/res.eff_entries) : 0; } return res; } /** @summary Fill TH2 statistic in 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; const print_name = Math.floor(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)); stat.clearPave(); 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 x = ' + stat.format(data.meanx)); stat.addText('Mean y = ' + stat.format(data.meany)); } if (print_rms > 0) { stat.addText('Std Dev x = ' + stat.format(data.rmsx)); stat.addText('Std Dev y = ' + stat.format(data.rmsy)); } if (print_integral > 0) stat.addText('Integral = ' + stat.format(data.matrix[4], 'entries')); if (print_skew === 2) { stat.addText(`Skewness x = ${stat.format(data.skewx)} #pm ${stat.format(data.skewd)}`); stat.addText(`Skewness y = ${stat.format(data.skewy)} #pm ${stat.format(data.skewd)}`); } else if (print_skew > 0) { stat.addText(`Skewness x = ${stat.format(data.skewx)}`); stat.addText(`Skewness y = ${stat.format(data.skewy)}`); } if (print_kurt === 2) { stat.addText(`Kurtosis x = ${stat.format(data.kurtx)} #pm ${stat.format(data.kurtd)}`); stat.addText(`Kurtosis y = ${stat.format(data.kurty)} #pm ${stat.format(data.kurtd)}`); } else if (print_kurt > 0) { stat.addText(`Kurtosis x = ${stat.format(data.kurtx)}`); stat.addText(`Kurtosis y = ${stat.format(data.kurty)}`); } if ((print_under > 0) || (print_over > 0)) { const get = i => data.matrix[i].toFixed(0); stat.addText(`${get(6)} | ${get(7)} | ${get(7)}`); stat.addText(`${get(3)} | ${get(4)} | ${get(5)}`); stat.addText(`${get(0)} | ${get(1)} | ${get(2)}`); } if (dofit) stat.fillFunctionStat(this.findFunction(clTF2), dofit, 2); return true; } /** @summary Draw TH2 bins as colors */ drawBinsColor() { const histo = this.getHisto(), handle = this.prepareDraw(), cntr = this.getContour(), palette = this.getHistPalette(), entries = [], has_sumw2 = histo.fSumw2?.length, show_empty = this.options.ShowEmpty, can_merge_x = (this.options.Color !== 7) || ((handle.xbar1 === 0) && (handle.xbar2 === 1)), can_merge_y = (this.options.Color !== 7) || ((handle.ybar1 === 0) && (handle.ybar2 === 1)), colindx0 = cntr.getPaletteIndex(palette, 0); let dx, dy, x1, y2, binz, is_zero, colindx, last_entry = null, skip_zero = !this.options.Zero, skip_bin; const test_cutg = this.options.cutg, flush_last_entry = () => { last_entry.path += `h${dx}v${last_entry.y1-last_entry.y2}h${-dx}z`; last_entry = null; }; // check in the beginning if zero can be skipped if (!skip_zero && !show_empty && (colindx0 === null)) skip_zero = true; // special check for TProfile2D - empty bin with no entries shown if (skip_zero && (histo?._typename === clTProfile2D)) skip_zero = 1; // now start build for (let i = handle.i1; i < handle.i2; ++i) { dx = (handle.grx[i+1] - handle.grx[i]) || 1; if (can_merge_x) x1 = handle.grx[i]; else { x1 = Math.round(handle.grx[i] + dx*handle.xbar1); dx = Math.round(dx*(handle.xbar2 - handle.xbar1)) || 1; } for (let j = handle.j2 - 1; j >= handle.j1; --j) { binz = histo.getBinContent(i + 1, j + 1); is_zero = (binz === 0) && (!has_sumw2 || histo.fSumw2[histo.getBin(i + 1, j + 1)] === 0); skip_bin = is_zero && ((skip_zero === 1) ? !histo.getBinEntries(i + 1, j + 1) : skip_zero); if (skip_bin || (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5)))) { if (last_entry) flush_last_entry(); continue; } colindx = cntr.getPaletteIndex(palette, binz); if (colindx === null) { if (is_zero && (show_empty || (skip_zero === 1))) colindx = colindx0 || 0; else { if (last_entry) flush_last_entry(); continue; } } dy = (handle.gry[j] - handle.gry[j+1]) || 1; if (can_merge_y) y2 = handle.gry[j+1]; else { y2 = Math.round(handle.gry[j] - dy*handle.ybar2); dy = Math.round(dy*(handle.ybar2 - handle.ybar1)) || 1; } const cmd1 = `M${x1},${y2}`; let entry = entries[colindx]; if (!entry) entry = entries[colindx] = { path: cmd1 }; else if (can_merge_y && (entry === last_entry)) { entry.y1 = y2 + dy; continue; } else { const ddx = x1 - entry.x1, ddy = y2 - entry.y2; if (ddx || ddy) { const cmd2 = `m${ddx},${ddy}`; entry.path += (cmd2.length < cmd1.length) ? cmd2 : cmd1; } } if (last_entry) flush_last_entry(); entry.x1 = x1; entry.y2 = y2; if (can_merge_y) { entry.y1 = y2 + dy; last_entry = entry; } else entry.path += `h${dx}v${dy}h${-dx}z`; } if (last_entry) flush_last_entry(); } entries.forEach((entry, ecolindx) => { if (entry) { this.draw_g.append('svg:path') .attr('fill', palette.getColor(ecolindx)) .attr('d', entry.path); } }); return handle; } /** @summary Draw TH2 bins as colors in polar coordinates */ drawBinsPolar() { const histo = this.getHisto(), handle = this.prepareDraw(), cntr = this.getContour(), palette = this.getHistPalette(), entries = [], has_sumw2 = histo.fSumw2?.length, show_empty = this.options.ShowEmpty, colindx0 = cntr.getPaletteIndex(palette, 0); let binz, is_zero, colindx, skip_zero = !this.options.Zero, skip_bin; const test_cutg = this.options.cutg; // check in the beginning if zero can be skipped if (!skip_zero && !show_empty && (colindx0 === null)) skip_zero = true; // special check for TProfile2D - empty bin with no entries shown if (skip_zero && (histo?._typename === clTProfile2D)) skip_zero = 1; handle.getBinPath = function(i, j) { const a1 = 2 * Math.PI * Math.max(0, this.grx[i]) / this.width, a2 = 2 * Math.PI * Math.min(this.grx[i + 1], this.width) / this.width, r2 = Math.min(this.gry[j], this.height) / this.height, r1 = Math.max(0, this.gry[j + 1]) / this.height, side = a2 - a1 > Math.PI ? 1 : 0; // handle very large sector // do not process bins outside visible range if ((a2 <= a1) || (r2 <= r1)) return ''; const x0 = this.width/2, y0 = this.height/2, rx1 = r1 * this.width/2, rx2 = r2 * this.width/2, ry1 = r1 * this.height/2, ry2 = r2 * this.height/2, x11 = x0 + rx1 * Math.cos(a1), x12 = x0 + rx1 * Math.cos(a2), y11 = y0 + ry1 * Math.sin(a1), y12 = y0 + ry1 * Math.sin(a2), x21 = x0 + rx2 * Math.cos(a1), x22 = x0 + rx2 * Math.cos(a2), y21 = y0 + ry2 * Math.sin(a1), y22 = y0 + ry2 * Math.sin(a2); return `M${x11.toFixed(2)},${y11.toFixed(2)}` + `A${rx1.toFixed(2)},${ry1.toFixed(2)},0,${side},1,${x12.toFixed(2)},${y12.toFixed(2)}` + `L${x22.toFixed(2)},${y22.toFixed(2)}` + `A${rx2.toFixed(2)},${ry2.toFixed(2)},0,${side},0,${x21.toFixed(2)},${y21.toFixed(2)}Z`; }; handle.findBin = function(x, y) { const x0 = this.width/2, y0 = this.height/2; let angle = Math.atan2((y - y0) / this.height, (x - x0) / this.width), i, j; const radius = Math.abs(Math.cos(angle)) > 0.5 ? (x - x0) / Math.cos(angle) / this.width * 2 : (y - y0) / Math.sin(angle) / this.height * 2; if (angle < 0) angle += 2*Math.PI; for (i = this.i1; i < this.i2; ++i) { const a1 = 2 * Math.PI * this.grx[i] / this.width, a2 = 2 * Math.PI * this.grx[i + 1] / this.width; if ((a1 <= angle) && (angle <= a2)) break; } for (j = this.j1; j < this.j2; ++j) { const r2 = this.gry[j] / this.height, r1 = this.gry[j + 1] / this.height; if ((r1 <= radius) && (radius <= r2)) break; } return { i, j }; }; // now start build for (let i = handle.i1; i < handle.i2; ++i) { for (let j = handle.j2 - 1; j >= handle.j1; --j) { binz = histo.getBinContent(i + 1, j + 1); is_zero = (binz === 0) && (!has_sumw2 || histo.fSumw2[histo.getBin(i + 1, j + 1)] === 0); skip_bin = is_zero && ((skip_zero === 1) ? !histo.getBinEntries(i + 1, j + 1) : skip_zero); if (skip_bin || (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5)))) continue; colindx = cntr.getPaletteIndex(palette, binz); if (colindx === null) { if (is_zero && (show_empty || (skip_zero === 1))) colindx = colindx0 || 0; else continue; } const cmd = handle.getBinPath(i, j); if (!cmd) continue; const entry = entries[colindx]; if (!entry) entries[colindx] = { path: cmd }; else entry.path += cmd; } } entries.forEach((entry, ecolindx) => { if (entry) { this.draw_g.append('svg:path') .attr('fill', palette.getColor(ecolindx)) .attr('d', entry.path); } }); return handle; } /** @summary Draw histogram bins with projection function */ drawBinsProjected() { const handle = this.prepareDraw({ rounding: false, nozoom: true, extra: 100, original: true }), main = this.getFramePainter(), funcs = this.getHistGrFuncs(main), ilevels = this.getContourLevels(), palette = this.getHistPalette(), func = main.getProjectionFunc(); handle.grz = z => z; handle.grz_min = ilevels.at(0); handle.grz_max = ilevels.at(-1); buildSurf3D(this.getHisto(), handle, ilevels, (lvl, pos) => { let dd = '', lastx, lasty; for (let i = 0; i < pos.length; i += 3) { const pnt = func(pos[i], pos[i + 1]), x = Math.round(funcs.grx(pnt.x)), y = Math.round(funcs.gry(pnt.y)); if (i === 0) dd = `M${x},${y}`; else { if ((x === lastx) && (y === lasty)) continue; if (i % 9 === 0) dd += `m${x-lastx},${y-lasty}`; else if (y === lasty) dd += `h${x-lastx}`; else if (x === lastx) dd += `v${y-lasty}`; else dd += `l${x-lastx},${y-lasty}`; } lastx = x; lasty = y; } this.draw_g .append('svg:path') .attr('d', dd) .style('fill', palette.calcColor(lvl, ilevels.length)); }); return handle; } /** @summary Draw histogram bins as contour */ drawBinsContour() { const handle = this.prepareDraw({ rounding: false, extra: 100 }), main = this.getFramePainter(), frame_w = main.getFrameWidth(), frame_h = main.getFrameHeight(), levels = this.getContourLevels(), palette = this.getHistPalette(), get_segm_intersection = (segm1, segm2) => { const s10_x = segm1.x2 - segm1.x1, s10_y = segm1.y2 - segm1.y1, s32_x = segm2.x2 - segm2.x1, s32_y = segm2.y2 - segm2.y1, denom = s10_x * s32_y - s32_x * s10_y; if (denom === 0) return 0; // Collinear const denomPositive = denom > 0, s02_x = segm1.x1 - segm2.x1, s02_y = segm1.y1 - segm2.y1, s_numer = s10_x * s02_y - s10_y * s02_x; if ((s_numer < 0) === denomPositive) return null; // No collision const t_numer = s32_x * s02_y - s32_y * s02_x; if ((t_numer < 0) === denomPositive) return null; // No collision if (((s_numer > denom) === denomPositive) || ((t_numer > denom) === denomPositive)) return null; // No collision // Collision detected const t = t_numer / denom; return { x: Math.round(segm1.x1 + (t * s10_x)), y: Math.round(segm1.y1 + (t * s10_y)) }; }, buildPath = (xp, yp, iminus, iplus, do_close, check_rapair) => { let cmd = '', lastx, lasty, x0, y0, isany = false, matched, x, y; for (let i = iminus; i <= iplus; ++i) { x = Math.round(xp[i]); y = Math.round(yp[i]); if (!cmd) { cmd = `M${x},${y}`; x0 = x; y0 = y; } else if ((i === iplus) && (iminus !== iplus) && (x === x0) && (y === y0)) { if (!isany) return ''; // all same points cmd += 'z'; do_close = false; matched = true; } else { const dx = x - lastx, dy = y - lasty; if (dx) { isany = true; cmd += dy ? `l${dx},${dy}` : `h${dx}`; } else if (dy) { isany = true; cmd += `v${dy}`; } } lastx = x; lasty = y; } if (!do_close || matched || !check_rapair) return do_close ? cmd + 'z' : cmd; // try to build path which fills area to outside borders const points = [{ x: 0, y: 0 }, { x: frame_w, y: 0 }, { x: frame_w, y: frame_h }, { x: 0, y: frame_h }], get_intersect = (indx, di) => { const segm = { x1: xp[indx], y1: yp[indx], x2: 2*xp[indx] - xp[indx+di], y2: 2*yp[indx] - yp[indx+di] }; for (let i = 0; i < 4; ++i) { const res = get_segm_intersection(segm, { x1: points[i].x, y1: points[i].y, x2: points[(i+1)%4].x, y2: points[(i+1)%4].y }); if (res) { res.indx = i + 0.5; return res; } } return null; }; let pnt1, pnt2; iminus--; while ((iminus < iplus - 1) && !pnt1) pnt1 = get_intersect(++iminus, 1); if (!pnt1) return ''; iplus++; while ((iminus < iplus - 1) && !pnt2) pnt2 = get_intersect(--iplus, -1); if (!pnt2) return ''; // TODO: now side is always same direction, could be that side should be checked more precise let dd = buildPath(xp, yp, iminus, iplus), indx = pnt2.indx; const side = 1, step = side*0.5; dd += `L${pnt2.x},${pnt2.y}`; while (Math.abs(indx - pnt1.indx) > 0.1) { indx = Math.round(indx + step) % 4; dd += `L${points[indx].x},${points[indx].y}`; indx += step; } return dd + `L${pnt1.x},${pnt1.y}z`; }; if (this.options.Contour === 14) { this.draw_g .append('svg:path') .attr('d', `M0,0h${frame_w}v${frame_h}h${-frame_w}z`) .style('fill', palette.calcColor(0, levels.length)); } buildHist2dContour(this.getHisto(), handle, levels, palette, (colindx, xp, yp, iminus, iplus, ipoly) => { const icol = palette.getColor(colindx); let fillcolor = icol, lineatt; switch (this.options.Contour) { case 1: break; case 11: fillcolor = 'none'; lineatt = this.createAttLine({ color: icol, std: false }); break; case 12: fillcolor = 'none'; lineatt = this.createAttLine({ color: 1, style: (ipoly%5 + 1), width: 1, std: false }); break; case 13: fillcolor = 'none'; lineatt = this.lineatt; break; case 14: break; } const dd = buildPath(xp, yp, iminus, iplus, fillcolor !== 'none', true); if (!dd) return; this.draw_g.append('svg:path') .attr('d', dd) .style('fill', fillcolor) .call(lineatt ? lineatt.func : () => {}); }); handle.hide_only_zeros = true; // text drawing suppress only zeros return handle; } getGrNPoints(gr) { const x = gr.fX, y = gr.fY; let npnts = gr.fNpoints; if ((npnts > 2) && (x[0] === x[npnts-1]) && (y[0] === y[npnts-1])) npnts--; return npnts; } /** @summary Create single graph path from TH2PolyBin */ createPolyGr(funcs, gr, textbin) { let grcmd = '', acc_x = 0, acc_y = 0; const x = gr.fX, y = gr.fY, flush = () => { if (acc_x) { grcmd += 'h' + acc_x; acc_x = 0; } if (acc_y) { grcmd += 'v' + acc_y; acc_y = 0; } }, addPoint = (x1, y1, x2, y2) => { const len = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2); textbin.sumx += (x1 + x2) * len / 2; textbin.sumy += (y1 + y2) * len / 2; textbin.sum += len; }, npnts = this.getGrNPoints(gr); if (npnts < 2) return ''; const grx0 = Math.round(funcs.grx(x[0])), gry0 = Math.round(funcs.gry(y[0])); let grx = grx0, gry = gry0; for (let n = 1; n < npnts; ++n) { const nextx = Math.round(funcs.grx(x[n])), nexty = Math.round(funcs.gry(y[n])), dx = nextx - grx, dy = nexty - gry; if (textbin) addPoint(grx, gry, nextx, nexty); if (dx || dy) { if (dx === 0) { if ((acc_y === 0) || ((dy < 0) !== (acc_y < 0))) flush(); acc_y += dy; } else if (dy === 0) { if ((acc_x === 0) || ((dx < 0) !== (acc_x < 0))) flush(); acc_x += dx; } else { flush(); grcmd += `l${dx},${dy}`; } grx = nextx; gry = nexty; } } if (textbin) addPoint(grx, gry, grx0, gry0); flush(); return grcmd ? `M${grx0},${gry0}` + grcmd + 'z' : ''; } /** @summary Create path for complete TH2PolyBin */ createPolyBin(funcs, bin) { const arr = (bin.fPoly._typename === clTMultiGraph) ? bin.fPoly.fGraphs.arr : [bin.fPoly]; let cmd = ''; for (let k = 0; k < arr.length; ++k) cmd += this.createPolyGr(funcs, arr[k]); return cmd; } /** @summary draw TH2Poly bins */ async drawPolyBins() { const histo = this.getObject(), fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), draw_colors = this.options.Color || (!this.options.Line && !this.options.Fill && !this.options.Text && !this.options.Mark), draw_lines = this.options.Line || (this.options.Text && !draw_colors), draw_fill = this.options.Fill && !draw_colors, draw_mark = this.options.Mark, h = fp.getFrameHeight(), textbins = [], len = histo.fBins.arr.length; let colindx, cmd, full_cmd = '', allmarkers_cmd = '', bin, item, i, gr0 = null, lineatt_match = draw_lines, fillatt_match = draw_fill, markatt_match = draw_mark; // force recalculations of contours // use global coordinates this.maxbin = this.gmaxbin; this.minbin = this.gminbin; this.minposbin = this.gminposbin; const cntr = draw_colors ? this.getContour(true) : null, palette = cntr ? this.getHistPalette() : null, rejectBin = bin2 => { // check if bin outside visible range return ((bin2.fXmin > funcs.scale_xmax) || (bin2.fXmax < funcs.scale_xmin) || (bin2.fYmin > funcs.scale_ymax) || (bin2.fYmax < funcs.scale_ymin)); }; // check if similar fill attributes for (i = 0; i < len; ++i) { bin = histo.fBins.arr[i]; if (rejectBin(bin)) continue; const arr = (bin.fPoly._typename === clTMultiGraph) ? bin.fPoly.fGraphs.arr : [bin.fPoly]; for (let k = 0; k < arr.length; ++k) { const gr = arr[k]; if (!gr0) { gr0 = gr; continue; } if (lineatt_match && ((gr0.fLineColor !== gr.fLineColor) || (gr0.fLineWidth !== gr.fLineWidth) || (gr0.fLineStyle !== gr.fLineStyle))) lineatt_match = false; if (fillatt_match && ((gr0.fFillColor !== gr.fFillColor) || (gr0.fFillStyle !== gr.fFillStyle))) fillatt_match = false; if (markatt_match && ((gr0.fMarkerColor !== gr.fMarkerColor) || (gr0.fMarkerStyle !== gr.fMarkerStyle) || (gr0.fMarkerSize !== gr.fMarkerSize))) markatt_match = false; } if (!lineatt_match && !fillatt_match && !markatt_match) break; } // do not try color draw optimization as with plain th2 while // bins are not rectangular and drawings artifacts are nasty // therefore draw each bin separately when doing color draw const lineatt0 = lineatt_match && gr0 ? this.createAttLine(gr0) : null, fillatt0 = fillatt_match && gr0 ? this.createAttFill(gr0) : null, markeratt0 = markatt_match && gr0 ? this.createAttMarker({ attr: gr0, style: this.options.MarkStyle, std: false }) : null, optimize_draw = !draw_colors && (draw_lines ? lineatt_match : true) && (draw_fill ? fillatt_match : true); // draw bins for (i = 0; i < len; ++i) { bin = histo.fBins.arr[i]; if (rejectBin(bin)) continue; const draw_bin = bin.fContent || this.options.Zero, arr = (bin.fPoly._typename === clTMultiGraph) ? bin.fPoly.fGraphs.arr : [bin.fPoly]; colindx = draw_colors && draw_bin ? cntr.getPaletteIndex(palette, bin.fContent) : null; const textbin = this.options.Text && draw_bin ? { bin, sumx: 0, sumy: 0, sum: 0 } : null; for (let k = 0; k < arr.length; ++k) { const gr = arr[k]; if (markeratt0) { const npnts = this.getGrNPoints(gr); for (let n = 0; n < npnts; ++n) allmarkers_cmd += markeratt0.create(funcs.grx(gr.fX[n]), funcs.gry(gr.fY[n])); } cmd = this.createPolyGr(funcs, gr, textbin); if (!cmd) continue; if (optimize_draw) full_cmd += cmd; else if ((colindx !== null) || draw_fill || draw_lines) { item = this.draw_g.append('svg:path').attr('d', cmd); if (draw_colors && (colindx !== null)) item.style('fill', this._color_palette.getColor(colindx)); else if (draw_fill) item.call(this.createAttFill(gr).func); else item.style('fill', 'none'); if (draw_lines) item.call(this.createAttLine(gr).func); } } // loop over graphs if (textbin?.sum) textbins.push(textbin); } // loop over bins if (optimize_draw) { item = this.draw_g.append('svg:path').attr('d', full_cmd); if (draw_fill && fillatt0) item.call(fillatt0.func); else item.style('fill', 'none'); if (draw_lines && lineatt0) item.call(lineatt0.func); } if (markeratt0 && !markeratt0.empty() && allmarkers_cmd) { this.draw_g.append('svg:path') .attr('d', allmarkers_cmd) .call(markeratt0.func); } else if (draw_mark) { for (i = 0; i < len; ++i) { bin = histo.fBins.arr[i]; if (rejectBin(bin)) continue; const arr = (bin.fPoly._typename === clTMultiGraph) ? bin.fPoly.fGraphs.arr : [bin.fPoly]; for (let k = 0; k < arr.length; ++k) { const gr = arr[k], npnts = this.getGrNPoints(gr), markeratt = this.createAttMarker({ attr: gr, style: this.options.MarkStyle, std: false }); if (!npnts || markeratt.empty()) continue; let cmdm = ''; for (let n = 0; n < npnts; ++n) cmdm += markeratt.create(funcs.grx(gr.fX[n]), funcs.gry(gr.fY[n])); this.draw_g.append('svg:path') .attr('d', cmdm) .call(markeratt.func); } // loop over graphs } // loop over bins } let pr = Promise.resolve(); if (textbins.length > 0) { const color = this.getColor(histo.fMarkerColor), rotate = -1*this.options.TextAngle, text_g = this.draw_g.append('svg:g').attr('class', 'th2poly_text'), text_size = ((histo.fMarkerSize !== 1) && rotate) ? Math.round(0.02*h*histo.fMarkerSize) : 12; pr = this.startTextDrawingAsync(42, text_size, text_g, text_size).then(() => { for (i = 0; i < textbins.length; ++i) { const textbin = textbins[i]; bin = textbin.bin; if (textbin.sum > 0) { textbin.midx = Math.round(textbin.sumx / textbin.sum); textbin.midy = Math.round(textbin.sumy / textbin.sum); } else { textbin.midx = Math.round(funcs.grx((bin.fXmin + bin.fXmax)/2)); textbin.midy = Math.round(funcs.gry((bin.fYmin + bin.fYmax)/2)); } let text; if (!this.options.TextKind) text = (Math.round(bin.fContent) === bin.fContent) ? bin.fContent.toString() : floatToString(bin.fContent, gStyle.fPaintTextFormat); else { text = bin.fPoly?.fName; if (!text || (text === 'Graph')) text = bin.fNumber.toString(); } this.drawText({ align: 22, x: textbin.midx, y: textbin.midy, rotate, text, color, latex: 0, draw_g: text_g }); } return this.finishTextDrawing(text_g, true); }); } return pr.then(() => { return { poly: true }; }); } /** @summary Draw TH2 bins as text */ async drawBinsText(handle) { const histo = this.getObject(), test_cutg = this.options.cutg, color = this.getColor(histo.fMarkerColor), rotate = -1*this.options.TextAngle, draw_g = this.draw_g.append('svg:g').attr('class', 'th2_text'), show_err = (this.options.TextKind === 'E'), latex = (show_err && !this.options.TextLine) ? 1 : 0, text_offset = histo.fBarOffset*1e-3, text_size = ((histo.fMarkerSize === 1) || !rotate) ? 20 : Math.round(0.02*histo.fMarkerSize*this.getFramePainter().getFrameHeight()); if (!handle) handle = this.prepareDraw({ rounding: false }); return this.startTextDrawingAsync(42, text_size, draw_g, text_size).then(() => { for (let i = handle.i1; i < handle.i2; ++i) { const binw = handle.grx[i+1] - handle.grx[i]; for (let j = handle.j1; j < handle.j2; ++j) { const binz = histo.getBinContent(i + 1, j + 1); if ((binz === 0) && !this.options.ShowEmpty) continue; if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5))) continue; const binh = handle.gry[j] - handle.gry[j+1]; let text = (binz === Math.round(binz)) ? binz.toString() : floatToString(binz, gStyle.fPaintTextFormat); if (show_err) { const errs = this.getBinErrors(histo, histo.getBin(i + 1, j + 1), binz); if (errs.poisson) { const lble = `-${floatToString(errs.low, gStyle.fPaintTextFormat)} +${floatToString(errs.up, gStyle.fPaintTextFormat)}`; if (this.options.TextLine) text += ' ' + lble; else text = `#splitmline{${text}}{${lble}}`; } else { const lble = (errs.up === Math.round(errs.up)) ? errs.up.toString() : floatToString(errs.up, gStyle.fPaintTextFormat); if (this.options.TextLine) text += '\xB1' + lble; else text = `#splitmline{${text}}{#pm${lble}}`; } } let x, y, width, height; if (rotate) { x = Math.round(handle.grx[i] + binw*0.5); y = Math.round(handle.gry[j+1] + binh*(0.5 + text_offset)); width = height = 0; } else { x = Math.round(handle.grx[i] + binw*0.1); y = Math.round(handle.gry[j+1] + binh*(0.1 + text_offset)); width = Math.round(binw*0.8); height = Math.round(binh*0.8); } this.drawText({ align: 22, x, y, width, height, rotate, text, color, latex, draw_g }); } } handle.hide_only_zeros = true; // text drawing suppress only zeros return this.finishTextDrawing(draw_g, true); }).then(() => handle); } /** @summary Draw TH2 bins as arrows */ drawBinsArrow() { const histo = this.getObject(), test_cutg = this.options.cutg, handle = this.prepareDraw({ rounding: false }), cntr = this.options.Color ? this.getContour() : null, palette = this.options.Color ? this.getHistPalette() : null, scale_x = (handle.grx[handle.i2] - handle.grx[handle.i1])/(handle.i2 - handle.i1 + 1)/2, scale_y = (handle.gry[handle.j2] - handle.gry[handle.j1])/(handle.j2 - handle.j1 + 1)/2, makeLine = (dx, dy) => dx ? (dy ? `l${dx},${dy}` : `h${dx}`) : (dy ? `v${dy}` : ''), entries = []; let dn = 1e-30, dx, dy, xc, yc, plain = '', dxn, dyn, x1, x2, y1, y2; for (let loop = 0; loop < 2; ++loop) { for (let i = handle.i1; i < handle.i2; ++i) { for (let j = handle.j1; j < handle.j2; ++j) { if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5))) continue; const bincont = histo.getBinContent(i+1, j+1); if (i === handle.i1) dx = histo.getBinContent(i+2, j+1) - bincont; else if (i === handle.i2-1) dx = bincont - histo.getBinContent(i, j+1); else dx = 0.5*(histo.getBinContent(i+2, j+1) - histo.getBinContent(i, j+1)); if (j === handle.j1) dy = histo.getBinContent(i+1, j+2) - bincont; else if (j === handle.j2-1) dy = bincont - histo.getBinContent(i+1, j); else dy = 0.5*(histo.getBinContent(i+1, j+2) - histo.getBinContent(i+1, j)); if (loop === 0) dn = Math.max(dn, Math.abs(dx), Math.abs(dy)); else { xc = (handle.grx[i] + handle.grx[i+1])/2; yc = (handle.gry[j] + handle.gry[j+1])/2; dxn = scale_x*dx/dn; dyn = scale_y*dy/dn; x1 = xc - dxn; x2 = xc + dxn; y1 = yc - dyn; y2 = yc + dyn; dx = Math.round(x2-x1); dy = Math.round(y2-y1); if (dx || dy) { let cmd = `M${Math.round(x1)},${Math.round(y1)}${makeLine(dx, dy)}`; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { const anr = Math.sqrt(9/(dx**2 + dy**2)), si = Math.round(anr*(dx + dy)), co = Math.round(anr*(dx - dy)); if (si || co) cmd += `m${-si},${co}${makeLine(si, -co)}${makeLine(-co, -si)}`; } if (palette && cntr) { const colindx = cntr.getPaletteIndex(palette, bincont); if (colindx !== null) { const entry = entries[colindx]; if (!entry) entries[colindx] = { path: cmd }; else entry.path += cmd; } } else plain += cmd; } } } } } if (plain) { this.draw_g .append('svg:path') .attr('d', plain) .style('fill', 'none') .call(this.lineatt.func); } entries.forEach((entry, colindx) => { if (entry) { const col0 = this.lineatt.color; this.lineatt.color = palette.getColor(colindx); this.draw_g.append('svg:path') .attr('fill', 'none') .attr('d', entry.path) .call(this.lineatt.func); this.lineatt.color = col0; } }); return handle; } /** @summary Draw TH2 bins as boxes */ drawBinsBox() { const histo = this.getObject(), handle = this.prepareDraw({ rounding: false }), main = this.getMainPainter(); if (main === this) { if (main.maxbin === main.minbin) { main.maxbin = main.gmaxbin; main.minbin = main.gminbin; main.minposbin = main.gminposbin; } if (main.maxbin === main.minbin) main.minbin = Math.min(0, main.maxbin-1); } const absmax = Math.max(Math.abs(main.maxbin), Math.abs(main.minbin)), absmin = Math.max(0, main.minbin), pad = this.getPadPainter().getRootPad(true), test_cutg = this.options.cutg; let i, j, binz, absz, res = '', cross = '', btn1 = '', btn2 = '', zdiff, dgrx, dgry, xx, yy, ww, hh, xyfactor, uselogz = false, logmin = 0; if ((pad?.fLogv ?? pad?.fLogz) && (absmax > 0)) { uselogz = true; const logmax = Math.log(absmax); if (absmin > 0) logmin = Math.log(absmin); else if ((main.minposbin >= 1) && (main.minposbin < 100)) logmin = Math.log(0.7); else logmin = (main.minposbin > 0) ? Math.log(0.7*main.minposbin) : logmax - 10; if (logmin >= logmax) logmin = logmax - 10; xyfactor = 1.0 / (logmax - logmin); } else xyfactor = 1.0 / (absmax - absmin); // now start build for (i = handle.i1; i < handle.i2; ++i) { for (j = handle.j1; j < handle.j2; ++j) { binz = histo.getBinContent(i + 1, j + 1); absz = Math.abs(binz); if ((absz === 0) || (absz < absmin)) continue; if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5))) continue; zdiff = uselogz ? ((absz > 0) ? Math.log(absz) - logmin : 0) : (absz - absmin); // area of the box should be proportional to absolute bin content zdiff = 0.5 * ((zdiff < 0) ? 1 : (1 - Math.sqrt(zdiff * xyfactor))); // avoid oversized bins if (zdiff < 0) zdiff = 0; ww = handle.grx[i+1] - handle.grx[i]; hh = handle.gry[j] - handle.gry[j+1]; dgrx = zdiff * ww; dgry = zdiff * hh; xx = Math.round(handle.grx[i] + dgrx); yy = Math.round(handle.gry[j+1] + dgry); ww = Math.max(Math.round(ww - 2*dgrx), 1); hh = Math.max(Math.round(hh - 2*dgry), 1); res += `M${xx},${yy}v${hh}h${ww}v${-hh}z`; if ((binz < 0) && (this.options.BoxStyle === 10)) cross += `M${xx},${yy}l${ww},${hh}m0,${-hh}l${-ww},${hh}`; if ((this.options.BoxStyle === 11) && (ww > 5) && (hh > 5)) { const arr = getBoxDecorations(xx, yy, ww, hh, binz < 0 ? -1 : 1, Math.round(ww*0.1), Math.round(hh*0.1)); btn1 += arr[0]; btn2 += arr[1]; } } } if (res) { const elem = this.draw_g.append('svg:path') .attr('d', res) .call(this.fillatt.func); if ((this.options.BoxStyle !== 11) && this.fillatt.empty()) elem.call(this.lineatt.func); } if (btn1 && this.fillatt.hasColor()) { this.draw_g.append('svg:path') .attr('d', btn1) .call(this.fillatt.func) .style('fill', d3_rgb(this.fillatt.color).brighter(0.5).formatRgb()); } if (btn2) { this.draw_g.append('svg:path') .attr('d', btn2) .call(this.fillatt.func) .style('fill', !this.fillatt.hasColor() ? 'red' : d3_rgb(this.fillatt.color).darker(0.5).formatRgb()); } if (cross) { const elem = this.draw_g.append('svg:path') .attr('d', cross) .style('fill', 'none'); if (!this.lineatt.empty()) elem.call(this.lineatt.func); else elem.style('stroke', 'black'); } return handle; } /** @summary Draw histogram bins as candle plot */ drawBinsCandle() { const kNoOption = 0, kBox = 1, kMedianLine = 10, kMedianNotched = 20, kMedianCircle = 30, kMeanLine = 100, kMeanCircle = 300, kWhiskerAll = 1000, kWhisker15 = 2000, kAnchor = 10000, kPointsOutliers = 100000, kPointsAll = 200000, kPointsAllScat = 300000, kHistoLeft = 1000000, kHistoRight = 2000000, kHistoViolin = 3000000, kHistoZeroIndicator = 10000000, kHorizontal = 100000000, fallbackCandle = kBox + kMedianLine + kMeanCircle + kWhiskerAll + kAnchor, fallbackViolin = kMeanCircle + kWhiskerAll + kHistoViolin + kHistoZeroIndicator; let fOption = kNoOption; const isOption = opt => { let mult = 1; while (opt >= mult) mult *= 10; mult /= 10; return Math.floor(fOption/mult) % 10 === Math.floor(opt/mult); }, parseOption = (opt, is_candle) => { let direction = '', preset = '', res = kNoOption; const c0 = opt[0], c1 = opt[1]; if (c0 >= 'A' && c0 <= 'Z') direction = c0; if (c0 >= '1' && c0 <= '9') preset = c0; if (c1 >= 'A' && c1 <= 'Z' && preset) direction = c1; if (c1 >= '1' && c1 <= '9' && direction) preset = c1; if (is_candle) { switch (preset) { case '1': res += fallbackCandle; break; case '2': res += kBox + kMeanLine + kMedianLine + kWhisker15 + kAnchor + kPointsOutliers; break; case '3': res += kBox + kMeanCircle + kMedianLine + kWhisker15 + kAnchor + kPointsOutliers; break; case '4': res += kBox + kMeanCircle + kMedianNotched + kWhisker15 + kAnchor + kPointsOutliers; break; case '5': res += kBox + kMeanLine + kMedianLine + kWhisker15 + kAnchor + kPointsAll; break; case '6': res += kBox + kMeanCircle + kMedianLine + kWhisker15 + kAnchor + kPointsAllScat; break; default: res += fallbackCandle; } } else { switch (preset) { case '1': res += fallbackViolin; break; case '2': res += kMeanCircle + kWhisker15 + kHistoViolin + kHistoZeroIndicator + kPointsOutliers; break; default: res += fallbackViolin; } } const l = opt.indexOf('('), r = opt.lastIndexOf(')'); if ((l >= 0) && (r > l+1)) res = parseInt(opt.slice(l+1, r)); fOption = res; if ((direction === 'Y' || direction === 'H') && !isOption(kHorizontal)) fOption += kHorizontal; }, extractQuantiles = (xx, proj, prob) => { let integral = 0, cnt = 0, sum1 = 0; const res = { max: 0, first: -1, last: -1, entries: 0 }; for (let j = 0; j < proj.length; ++j) { if (proj[j] > 0) { res.max = Math.max(res.max, proj[j]); if (res.first < 0) res.first = j; res.last = j; } integral += proj[j]; sum1 += proj[j]*(xx[j]+xx[j+1])/2; } if (integral <= 0) return null; res.entries = integral; res.mean = sum1/integral; res.quantiles = new Array(prob.length); res.indx = new Array(prob.length); for (let j = 0, sum = 0, nextv = 0; j < proj.length; ++j) { const v = nextv; let x = xx[j]; // special case - flat integral with const value if ((v === prob[cnt]) && (proj[j] === 0) && (v < 0.99)) { while ((proj[j] === 0) && (j < proj.length)) j++; x = (xx[j] + x) / 2; // this will be mid value } sum += proj[j]; nextv = sum / integral; while ((prob[cnt] >= v) && (prob[cnt] < nextv)) { res.indx[cnt] = j; res.quantiles[cnt] = x + ((prob[cnt] - v) / (nextv - v)) * (xx[j + 1] - x); if (cnt++ === prob.length) return res; x = xx[j]; } } while (cnt < prob.length) { res.indx[cnt] = proj.length - 1; res.quantiles[cnt++] = xx.at(-1); } return res; }; if (this.options.Candle) parseOption(this.options.Candle, true); else if (this.options.Violin) parseOption(this.options.Violin, false); const histo = this.getHisto(), handle = this.prepareDraw(), fp = this.getFramePainter(), // used for axis values conversions cp = this.getCanvPainter(), funcs = this.getHistGrFuncs(fp), swapXY = isOption(kHorizontal); let scaledViolin = gStyle.fViolinScaled, scaledCandle = gStyle.fCandleScaled, maxContent = 0, markers = '', cmarkers = '', attrcmarkers = null; if (this.options.Scaled !== null) scaledViolin = scaledCandle = this.options.Scaled; else if (cp?.online_canvas) { // console.log('ignore hist title in online canvas'); } else if (histo.fTitle.indexOf('unscaled') >= 0) scaledViolin = scaledCandle = false; else if (histo.fTitle.indexOf('scaled') >= 0) scaledViolin = scaledCandle = true; if (scaledViolin && (isOption(kHistoRight) || isOption(kHistoLeft) || isOption(kHistoViolin))) { for (let i = 0; i < this.nbinsx; ++i) { for (let j = 0; j < this.nbinsy; ++j) maxContent = Math.max(maxContent, histo.getBinContent(i + 1, j + 1)); } } const make_path = (...a) => { if (a[1] === 'array') a = a[0]; const l = a.length; let i = 2, xx = a[0], yy = a[1], res = swapXY ? `M${yy},${xx}` : `M${xx},${yy}`; while (i < l) { switch (a[i]) { case 'Z': return res + 'z'; case 'V': if (yy !== a[i+1]) { res += (swapXY ? 'h' : 'v') + (a[i+1] - yy); yy = a[i+1]; } break; case 'H': if (xx !== a[i+1]) { res += (swapXY ? 'v' : 'h') + (a[i+1] - xx); xx = a[i+1]; } break; default: res += swapXY ? `l${a[i+1]-yy},${a[i]-xx}` : `l${a[i]-xx},${a[i+1]-yy}`; xx = a[i]; yy = a[i+1]; } i += 2; } return res; }, make_marker = (x, y) => { if (!markers) { const mw = gStyle.fCandleCrossLineWidth ?? 1; this.createAttMarker({ attr: histo, style: isOption(kPointsAllScat) ? 0 : (mw === 1 ? 5 : 18 * mw + 16) }); this.markeratt.resetPos(); } markers += swapXY ? this.markeratt.create(y, x) : this.markeratt.create(x, y); }, make_cmarker = (x, y) => { if (!attrcmarkers) { const mw = gStyle.fCandleCircleLineWidth ?? 1; attrcmarkers = this.createAttMarker({ attr: histo, style: (mw === 1 ? 24 : 18 * mw + 17), std: false }); attrcmarkers.resetPos(); } cmarkers += swapXY ? attrcmarkers.create(y, x) : attrcmarkers.create(x, y); }; if (histo.fMarkerColor === 1) histo.fMarkerColor = histo.fLineColor; handle.candle = []; // array of drawn points let xx, bars = '', lines = '', dashed_lines = '', hists = '', hlines = '', proj, maxIntegral = 0; // Determining the quintiles const wRange = gStyle.fCandleWhiskerRange, bRange = gStyle.fCandleBoxRange, prob = [(wRange >= 1) ? 1e-15 : 0.5 - wRange/2.0, (bRange >= 1) ? 1E-14 : 0.5 - bRange/2.0, 0.5, (bRange >= 1) ? 1-1E-14 : 0.5 + bRange/2.0, (wRange >= 1) ? 1-1e-15 : 0.5 + wRange/2.0], produceCandlePoint = (bin_indx, grx_left, grx_right, xindx1, xindx2) => { const res = extractQuantiles(xx, proj, prob); if (!res) return; const pnt = { bin: bin_indx, swapXY, fBoxDown: res.quantiles[1], fMedian: res.quantiles[2], fBoxUp: res.quantiles[3] }, iqr = pnt.fBoxUp - pnt.fBoxDown; let fWhiskerDown = res.quantiles[0], fWhiskerUp = res.quantiles[4]; if (isOption(kWhisker15)) { // Improved whisker definition, with 1.5*iqr let pos = pnt.fBoxDown-1.5*iqr, indx = res.indx[1]; while ((xx[indx] > pos) && (indx > 0)) indx--; while (!proj[indx]) indx++; fWhiskerDown = xx[indx]; // use lower edge here pos = pnt.fBoxUp+1.5*iqr; indx = res.indx[3]; while ((xx[indx] < pos) && (indx < proj.length)) indx++; while (!proj[indx]) indx--; fWhiskerUp = xx[indx+1]; // use upper index edge here } const fMean = res.mean, fMedianErr = 1.57*iqr/Math.sqrt(res.entries); // estimate quantiles... simple function... not so nice as GetQuantiles // exclude points with negative y when log scale is specified if (fWhiskerDown <= 0) if ((swapXY && funcs.logx) || (!swapXY && funcs.logy)) return; const w = (grx_right - grx_left); let candleWidth, histoWidth, center = (grx_left + grx_right) / 2 + histo.fBarOffset/1000*w; if ((histo.fBarWidth > 0) && (histo.fBarWidth !== 1000)) candleWidth = histoWidth = w * histo.fBarWidth / 1000; else { candleWidth = w*0.66; histoWidth = w*0.8; } if (scaledViolin && (maxContent > 0)) histoWidth *= res.max/maxContent; if (scaledCandle && (maxIntegral > 0)) candleWidth *= res.entries/maxIntegral; pnt.x1 = Math.round(center - candleWidth/2); pnt.x2 = Math.round(center + candleWidth/2); center = Math.round(center); const x1d = Math.round(center - candleWidth/3), x2d = Math.round(center + candleWidth/3), ff = swapXY ? funcs.grx : funcs.gry; pnt.yy1 = Math.round(ff(fWhiskerUp)); pnt.y1 = Math.round(ff(pnt.fBoxUp)); pnt.y0 = Math.round(ff(pnt.fMedian)); pnt.y2 = Math.round(ff(pnt.fBoxDown)); pnt.yy2 = Math.round(ff(fWhiskerDown)); const y0m = Math.round(ff(fMean)), y01 = Math.round(ff(pnt.fMedian + fMedianErr)), y02 = Math.round(ff(pnt.fMedian - fMedianErr)); if (isOption(kHistoZeroIndicator)) hlines += make_path(center, Math.round(ff(xx[xindx1])), 'V', Math.round(ff(xx[xindx2]))); if (isOption(kMedianLine)) lines += make_path(pnt.x1, pnt.y0, 'H', pnt.x2); else if (isOption(kMedianNotched)) lines += make_path(x1d, pnt.y0, 'H', x2d); else if (isOption(kMedianCircle)) make_cmarker(center, pnt.y0); if (isOption(kMeanCircle)) make_cmarker(center, y0m); else if (isOption(kMeanLine)) dashed_lines += make_path(pnt.x1, y0m, 'H', pnt.x2); if (isOption(kBox)) { if (isOption(kMedianNotched)) bars += make_path(pnt.x1, pnt.y1, 'V', y01, x1d, pnt.y0, pnt.x1, y02, 'V', pnt.y2, 'H', pnt.x2, 'V', y02, x2d, pnt.y0, pnt.x2, y01, 'V', pnt.y1, 'Z'); else bars += make_path(pnt.x1, pnt.y1, 'V', pnt.y2, 'H', pnt.x2, 'V', pnt.y1, 'Z'); } if (isOption(kAnchor)) // Draw the anchor line lines += make_path(pnt.x1, pnt.yy1, 'H', pnt.x2) + make_path(pnt.x1, pnt.yy2, 'H', pnt.x2); if (isOption(kWhiskerAll) && !isOption(kHistoZeroIndicator)) { // Whiskers are dashed dashed_lines += make_path(center, pnt.y1, 'V', pnt.yy1) + make_path(center, pnt.y2, 'V', pnt.yy2); } else if ((isOption(kWhiskerAll) && isOption(kHistoZeroIndicator)) || isOption(kWhisker15)) lines += make_path(center, pnt.y1, 'V', pnt.yy1) + make_path(center, pnt.y2, 'V', pnt.yy2); if (isOption(kPointsOutliers) || isOption(kPointsAll) || isOption(kPointsAllScat)) { // reset seed for each projection to have always same pixels const rnd = new TRandom(bin_indx*7521 + Math.round(res.integral)), show_all = !isOption(kPointsOutliers), show_scat = isOption(kPointsAllScat); for (let ii = 0; ii < proj.length; ++ii) { const bin_content = proj[ii], binx = (xx[ii] + xx[ii+1])/2; let marker_x = center, marker_y; if (!bin_content) continue; if (!show_all && (binx >= fWhiskerDown) && (binx <= fWhiskerUp)) continue; for (let k = 0; k < bin_content; k++) { if (show_scat) marker_x = center + Math.round(((rnd.random() - 0.5) * candleWidth)); if ((bin_content === 1) && !show_scat) marker_y = Math.round(ff(binx)); else marker_y = Math.round(ff(xx[ii] + rnd.random()*(xx[ii+1]-xx[ii]))); make_marker(marker_x, marker_y); } } } if ((isOption(kHistoRight) || isOption(kHistoLeft) || isOption(kHistoViolin)) && (res.max > 0) && (res.first >= 0)) { const arr = [], scale = (swapXY ? -0.5 : 0.5) * histoWidth / res.max; xindx1 = Math.max(xindx1, res.first); xindx2 = Math.min(xindx2-1, res.last); if (isOption(kHistoRight) || isOption(kHistoViolin)) { let prev_x = center, prev_y = Math.round(ff(xx[xindx1])); arr.push(prev_x, prev_y); for (let ii = xindx1; ii <= xindx2; ii++) { const curr_x = Math.round(center + scale*proj[ii]), curr_y = Math.round(ff(xx[ii+1])); if (curr_x !== prev_x) { if (ii !== xindx1) arr.push('V', prev_y); arr.push('H', curr_x); } prev_x = curr_x; prev_y = curr_y; } arr.push('V', prev_y); } if (isOption(kHistoLeft) || isOption(kHistoViolin)) { let prev_x = center, prev_y = Math.round(ff(xx[xindx2+1])); if (arr.length === 0) arr.push(prev_x, prev_y); for (let ii = xindx2; ii >= xindx1; ii--) { const curr_x = Math.round(center - scale*proj[ii]), curr_y = Math.round(ff(xx[ii])); if (curr_x !== prev_x) { if (ii !== xindx2) arr.push('V', prev_y); arr.push('H', curr_x); } prev_x = curr_x; prev_y = curr_y; } arr.push('V', prev_y); } arr.push('H', center); // complete histogram hists += make_path(arr, 'array'); if (!this.fillatt.empty()) hists += 'Z'; } handle.candle.push(pnt); // keep point for the tooltip }; if (swapXY) { xx = new Array(this.nbinsx+1); proj = new Array(this.nbinsx); for (let i = 0; i < this.nbinsx+1; ++i) xx[i] = histo.fXaxis.GetBinLowEdge(i+1); if (scaledCandle) { for (let j = 0; j < this.nbinsy; ++j) { let sum = 0; for (let i = 0; i < this.nbinsx; ++i) sum += histo.getBinContent(i+1, j+1); maxIntegral = Math.max(maxIntegral, sum); } } for (let j = handle.j1; j < handle.j2; ++j) { for (let i = 0; i < this.nbinsx; ++i) proj[i] = histo.getBinContent(i+1, j+1); produceCandlePoint(j, handle.gry[j+1], handle.gry[j], handle.i1, handle.i2); } } else { xx = new Array(this.nbinsy+1); proj = new Array(this.nbinsy); for (let j = 0; j < this.nbinsy+1; ++j) xx[j] = histo.fYaxis.GetBinLowEdge(j+1); if (scaledCandle) { for (let i = 0; i < this.nbinsx; ++i) { let sum = 0; for (let j = 0; j < this.nbinsy; ++j) sum += histo.getBinContent(i+1, j+1); maxIntegral = Math.max(maxIntegral, sum); } } // loop over visible x-bins for (let i = handle.i1; i < handle.i2; ++i) { for (let j = 0; j < this.nbinsy; ++j) proj[j] = histo.getBinContent(i+1, j+1); produceCandlePoint(i, handle.grx[i], handle.grx[i+1], handle.j1, handle.j2); } } if (hlines && (histo.fFillColor > 0)) { this.draw_g.append('svg:path') .attr('d', hlines) .style('stroke', this.getColor(histo.fFillColor)); } const hline_color = (isOption(kHistoZeroIndicator) && (histo.fFillStyle !== 0)) ? this.fillatt.color : this.lineatt.color; if (hists && (!this.fillatt.empty() || (hline_color !== 'none'))) { this.draw_g.append('svg:path') .attr('d', hists) .style('stroke', (hline_color !== 'none') ? hline_color : null) .style('pointer-events', this.isBatchMode() ? null : 'visibleFill') .call(this.fillatt.func); } if (bars) { this.draw_g.append('svg:path') .attr('d', bars) .call(this.lineatt.func) .call(this.fillatt.func); } if (lines) { this.draw_g.append('svg:path') .attr('d', lines) .call(this.lineatt.func) .style('fill', 'none'); } if (dashed_lines) { const dashed = this.createAttLine({ attr: histo, style: 2, std: false, color: kBlack }); this.draw_g.append('svg:path') .attr('d', dashed_lines) .call(dashed.func) .style('fill', 'none'); } if (cmarkers) { this.draw_g.append('svg:path') .attr('d', cmarkers) .call(attrcmarkers.func); } if (markers) { this.draw_g.append('svg:path') .attr('d', markers) .call(this.markeratt.func); } return handle; } /** @summary Draw TH2 bins as scatter plot */ drawBinsScatter() { const histo = this.getObject(), handle = this.prepareDraw({ rounding: true, pixel_density: true }), test_cutg = this.options.cutg, colPaths = [], currx = [], curry = [], cell_w = [], cell_h = [], scale = this.options.ScatCoef * ((this.gmaxbin) > 2000 ? 2000 / this.gmaxbin : 1), rnd = new TRandom(handle.sumz); let colindx, cmd1, cmd2, i, j, binz, cw, ch, factor = 1.0; handle.ScatterPlot = true; if (scale*handle.sumz < 1e5) { // one can use direct drawing of scatter plot without any patterns this.createAttMarker({ attr: histo }); this.markeratt.resetPos(); let path = ''; for (i = handle.i1; i < handle.i2; ++i) { cw = handle.grx[i+1] - handle.grx[i]; for (j = handle.j1; j < handle.j2; ++j) { ch = handle.gry[j] - handle.gry[j+1]; binz = histo.getBinContent(i + 1, j + 1); const npix = Math.round(scale*binz); if (npix <= 0) continue; if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5))) continue; for (let k = 0; k < npix; ++k) { path += this.markeratt.create( Math.round(handle.grx[i] + cw * rnd.random()), Math.round(handle.gry[j+1] + ch * rnd.random())); } } } this.draw_g .append('svg:path') .attr('d', path) .call(this.markeratt.func); return handle; } // limit filling factor, do not try to produce as many points as filled area; if (this.maxbin > 0.7) factor = 0.7/this.maxbin; const nlevels = Math.round(handle.max - handle.min), cntr = this.createContour((nlevels > 50) ? 50 : nlevels, this.minposbin, this.maxbin, this.minposbin); // now start build for (i = handle.i1; i < handle.i2; ++i) { for (j = handle.j1; j < handle.j2; ++j) { binz = histo.getBinContent(i + 1, j + 1); if ((binz <= 0) || (binz < this.minbin)) continue; cw = handle.grx[i+1] - handle.grx[i]; ch = handle.gry[j] - handle.gry[j+1]; if (cw*ch <= 0) continue; colindx = cntr.getContourIndex(binz/cw/ch); if (colindx < 0) continue; if (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5))) continue; cmd1 = `M${handle.grx[i]},${handle.gry[j+1]}`; if (colPaths[colindx] === undefined) { colPaths[colindx] = cmd1; cell_w[colindx] = cw; cell_h[colindx] = ch; } else { cmd2 = `m${handle.grx[i]-currx[colindx]},${handle.gry[j+1]-curry[colindx]}`; colPaths[colindx] += (cmd2.length < cmd1.length) ? cmd2 : cmd1; cell_w[colindx] = Math.max(cell_w[colindx], cw); cell_h[colindx] = Math.max(cell_h[colindx], ch); } currx[colindx] = handle.grx[i]; curry[colindx] = handle.gry[j+1]; colPaths[colindx] += `v${ch}h${cw}v${-ch}z`; } } const layer = this.getFrameSvg().selectChild('.main_layer'); let defs = layer.selectChild('defs'); if (defs.empty() && (colPaths.length > 0)) defs = layer.insert('svg:defs', ':first-child'); this.createAttMarker({ attr: histo }); for (colindx = 0; colindx < colPaths.length; ++colindx) { if ((colPaths[colindx] !== undefined) && (colindx < cntr.arr.length)) { const pattern_id = (this.pad_name || 'canv') + `_scatter_${colindx}`; let pattern = defs.selectChild(`#${pattern_id}`); if (pattern.empty()) { pattern = defs.append('svg:pattern') .attr('id', pattern_id) .attr('patternUnits', 'userSpaceOnUse'); } else pattern.selectAll('*').remove(); let npix = Math.round(factor*cntr.arr[colindx]*cell_w[colindx]*cell_h[colindx]); if (npix < 1) npix = 1; const arrx = new Float32Array(npix), arry = new Float32Array(npix); if (npix === 1) arrx[0] = arry[0] = 0.5; else { for (let n = 0; n < npix; ++n) { arrx[n] = rnd.random(); arry[n] = rnd.random(); } } this.markeratt.resetPos(); let path = ''; for (let n = 0; n < npix; ++n) path += this.markeratt.create(arrx[n] * cell_w[colindx], arry[n] * cell_h[colindx]); pattern.attr('width', cell_w[colindx]) .attr('height', cell_h[colindx]) .append('svg:path') .attr('d', path) .call(this.markeratt.func); this.draw_g .append('svg:path') .attr('scatter-index', colindx) .style('fill', `url(#${pattern_id})`) .attr('d', colPaths[colindx]); } } return handle; } /** @summary Draw TH2 bins in 2D mode */ draw2DBins() { if (this._hide_frame && this.isMainPainter()) { this.getFrameSvg().style('display', null); delete this._hide_frame; } else if (this.options.Same && this._ignore_frame) this.getFrameSvg().style('display', 'none'); if (!this.draw_content) { if (this.options.Zscale && this.options.ohmin && this.options.ohmax) { this.getContour(true); this.getHistPalette(); } return this.removeG(); } this.createHistDrawAttributes(); this.createG(!this._ignore_frame); let handle, pr; if (this.isTH2Poly()) pr = this.drawPolyBins(); else { if (this.options.Scat) handle = this.drawBinsScatter(); if (this.options.System === kPOLAR) handle = this.drawBinsPolar(); else if (this.options.Arrow) handle = this.drawBinsArrow(); else if (this.options.Color) handle = this.drawBinsColor(); else if (this.options.Box) handle = this.drawBinsBox(); else if (this.options.Proj) handle = this.drawBinsProjected(); else if (this.options.Contour) handle = this.drawBinsContour(); else if (this.options.Candle || this.options.Violin) handle = this.drawBinsCandle(); if (this.options.Text) pr = this.drawBinsText(handle); if (!handle && !pr) handle = this.drawBinsColor(); } if (handle) this.tt_handle = handle; else if (pr) return pr.then(tt => { this.tt_handle = tt; }); } /** @summary Draw TH2 in circular mode */ async drawBinsCircular() { this.getFrameSvg().style('display', 'none'); this._hide_frame = true; const rect = this.getPadPainter().getFrameRect(), hist = this.getHisto(), palette = this.options.Circular > 10 ? this.getHistPalette() : null, text_size = 20, circle_size = 16, axis = hist.fXaxis, getBinLabel = indx => { if (axis.fLabels) { for (let i = 0; i < axis.fLabels.arr.length; ++i) { const tstr = axis.fLabels.arr[i]; if (tstr.fUniqueID === indx+1) return tstr.fString; } } return indx.toString(); }; this.createG(); makeTranslate(this.draw_g, Math.round(rect.x + rect.width/2), Math.round(rect.y + rect.height/2)); const nbins = Math.min(this.nbinsx, this.nbinsy); return this.startTextDrawingAsync(42, text_size, this.draw_g).then(() => { const pnts = []; for (let n = 0; n < nbins; n++) { const a = (0.5 - n/nbins)*Math.PI*2, cx = Math.round((0.9*rect.width/2 - 2*circle_size) * Math.cos(a)), cy = Math.round((0.9*rect.height/2 - 2*circle_size) * Math.sin(a)), x = Math.round(0.9*rect.width/2 * Math.cos(a)), y = Math.round(0.9*rect.height/2 * Math.sin(a)), color = palette?.calcColor(n, nbins) ?? 'black'; let rotate = Math.round(a/Math.PI*180), align = 12; pnts.push({ x: cx, y: cy, a, color }); // remember points coordinates if ((rotate < -90) || (rotate > 90)) { rotate += 180; align = 32; } const s2 = Math.round(text_size/2), s1 = 2*s2; this.draw_g.append('path') .attr('d', `M${cx-s2},${cy} a${s2},${s2},0,1,0,${s1},0a${s2},${s2},0,1,0,${-s1},0z`) .style('stroke', color) .style('fill', 'none'); this.drawText({ align, rotate, x, y, text: getBinLabel(n) }); } const max_width = circle_size/2; let max_value = 0, min_value = 0; if (this.options.Circular > 11) { for (let i = 0; i < nbins - 1; ++i) { for (let j = i+1; j < nbins; ++j) { const cont = hist.getBinContent(i+1, j+1); if (cont > 0) { max_value = Math.max(max_value, cont); if (!min_value || (cont < min_value)) min_value = cont; } } } } for (let i = 0; i < nbins-1; ++i) { const pi = pnts[i]; let path = ''; for (let j = i+1; j < nbins; ++j) { const cont = hist.getBinContent(i+1, j+1); if (cont <= 0) continue; const pj = pnts[j], a = (pi.a + pj.a)/2, qr = 0.5*(1-Math.abs(pi.a - pj.a)/Math.PI), // how far Q point will be away from center qx = Math.round(qr*rect.width/2 * Math.cos(a)), qy = Math.round(qr*rect.height/2 * Math.sin(a)); path += `M${pi.x},${pi.y}Q${qx},${qy},${pj.x},${pj.y}`; if ((this.options.Circular > 11) && (max_value > min_value)) { const width = Math.round((cont - min_value) / (max_value - min_value) * (max_width - 1) + 1); this.draw_g.append('path').attr('d', path).style('stroke', pi.color).style('stroke-width', width).style('fill', 'none'); path = ''; } } if (path) this.draw_g.append('path').attr('d', path).style('stroke', pi.color).style('fill', 'none'); } return this.finishTextDrawing(); }); } /** @summary Draw histogram bins as chord diagram */ async drawBinsChord() { this.getFrameSvg().style('display', 'none'); this._hide_frame = true; const used = [], nbins = Math.min(this.nbinsx, this.nbinsy), hist = this.getHisto(); let fullsum = 0, isint = true; for (let i = 0; i < nbins; ++i) { let sum = 0; for (let j = 0; j < nbins; ++j) { const cont = hist.getBinContent(i+1, j+1); if (cont > 0) { sum += cont; if (isint && (Math.round(cont) !== cont)) isint = false; } } if (sum > 0) used.push(i); fullsum += sum; } // do not show less than 2 elements if (used.length < 2) return true; let ndig = 0, tickStep = 1; const rect = this.getPadPainter().getFrameRect(), midx = Math.round(rect.x + rect.width/2), midy = Math.round(rect.y + rect.height/2), palette = this.getHistPalette(), outerRadius = Math.max(10, Math.min(rect.width, rect.height) * 0.5 - 60), innerRadius = Math.max(2, outerRadius - 10), data = [], labels = [], formatValue = v => v.toString(), formatTicks = v => ndig > 3 ? v.toExponential(0) : v.toFixed(ndig), d3_descending = (a, b) => { return b < a ? -1 : b > a ? 1 : b >= a ? 0 : Number.NaN; }; if (!isint && fullsum < 10) { const lstep = Math.round(Math.log10(fullsum) - 2.3); ndig = -lstep; tickStep = Math.pow(10, lstep); } else if (fullsum > 200) { const lstep = Math.round(Math.log10(fullsum) - 2.3); tickStep = Math.pow(10, lstep); } if (tickStep * 250 < fullsum) tickStep *= 5; else if (tickStep * 100 < fullsum) tickStep *= 2; for (let i = 0; i < used.length; ++i) { data[i] = []; for (let j = 0; j < used.length; ++j) data[i].push(hist.getBinContent(used[i]+1, used[j]+1)); const axis = hist.fXaxis; let lbl = 'indx_' + used[i].toString(); if (axis.fLabels) { for (let k = 0; k < axis.fLabels.arr.length; ++k) { const tstr = axis.fLabels.arr[k]; if (tstr.fUniqueID === used[i]+1) { lbl = tstr.fString; break; } } } labels.push(lbl); } this.createG(); makeTranslate(this.draw_g, midx + (this._shiftx ?? 0), midy + (this._shifty ?? 0), this._zoom); const chord = d3_chord() .padAngle(10 / innerRadius) .sortSubgroups(d3_descending) .sortChords(d3_descending), chords = chord(data), group = this.draw_g.append('g') .attr('font-size', 10) .attr('font-family', 'sans-serif') .selectAll('g') .data(chords.groups) .join('g'), arc = d3_arc().innerRadius(innerRadius).outerRadius(outerRadius), ribbon = d3_ribbon().radius(innerRadius - 1).padAngle(1 / innerRadius); function ticks({ startAngle, endAngle, value }) { const k = (endAngle - startAngle) / value, arr = []; for (let z = 0; z <= value; z += tickStep) arr.push({ value: z, angle: z * k + startAngle }); return arr; } group.append('path') .attr('fill', d => palette.calcColor(d.index, used.length)) .attr('d', arc); group.append('title').text(d => `${labels[d.index]} ${formatValue(d.value)}`); const groupTick = group.append('g') .selectAll('g') .data(ticks) .join('g') .attr('transform', d => `rotate(${Math.round(d.angle*180/Math.PI-90)}) translate(${outerRadius})`); groupTick.append('line') .attr('stroke', 'currentColor') .attr('x2', 6); groupTick.append('text') .attr('x', 8) .attr('dy', '0.35em') .attr('transform', d => d.angle > Math.PI ? 'rotate(180) translate(-16)' : null) .attr('text-anchor', d => d.angle > Math.PI ? 'end' : null) .text(d => formatTicks(d.value)); group.select('text') .attr('font-weight', 'bold') .text(function(d) { return this.getAttribute('text-anchor') === 'end' ? `↑ ${labels[d.index]}` : `${labels[d.index]} ↓`; }); this.draw_g.append('g') .attr('fill-opacity', 0.8) .selectAll('path') .data(chords) .join('path') .style('mix-blend-mode', 'multiply') .attr('fill', d => palette.calcColor(d.source.index, used.length)) .attr('d', ribbon) .append('title') .text(d => `${formatValue(d.source.value)} ${labels[d.target.index]} → ${labels[d.source.index]}${d.source.index === d.target.index ? '' : `\n${formatValue(d.target.value)} ${labels[d.source.index]} → ${labels[d.target.index]}`}`); if (!this.isBatchMode()) { this.draw_g.insert('ellipse', ':first-child') .attr('cx', 0) .attr('cy', 0) .attr('rx', outerRadius*1.2) .attr('ry', outerRadius*1.2) .style('opacity', 0) .style('fill', 'none') .style('pointer-events', 'visibleFill'); if (settings.Zooming && settings.ZoomWheel) { this.draw_g.on('wheel', evnt => { if (!this._zoom) { this._zoom = 1; this._shiftx = 0; this._shifty = 0; } const pos = d3_pointer(evnt, this.draw_g.node()), delta = evnt.wheelDelta ? -evnt.wheelDelta : (evnt.deltaY || evnt.detail), prev_zoom = this._zoom; this._zoom *= (delta > 0) ? 0.8 : 1.2; this._shiftx += pos[0] * (prev_zoom - this._zoom); this._shifty += pos[1] * (prev_zoom - this._zoom); makeTranslate(this.draw_g, midx + this._shiftx, midy + this._shifty, this._zoom); }).on('dblclick', () => { delete this._zoom; delete this._shiftx; delete this._shifty; makeTranslate(this.draw_g, midx, midy); }); } assignContextMenu(this); } return true; } /** @summary Provide text information (tooltips) for histogram bin */ getBinTooltips(i, j) { const histo = this.getHisto(), profile2d = this.matchObjectType(clTProfile2D) && isFunc(histo.getBinEntries), bincontent = histo.getBinContent(i + 1, j + 1); let binz = bincontent; if (histo.$baseh) binz -= histo.$baseh.getBinContent(i + 1, j + 1); const lines = [this.getObjectHint(), 'x = ' + this.getAxisBinTip('x', histo.fXaxis, i), 'y = ' + this.getAxisBinTip('y', histo.fYaxis, j), `bin = ${histo.getBin(i + 1, j + 1)} x: ${i + 1} y: ${j + 1}`, 'content = ' + ((binz === Math.round(binz)) ? binz : floatToString(binz, gStyle.fStatFormat))]; if ((this.options.TextKind === 'E') || profile2d || histo.fSumw2?.length) { const errs = this.getBinErrors(histo, histo.getBin(i + 1, j + 1), bincontent); if (errs.poisson) lines.push('error low = ' + floatToString(errs.low, gStyle.fPaintTextFormat), 'error up = ' + floatToString(errs.up, gStyle.fPaintTextFormat)); else lines.push('error = ' + floatToString(errs.up, gStyle.fPaintTextFormat)); } if (profile2d) { const entries = histo.getBinEntries(i+1, j+1); lines.push('entries = ' + ((entries === Math.round(entries)) ? entries : floatToString(entries, gStyle.fStatFormat))); } return lines; } /** @summary Provide text information (tooltips) for candle bin */ getCandleTooltips(p) { const fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), histo = this.getHisto(); return [this.getObjectHint(), p.swapXY ? 'y = ' + funcs.axisAsText('y', histo.fYaxis.GetBinLowEdge(p.bin+1)) : 'x = ' + funcs.axisAsText('x', histo.fXaxis.GetBinLowEdge(p.bin+1)), 'm-25% = ' + floatToString(p.fBoxDown, gStyle.fStatFormat), 'median = ' + floatToString(p.fMedian, gStyle.fStatFormat), 'm+25% = ' + floatToString(p.fBoxUp, gStyle.fStatFormat)]; } /** @summary Provide text information (tooltips) for poly bin */ getPolyBinTooltips(binindx, realx, realy) { const histo = this.getHisto(), bin = histo.fBins.arr[binindx], fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), lines = []; let binname = bin.fPoly.fName, numpoints = 0; if (binname === 'Graph') binname = ''; if (binname.length === 0) binname = bin.fNumber; if ((realx === undefined) && (realy === undefined)) { realx = realy = 0; let gr = bin.fPoly, numgraphs = 1; if (gr._typename === clTMultiGraph) { numgraphs = bin.fPoly.fGraphs.arr.length; gr = null; } for (let ngr = 0; ngr < numgraphs; ++ngr) { if (!gr || (ngr > 0)) gr = bin.fPoly.fGraphs.arr[ngr]; for (let n = 0; n < gr.fNpoints; ++n) { ++numpoints; realx += gr.fX[n]; realy += gr.fY[n]; } } if (numpoints > 1) { realx /= numpoints; realy /= numpoints; } } lines.push(this.getObjectHint(), 'x = ' + funcs.axisAsText('x', realx), 'y = ' + funcs.axisAsText('y', realy)); if (numpoints > 0) lines.push('npnts = ' + numpoints); lines.push(`bin = ${binname}`); if (bin.fContent === Math.round(bin.fContent)) lines.push('content = ' + bin.fContent); else lines.push('content = ' + floatToString(bin.fContent, gStyle.fStatFormat)); return lines; } /** @summary Process tooltip event */ processTooltipEvent(pnt) { const histo = this.getHisto(), h = this.tt_handle; let ttrect = this.draw_g?.selectChild('.tooltip_bin'); if (!pnt || !this.draw_content || !this.draw_g || !h || this.options.Proj) { ttrect?.remove(); return null; } if (h.poly) { // process tooltips from TH2Poly const fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), realx = funcs.revertAxis('x', pnt.x), realy = funcs.revertAxis('y', pnt.y); let foundindx = -1, bin; if ((realx !== undefined) && (realy !== undefined)) { const len = histo.fBins.arr.length; for (let i = 0; (i < len) && (foundindx < 0); ++i) { bin = histo.fBins.arr[i]; // found potential bins candidate if ((realx < bin.fXmin) || (realx > bin.fXmax) || (realy < bin.fYmin) || (realy > bin.fYmax)) continue; // ignore empty bins with col0 option if (!bin.fContent && !this.options.Zero) continue; let gr = bin.fPoly, numgraphs = 1; if (gr._typename === clTMultiGraph) { numgraphs = bin.fPoly.fGraphs.arr.length; gr = null; } for (let ngr = 0; ngr < numgraphs; ++ngr) { if (!gr || (ngr > 0)) gr = bin.fPoly.fGraphs.arr[ngr]; if (gr.IsInside(realx, realy)) { foundindx = i; break; } } } } if (foundindx < 0) { ttrect.remove(); return null; } const res = { name: histo.fName, title: histo.fTitle, x: pnt.x, y: pnt.y, color1: this.lineatt?.color ?? 'green', color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue', exact: true, menu: true, lines: this.getPolyBinTooltips(foundindx, realx, realy) }; if (pnt.disabled) { ttrect.remove(); res.changed = true; } else { if (ttrect.empty()) { ttrect = this.draw_g.append('svg:path') .attr('class', 'tooltip_bin') .style('pointer-events', 'none') .call(addHighlightStyle); } res.changed = ttrect.property('current_bin') !== foundindx; if (res.changed) { ttrect.attr('d', this.createPolyBin(funcs, bin)) .style('opacity', '0.7') .property('current_bin', foundindx); } } if (res.changed) { res.user_info = { obj: histo, name: histo.fName, bin: foundindx, cont: bin.fContent, grx: pnt.x, gry: pnt.y }; } return res; } else if (h.candle) { // process tooltips for candle let i, p, match; for (i = 0; i < h.candle.length; ++i) { p = h.candle[i]; match = p.swapXY ? ((p.x1 <= pnt.y) && (pnt.y <= p.x2) && (p.yy1 >= pnt.x) && (pnt.x >= p.yy2)) : ((p.x1 <= pnt.x) && (pnt.x <= p.x2) && (p.yy1 <= pnt.y) && (pnt.y <= p.yy2)); if (match) break; } if (!match) { ttrect.remove(); return null; } const res = { name: histo.fName, title: histo.fTitle, x: pnt.x, y: pnt.y, color1: this.lineatt?.color ?? 'green', color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue', lines: this.getCandleTooltips(p), exact: true, menu: true }; if (pnt.disabled) { ttrect.remove(); res.changed = true; } else { if (ttrect.empty()) { ttrect = this.draw_g.append('svg:path') .attr('class', 'tooltip_bin') .style('pointer-events', 'none') .call(addHighlightStyle) .style('opacity', '0.7'); } res.changed = ttrect.property('current_bin') !== i; if (res.changed) { ttrect.attr('d', p.swapXY ? `M${p.yy1},${p.x1}H${p.yy2}V${p.x2}H${p.yy1}Z` : `M${p.x1},${p.yy1}H${p.x2}V${p.yy2}H${p.x1}Z`) .property('current_bin', i); } } if (res.changed) { res.user_info = { obj: histo, name: histo.fName, bin: i+1, cont: p.fMedian, binx: i+1, biny: 1, grx: pnt.x, gry: pnt.y }; } return res; } const fp = this.getFramePainter(); let i, j, binz = 0, colindx = null, is_pol = false, i1, i2, j1, j2, x1, x2, y1, y2; if (isFunc(h.findBin)) { const bin = h.findBin(pnt.x, pnt.y); i = bin?.i ?? h.i2; j = bin?.j ?? h.j2; is_pol = true; } else { // search bins position if (fp.reverse_x) { for (i = h.i1; i < h.i2; ++i) if ((pnt.x <= h.grx[i]) && (pnt.x >= h.grx[i+1])) break; } else { for (i = h.i1; i < h.i2; ++i) if ((pnt.x >= h.grx[i]) && (pnt.x <= h.grx[i+1])) break; } if (fp.reverse_y) { for (j = h.j1; j < h.j2; ++j) if ((pnt.y <= h.gry[j+1]) && (pnt.y >= h.gry[j])) break; } else { for (j = h.j1; j < h.j2; ++j) if ((pnt.y >= h.gry[j+1]) && (pnt.y <= h.gry[j])) break; } } if ((i < h.i2) && (j < h.j2)) { i1 = i; i2 = i+1; j1 = j; j2 = j+1; x1 = h.grx[i1]; x2 = h.grx[i2]; y1 = h.gry[j2]; y2 = h.gry[j1]; let match = true; if (this.options.Color && !is_pol) { // take into account bar settings const dx = x2 - x1, dy = y2 - y1; x2 = Math.round(x1 + dx*h.xbar2); x1 = Math.round(x1 + dx*h.xbar1); y2 = Math.round(y1 + dy*h.ybar2); y1 = Math.round(y1 + dy*h.ybar1); if (fp.reverse_x) { if ((pnt.x > x1) || (pnt.x <= x2)) match = false; } else if ((pnt.x < x1) || (pnt.x >= x2)) match = false; if (fp.reverse_y) { if ((pnt.y > y1) || (pnt.y <= y2)) match = false; } else if ((pnt.y < y1) || (pnt.y >= y2)) match = false; } binz = histo.getBinContent(i+1, j+1); if (this.is_projection) colindx = 0; // just to avoid hide else if (!match) colindx = null; else if (h.hide_only_zeros) colindx = (binz === 0) && !this.options.ShowEmpty ? null : 0; else { colindx = this.getContour().getPaletteIndex(this.getHistPalette(), binz); if ((colindx === null) && (binz === 0) && (this.options.ShowEmpty || (histo._typename === clTProfile2D && histo.getBinEntries(i + 1, j + 1)))) colindx = 0; } } if (colindx === null) { ttrect.remove(); return null; } const res = { name: histo.fName, title: histo.fTitle, x: pnt.x, y: pnt.y, color1: this.lineatt?.color ?? 'green', color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue', lines: this.getBinTooltips(i, j), exact: true, menu: true }; if (this.options.Color) res.color2 = this.getHistPalette().getColor(colindx); if (pnt.disabled && !this.is_projection) { ttrect.remove(); res.changed = true; } else { if (ttrect.empty()) { ttrect = this.draw_g.append('svg:path') .attr('class', 'tooltip_bin') .style('pointer-events', 'none') .call(addHighlightStyle); } let binid = i*10000 + j, path; if (this.is_projection) { const pwx = this.projection_widthX || 1, ddx = (pwx - 1) / 2; if ((this.is_projection.indexOf('X')) >= 0 && (pwx > 1)) { if (j2+ddx >= h.j2) { j2 = Math.min(Math.round(j2+ddx), h.j2); j1 = Math.max(j2-pwx, h.j1); } else { j1 = Math.max(Math.round(j1-ddx), h.j1); j2 = Math.min(j1+pwx, h.j2); } } const pwy = this.projection_widthY || 1, ddy = (pwy - 1) / 2; if ((this.is_projection.indexOf('Y')) >= 0 && (pwy > 1)) { if (i2+ddy >= h.i2) { i2 = Math.min(Math.round(i2+ddy), h.i2); i1 = Math.max(i2-pwy, h.i1); } else { i1 = Math.max(Math.round(i1-ddy), h.i1); i2 = Math.min(i1+pwy, h.i2); } } } if (is_pol) path = h.getBinPath(i, j); else if (this.is_projection === 'X') { x1 = 0; x2 = fp.getFrameWidth(); y1 = h.gry[j2]; y2 = h.gry[j1]; binid = j1*777 + j2*333; } else if (this.is_projection === 'Y') { y1 = 0; y2 = fp.getFrameHeight(); x1 = h.grx[i1]; x2 = h.grx[i2]; binid = i1*777 + i2*333; } else if (this.is_projection === 'XY') { y1 = h.gry[j2]; y2 = h.gry[j1]; x1 = h.grx[i1]; x2 = h.grx[i2]; binid = i1*789 + i2*653 + j1*12345 + j2*654321; path = `M${x1},0H${x2}V${y1}H${fp.getFrameWidth()}V${y2}H${x2}V${fp.getFrameHeight()}H${x1}V${y2}H0V${y1}H${x1}Z`; } res.changed = ttrect.property('current_bin') !== binid; if (res.changed) { ttrect.attr('d', path || `M${x1},${y1}H${x2}V${y2}H${x1}Z`) .style('opacity', '0.7') .property('current_bin', binid); } if (this.is_projection && res.changed) this.redrawProjection(i1, i2, j1, j2); } if (res.changed) { res.user_info = { obj: histo, name: histo.fName, bin: histo.getBin(i+1, j+1), cont: binz, binx: i+1, biny: j+1, grx: pnt.x, gry: pnt.y }; } return res; } /** @summary Checks if it makes sense to zoom inside specified axis range */ canZoomInside(axis, min, max) { if (this.options.Proj) return true; // z-scale zooming allowed only if special ignore-palette is not provided if (axis === 'z') { if (this.mode3d) return true; if (this.options.IgnorePalette) return false; const fp = this.getFramePainter(), nlevels = Math.max(2*gStyle.fNumberContours, 100), pad = this.getPadPainter().getRootPad(true), logv = pad?.fLogv ?? pad?.fLogz; if (!fp || (fp.zmin === fp.zmax)) return true; if (logv && (fp.zmin > 0) && (min > 0)) return nlevels * Math.log(max/min) > Math.log(fp.zmax/fp.zmin); return (fp.zmax - fp.zmin) < (max - min) * nlevels; } let obj = this.getHisto(); if (obj) obj = (axis === 'y') ? obj.fYaxis : obj.fXaxis; return !obj || (obj.FindBin(max, 0.5) - obj.FindBin(min, 0) > 1); } /** @summary Complete palette drawing */ completePalette(pp) { if (!pp) return true; pp.$main_painter = this; this.options.Zvert = pp._palette_vertical; // redraw palette till the end when contours are available return pp.drawPave(this.options.Cjust ? 'cjust' : ''); } /** @summary Performs 2D drawing of histogram * @return {Promise} when ready */ async draw2D(/* reason */) { this.clear3DScene(); const need_palette = this.options.Zscale && this.options.canHavePalette() && !this._ignore_frame; // draw new palette, resize frame if required return this.drawColorPalette(need_palette, true).then(async pp => { let pr; if (this.options.Circular && this.isMainPainter()) pr = this.drawBinsCircular(); else if (this.options.Chord && this.isMainPainter()) pr = this.drawBinsChord(); else pr = this.drawAxes().then(() => this.draw2DBins()); return pr.then(() => this.completePalette(pp)); }).then(() => this.updateFunctions()) .then(() => this.updateHistTitle()) .then(() => { this.updateStatWebCanvas(); return this.addInteractivity(); }); } /** @summary Should performs 3D drawing of histogram * @desc Disabled in 2D case. just draw default draw options * @return {Promise} when ready */ async draw3D(reason) { console.log('3D drawing is disabled, load ./hist/TH2Painter.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); else console.error('Fail to draw histogram in 3D - back to 2D'); this.options.Mode3D = false; return this.draw2D(reason); }); } /** @summary Redraw histogram */ async redraw(reason) { return this.callDrawFunc(reason); } /** @summary draw TH2 object in 2D only */ static async draw(dom, histo, opt) { return THistPainter._drawHist(new TH2Painter(dom, histo), opt); } } // class TH2Painter export { TH2Painter, buildHist2dContour, buildSurf3D, Triangles3DHandler };