jsroot
Version:
JavaScript ROOT
1,348 lines (1,120 loc) • 135 kB
JavaScript
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),
x