import { gStyle, createHistogram, createTPolyLine, isFunc, isStr,
clTMultiGraph, clTH1D, clTF2, clTProfile2D, kInspect } from '../core.mjs';
import { 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 } from '../base/BasePainter.mjs';
import { EAxisBits } from '../base/ObjectPainter.mjs';
import { THistPainter } from './THistPainter.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, ipoly, poly, np, npmax = 0,
i, j, k, n, m, ljfill, count,
xsave, ysave, itars, ix, jx;
const LinearSearch = zc => {
if (zc >= last_level)
return nlevels-1;
for (let kk = 0; kk < nlevels; ++kk) {
if (zc < levels[kk])
return kk-1;
}
return nlevels-1;
}, BinarySearch = zc => {
if (zc < first_level)
return -1;
if (zc >= last_level)
return nlevels - 1;
let l = 0, r = nlevels - 1, m;
while (r - l > 1) {
m = Math.round((r + l) / 2);
if (zc < 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;
};
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);
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, np = 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 < np; 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 < np; 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[levels.length-1] - levels[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) {
const axis = p.fXaxis;
axis.fFirst = first;
axis.fLast = last;
if (((axis.fFirst === 1) && (axis.fLast === axis.fNbins)) === axis.TestBit(EAxisBits.kAxisRange))
axis.InvertBit(EAxisBits.kAxisRange);
}
// 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 = fp.getGrFuncs(this.options.second_x, this.options.second_y),
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 = xx / numpoints;
yy = 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 = 0;
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 = [],
show_empty = this.options.ShowEmpty,
can_merge_x = (handle.xbar2 === 1) && (handle.xbar1 === 0),
can_merge_y = (handle.ybar2 === 1) && (handle.ybar1 === 0),
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);
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;
} else
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, colindx) => {
if (entry) {
this.draw_g.append('svg:path')
.attr('fill', palette.getColor(colindx))
.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 = main.getGrFuncs(this.options.second_x, this.options.second_y),
ilevels = this.getContourLevels(),
palette = this.getHistPalette(),
func = main.getProjectionFunc();
handle.grz = z => z;
handle.grz_min = ilevels[0];
handle.grz_max = ilevels[ilevels.length - 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 = (i, di) => {
const segm = { x1: xp[i], y1: yp[i], x2: 2*xp[i] - xp[i+di], y2: 2*yp[i] - yp[i+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 = fp.getGrFuncs(this.options.second_x, this.options.second_y),
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 = bin => {
// check if bin outside visible range
return ((bin.fXmin > funcs.scale_xmax) || (bin.fXmax < funcs.scale_xmin) ||
(bin.fYmin > funcs.scale_ymax) || (bin.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 cmd = '';
for (let n = 0; n < npnts; ++n)
cmd += markeratt.create(funcs.grx(gr.fX[n]), funcs.gry(gr.fY[n]));
this.draw_g.append('svg:path')
.attr('d', cmd)
.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 errz = histo.getBinError(histo.getBin(i+1, j+1)),
lble = (errz === Math.round(errz)) ? errz.toString() : floatToString(errz, gStyle.fPaintTextFormat);
if (this.options.TextLine)
text += '\xB1' + lble;
else
text = `#splitline{${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 }),
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}` : '');
let i, j, dn = 1e-30, dx, dy, xc, yc, cmd = '',
dxn, dyn, x1, x2, y1, y2, anr, si, co;
for (let loop = 0; loop < 2; ++loop) {
for (i = handle.i1; i < handle.i2; ++i) {
for (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;
if (i === handle.i1)
dx = histo.getBinContent(i+2, j+1) - histo.getBinContent(i+1, j+1);
else if (i === handle.i2-1)
dx = histo.getBinContent(i+1, j+1) - 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) - histo.getBinContent(i+1, j+1);
else if (j === handle.j2-1)
dy = histo.getBinContent(i+1, j+1) - 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) {
cmd += `M${Math.round(x1)},${Math.round(y1)}${makeLine(dx, dy)}`;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
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)}`;
}
}
}
}
}
}
this.draw_g
.append('svg:path')
.attr('d', cmd)
.style('fill', 'none')
.call(this.lineatt.func);
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 pww = Math.round(ww*0.1),
phh = Math.round(hh*0.1),
side1 = `M${xx},${yy}h${ww}l${-pww},${phh}h${2*pww-ww}v${hh-2*phh}l${-pww},${phh}z`,
side2 = `M${xx+ww},${yy+hh}v${-hh}l${-pww},${phh}v${hh-2*phh}h${2*pww-ww}l${-pww},${phh}z`;
btn1 += (binz < 0) ? side2 : side1;
btn2 += (binz < 0) ? side1 : side2;
}
}
}
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[xx.length-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 = fp.getGrFuncs(this.options.second_x, this.options.second_y),
swapXY = isOption(kHorizontal);
let bars = '', lines = '', dashed_lines = '',
hists = '', hlines = '',
markers = '', cmarkers = '', attrcmarkers = null,
xx, proj,
scaledViolin = gStyle.fViolinScaled,
scaledCandle = gStyle.fCandleScaled,
maxContent = 0, maxIntegral = 0;
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) {
this.createAttMarker({ attr: histo, style: isOption(kPointsAllScat) ? 0 : 5 });
this.markeratt.resetPos();
}
markers += swapXY ? this.markeratt.create(y, x) : this.markeratt.create(x, y);
}, make_cmarker = (x, y) => {
if (!attrcmarkers) {
attrcmarkers = this.createAttMarker({ attr: histo, style: 24, 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
// 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 = 0;
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;
}
if (!this.draw_content)
return this.removeG();
this.createHistDrawAttributes();
this.createG(true);
let handle, pr;
if (this.isTH2Poly())
pr = this.drawPolyBins();
else {
if (this.options.Scat)
handle = this.drawBinsScatter();
if (this.options.Color)
handle = this.drawBinsColor();
else if (this.options.Box)
handle = this.drawBinsBox();
else if (this.options.Arrow)
handle = this.drawBinsArrow();
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(),
palette = this.getHistPalette(),
outerRadius = Math.max(10, Math.min(rect.width, rect.height) * 0.5 - 60),
innerRadius = Math.max(2, outerRadius - 10),
data = [], labels = [],
getColor = indx => palette.calcColor(indx, used.length),
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, Math.round(rect.x + rect.width/2), Math.round(rect.y + rect.height/2));
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 => getColor(d.index))
.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 => getColor(d.source.index))
.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]}`}`);
return true;
}
/** @summary Provide text information (tooltips) for histogram bin */
getBinTooltips(i, j) {
const histo = this.getHisto(),
profile2d = this.matchObjectType(clTProfile2D) && isFunc(histo.getBinEntries);
let binz = histo.getBinContent(i+1, j+1);
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) {
const errz = histo.getBinError(histo.getBin(i+1, j+1));
lines.push('error = ' + ((errz === Math.round(errz)) ? errz.toString() : floatToString(errz, 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 = fp.getGrFuncs(this.options.second_x, this.options.second_y),
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 = fp.getGrFuncs(this.options.second_x, this.options.second_y),
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 = realx / numpoints;
realy = 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 = fp.getGrFuncs(this.options.second_x, this.options.second_y),
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,
i1, i2, j1, j2, x1, x2, y1, y2;
// 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) {
// 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 (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();
// 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);
return this.options.Mode3D ? this.draw3D(reason) : this.draw2D(reason);
}
/** @summary Redraw histogram */
async redraw(reason) {
return this.callDrawFunc(reason);
}
/** @summary draw TH2 object */
static async draw(dom, histo, opt) {
return THistPainter._drawHist(new TH2Painter(dom, histo), opt);
}
} // class TH2Painter
export { TH2Painter, buildHist2dContour, buildSurf3D, Triangles3DHandler };