UNPKG

jsroot

Version:
1,348 lines (1,120 loc) 135 kB
import { gStyle, settings, createHistogram, createTPolyLine, isFunc, isStr, clTMultiGraph, clTH1D, clTF2, clTProfile2D, kInspect } from '../core.mjs'; import { pointer as d3_pointer, rgb as d3_rgb, chord as d3_chord, arc as d3_arc, ribbon as d3_ribbon } from '../d3.mjs'; import { kBlack } from '../base/colors.mjs'; import { TRandom, floatToString, makeTranslate, addHighlightStyle, getBoxDecorations } from '../base/BasePainter.mjs'; import { EAxisBits } from '../base/ObjectPainter.mjs'; import { THistPainter, kPOLAR } from './THistPainter.mjs'; import { assignContextMenu } from '../gui/menu.mjs'; /** @summary Build histogram contour lines * @private */ function buildHist2dContour(histo, handle, levels, palette, contour_func) { const kMAXCONTOUR = 2004, kMAXCOUNT = 2000, // arguments used in the PaintContourLine xarr = new Float32Array(2*kMAXCONTOUR), yarr = new Float32Array(2*kMAXCONTOUR), itarr = new Int32Array(2*kMAXCONTOUR), nlevels = levels.length, first_level = levels[0], last_level = levels[nlevels - 1], polys = [], x = [0, 0, 0, 0], y = [0, 0, 0, 0], zc = [0, 0, 0, 0], ir = [0, 0, 0, 0], arrx = handle.grx, arry = handle.gry; let lj = 0; const LinearSearch = zvalue => { if (zvalue >= last_level) return nlevels - 1; for (let kk = 0; kk < nlevels; ++kk) { if (zvalue < levels[kk]) return kk-1; } return nlevels - 1; }, BinarySearch = zvalue => { if (zvalue < first_level) return -1; if (zvalue >= last_level) return nlevels - 1; let l = 0, r = nlevels - 1, m; while (r - l > 1) { m = Math.round((r + l) / 2); if (zvalue < levels[m]) r = m; else l = m; } return l; }, LevelSearch = nlevels < 10 ? LinearSearch : BinarySearch, PaintContourLine = (elev1, icont1, x1, y1, elev2, icont2, x2, y2) => { /* Double_t *xarr, Double_t *yarr, Int_t *itarr, Double_t *levels */ const vert = (x1 === x2), tlen = vert ? (y2 - y1) : (x2 - x1), tdif = elev2 - elev1; let n = icont1 + 1, ii = lj-1, icount = 0, xlen, pdif, diff, elev; const maxii = ii + kMAXCONTOUR/2 -3; while (n <= icont2 && ii <= maxii) { // elev = fH->GetContourLevel(n); elev = levels[n]; diff = elev - elev1; pdif = diff/tdif; xlen = tlen*pdif; if (vert) { xarr[ii] = x1; yarr[ii] = y1 + xlen; } else { xarr[ii] = x1 + xlen; yarr[ii] = y1; } itarr[ii] = n; icount++; ii += 2; n++; } return icount; }; let ipoly, poly, npmax = 0, i, j, k, m, n, ljfill, count, xsave, ysave, itars, ix, jx; for (j = handle.j1; j < handle.j2-1; ++j) { y[1] = y[0] = (arry[j] + arry[j+1])/2; y[3] = y[2] = (arry[j+1] + arry[j+2])/2; for (i = handle.i1; i < handle.i2-1; ++i) { zc[0] = histo.getBinContent(i+1, j+1); zc[1] = histo.getBinContent(i+2, j+1); zc[2] = histo.getBinContent(i+2, j+2); zc[3] = histo.getBinContent(i+1, j+2); for (k = 0; k < 4; k++) ir[k] = LevelSearch(zc[k]); if ((ir[0] !== ir[1]) || (ir[1] !== ir[2]) || (ir[2] !== ir[3]) || (ir[3] !== ir[0])) { x[3] = x[0] = (arrx[i] + arrx[i+1])/2; x[2] = x[1] = (arrx[i+1] + arrx[i+2])/2; if (zc[0] <= zc[1]) n = 0; else n = 1; if (zc[2] <= zc[3]) m = 2; else m = 3; if (zc[n] > zc[m]) n = m; n++; lj=1; for (ix=1; ix<=4; ix++) { m = n%4 + 1; ljfill = PaintContourLine(zc[n-1], ir[n-1], x[n-1], y[n-1], zc[m-1], ir[m-1], x[m-1], y[m-1]); lj += 2*ljfill; n = m; } if (zc[0] <= zc[1]) n = 0; else n = 1; if (zc[2] <= zc[3]) m = 2; else m = 3; if (zc[n] > zc[m]) n = m; n++; lj=2; for (ix=1; ix<=4; ix++) { m = (n === 1) ? 4 : n-1; ljfill = PaintContourLine(zc[n-1], ir[n-1], x[n-1], y[n-1], zc[m-1], ir[m-1], x[m-1], y[m-1]); lj += 2*ljfill; n = m; } // Re-order endpoints count = 0; for (ix = 1; ix <= lj - 5; ix += 2) { // count = 0; while (itarr[ix-1] !== itarr[ix]) { xsave = xarr[ix]; ysave = yarr[ix]; itars = itarr[ix]; for (jx=ix; jx<=lj-5; jx +=2) { xarr[jx] = xarr[jx+2]; yarr[jx] = yarr[jx+2]; itarr[jx] = itarr[jx+2]; } xarr[lj-3] = xsave; yarr[lj-3] = ysave; itarr[lj-3] = itars; if (count > kMAXCOUNT) break; count++; } } if (count > 100) continue; for (ix = 1; ix <= lj - 2; ix += 2) { ipoly = itarr[ix-1]; if ((ipoly >= 0) && (ipoly < levels.length)) { poly = polys[ipoly]; if (!poly) poly = polys[ipoly] = createTPolyLine(kMAXCONTOUR*4, true); const np = poly.fLastPoint; if (np < poly.fN-2) { poly.fX[np+1] = Math.round(xarr[ix-1]); poly.fY[np+1] = Math.round(yarr[ix-1]); poly.fX[np+2] = Math.round(xarr[ix]); poly.fY[np+2] = Math.round(yarr[ix]); poly.fLastPoint = np+2; npmax = Math.max(npmax, poly.fLastPoint+1); } else { // console.log(`reject point ${poly.fLastPoint}`); } } } } // end of if (ir[0] } // end of j } // end of i const polysort = new Int32Array(levels.length); let first = 0; // find first positive contour for (ipoly = 0; ipoly < levels.length; ipoly++) if (levels[ipoly] >= 0) { first = ipoly; break; } // store negative contours from 0 to minimum, then all positive contours k = 0; for (ipoly = first-1; ipoly >= 0; ipoly--) { polysort[k] = ipoly; k++; } for (ipoly = first; ipoly < levels.length; ipoly++) { polysort[k] = ipoly; k++; } const xp = new Float32Array(2*npmax), yp = new Float32Array(2*npmax), has_func = isFunc(palette.calcColorIndex); // rcanvas for v7 for (k = 0; k < levels.length; ++k) { ipoly = polysort[k]; poly = polys[ipoly]; if (!poly) continue; const colindx = has_func ? palette.calcColorIndex(ipoly, levels.length) : ipoly, xx = poly.fX, yy = poly.fY, np2 = poly.fLastPoint+1, xmin = 0, ymin = 0; let istart = 0, iminus, iplus, nadd; while (true) { iminus = npmax; iplus = iminus+1; xp[iminus]= xx[istart]; yp[iminus] = yy[istart]; xp[iplus] = xx[istart+1]; yp[iplus] = yy[istart+1]; xx[istart] = xx[istart+1] = xmin; yy[istart] = yy[istart+1] = ymin; while (true) { nadd = 0; for (i = 2; i < np2; i += 2) { if ((iplus < 2*npmax-1) && (xx[i] === xp[iplus]) && (yy[i] === yp[iplus])) { iplus++; xp[iplus] = xx[i+1]; yp[iplus] = yy[i+1]; xx[i] = xx[i+1] = xmin; yy[i] = yy[i+1] = ymin; nadd++; } if ((iminus > 0) && (xx[i+1] === xp[iminus]) && (yy[i+1] === yp[iminus])) { iminus--; xp[iminus] = xx[i]; yp[iminus] = yy[i]; xx[i] = xx[i+1] = xmin; yy[i] = yy[i+1] = ymin; nadd++; } } if (nadd === 0) break; } if ((iminus+1 < iplus) && (iminus >= 0)) contour_func(colindx, xp, yp, iminus, iplus, ipoly); istart = 0; for (i = 2; i < np2; i += 2) { if (xx[i] !== xmin && yy[i] !== ymin) { istart = i; break; } } if (istart === 0) break; } } } /** @summary Handle 3D triangles with color levels */ class Triangles3DHandler { constructor(ilevels, grz, grz_min, grz_max, dolines, donormals, dogrid) { let levels = [grz_min, grz_max]; // just cut top/bottom parts if (ilevels) { // recalculate levels into graphical coordinates levels = new Float32Array(ilevels.length); for (let ll = 0; ll < ilevels.length; ++ll) levels[ll] = grz(ilevels[ll]); } Object.assign(this, { grz_min, grz_max, dolines, donormals, dogrid }); this.loop = 0; const nfaces = [], posbuf = [], posbufindx = [], // buffers for faces pntbuf = new Float32Array(6*3), // maximal 6 points gridpnts = new Float32Array(2*3), levels_eps = (levels.at(-1) - levels.at(0)) / levels.length / 1e2; let nsegments = 0, lpos = null, lindx = 0, // buffer for lines ngridsegments = 0, grid = null, gindx = 0, // buffer for grid lines segments normindx = [], // buffer to remember place of vertex for each bin pntindx = 0, lastpart = 0, gridcnt = 0; function checkSide(z, level1, level2, eps) { return (z < level1 - eps) ? -1 : (z > level2 + eps ? 1 : 0); } this.createNormIndex = function(handle) { // for each bin maximal 8 points reserved if (handle.donormals) normindx = new Int32Array((handle.i2-handle.i1)*(handle.j2-handle.j1)*8).fill(-1); }; this.createBuffers = function() { if (!this.loop) return; for (let lvl = 1; lvl < levels.length; ++lvl) { if (nfaces[lvl]) { posbuf[lvl] = new Float32Array(nfaces[lvl] * 9); posbufindx[lvl] = 0; } } if (this.dolines && (nsegments > 0)) lpos = new Float32Array(nsegments * 6); if (this.dogrid && (ngridsegments > 0)) grid = new Float32Array(ngridsegments * 6); }; this.addLineSegment = function(x1, y1, z1, x2, y2, z2) { if (!this.dolines) return; const side1 = checkSide(z1, this.grz_min, this.grz_max, 0), side2 = checkSide(z2, this.grz_min, this.grz_max, 0); if ((side1 === side2) && (side1 !== 0)) return; if (!this.loop) return ++nsegments; if (side1 !== 0) { const diff = z2 - z1; z1 = (side1 < 0) ? this.grz_min : this.grz_max; x1 = x2 - (x2 - x1) / diff * (z2 - z1); y1 = y2 - (y2 - y1) / diff * (z2 - z1); } if (side2 !== 0) { const diff = z1 - z2; z2 = (side2 < 0) ? this.grz_min : this.grz_max; x2 = x1 - (x1 - x2) / diff * (z1 - z2); y2 = y1 - (y1 - y2) / diff * (z1 - z2); } lpos[lindx] = x1; lpos[lindx+1] = y1; lpos[lindx+2] = z1; lindx+=3; lpos[lindx] = x2; lpos[lindx+1] = y2; lpos[lindx+2] = z2; lindx+=3; }; function addCrossingPoint(xx1, yy1, zz1, xx2, yy2, zz2, crossz, with_grid) { if (pntindx >= pntbuf.length) console.log('more than 6 points???'); const part = (crossz - zz1) / (zz2 - zz1); let shift = 3; if ((lastpart !== 0) && (Math.abs(part) < Math.abs(lastpart))) { // while second crossing point closer than first to original, move it in memory pntbuf[pntindx] = pntbuf[pntindx-3]; pntbuf[pntindx+1] = pntbuf[pntindx-2]; pntbuf[pntindx+2] = pntbuf[pntindx-1]; pntindx-=3; shift = 6; } pntbuf[pntindx] = xx1 + part*(xx2-xx1); pntbuf[pntindx+1] = yy1 + part*(yy2-yy1); pntbuf[pntindx+2] = crossz; if (with_grid && grid) { gridpnts[gridcnt] = pntbuf[pntindx]; gridpnts[gridcnt+1] = pntbuf[pntindx+1]; gridpnts[gridcnt+2] = pntbuf[pntindx+2]; gridcnt += 3; } pntindx += shift; lastpart = part; } function rememberVertex(indx, handle, ii, jj) { const bin = ((ii-handle.i1) * (handle.j2-handle.j1) + (jj-handle.j1))*8; if (normindx[bin] >= 0) return console.error('More than 8 vertexes for the bin'); const pos = bin + 8 + normindx[bin]; // position where write index normindx[bin]--; normindx[pos] = indx; // at this moment index can be overwritten, means all 8 position are there } this.addMainTriangle = function(x1, y1, z1, x2, y2, z2, x3, y3, z3, is_first, handle, i, j) { for (let lvl = 1; lvl < levels.length; ++lvl) { let side1 = checkSide(z1, levels[lvl-1], levels[lvl], levels_eps), side2 = checkSide(z2, levels[lvl-1], levels[lvl], levels_eps), side3 = checkSide(z3, levels[lvl-1], levels[lvl], levels_eps), side_sum = side1 + side2 + side3; // always show top segments if ((lvl > 1) && (lvl === levels.length - 1) && (side_sum === 3) && (z1 <= this.grz_max)) side1 = side2 = side3 = side_sum = 0; if (side_sum === 3) continue; if (side_sum === -3) return; if (!this.loop) { let npnts = Math.abs(side2-side1) + Math.abs(side3-side2) + Math.abs(side1-side3); if (side1 === 0) ++npnts; if (side2 === 0) ++npnts; if (side3 === 0) ++npnts; if ((npnts === 1) || (npnts === 2)) console.error(`FOUND npnts = ${npnts}`); if (npnts > 2) { if (nfaces[lvl] === undefined) nfaces[lvl] = 0; nfaces[lvl] += npnts-2; } // check if any(contours for given level exists if (((side1 > 0) || (side2 > 0) || (side3 > 0)) && ((side1 !== side2) || (side2 !== side3) || (side3 !== side1))) ++ngridsegments; continue; } gridcnt = 0; pntindx = 0; if (side1 === 0) { pntbuf[pntindx] = x1; pntbuf[pntindx+1] = y1; pntbuf[pntindx+2] = z1; pntindx += 3; } if (side1 !== side2) { // order is important, should move from 1->2 point, checked via lastpart lastpart = 0; if ((side1 < 0) || (side2 < 0)) addCrossingPoint(x1, y1, z1, x2, y2, z2, levels[lvl-1]); if ((side1 > 0) || (side2 > 0)) addCrossingPoint(x1, y1, z1, x2, y2, z2, levels[lvl], true); } if (side2 === 0) { pntbuf[pntindx] = x2; pntbuf[pntindx+1] = y2; pntbuf[pntindx+2] = z2; pntindx += 3; } if (side2 !== side3) { // order is important, should move from 2->3 point, checked via lastpart lastpart = 0; if ((side2 < 0) || (side3 < 0)) addCrossingPoint(x2, y2, z2, x3, y3, z3, levels[lvl-1]); if ((side2 > 0) || (side3 > 0)) addCrossingPoint(x2, y2, z2, x3, y3, z3, levels[lvl], true); } if (side3 === 0) { pntbuf[pntindx] = x3; pntbuf[pntindx+1] = y3; pntbuf[pntindx+2] = z3; pntindx += 3; } if (side3 !== side1) { // order is important, should move from 3->1 point, checked via lastpart lastpart = 0; if ((side3 < 0) || (side1 < 0)) addCrossingPoint(x3, y3, z3, x1, y1, z1, levels[lvl-1]); if ((side3 > 0) || (side1 > 0)) addCrossingPoint(x3, y3, z3, x1, y1, z1, levels[lvl], true); } if (pntindx === 0) continue; if (pntindx < 9) { console.log(`found ${pntindx/3} points, must be at least 3`); continue; } if (grid && (gridcnt === 6)) { for (let jj = 0; jj < 6; ++jj) grid[gindx+jj] = gridpnts[jj]; gindx += 6; } // if three points and surf === 14, remember vertex for each point const buf = posbuf[lvl]; let s = posbufindx[lvl]; if (this.donormals && (pntindx === 9)) { rememberVertex(s, handle, i, j); rememberVertex(s+3, handle, i+1, is_first ? j+1 : j); rememberVertex(s+6, handle, is_first ? i : i+1, j+1); } for (let k1 = 3; k1 < pntindx - 3; k1 += 3) { buf[s] = pntbuf[0]; buf[s+1] = pntbuf[1]; buf[s+2] = pntbuf[2]; s+=3; buf[s] = pntbuf[k1]; buf[s+1] = pntbuf[k1+1]; buf[s+2] = pntbuf[k1+2]; s+=3; buf[s] = pntbuf[k1+3]; buf[s+1] = pntbuf[k1+4]; buf[s+2] = pntbuf[k1+5]; s+=3; } posbufindx[lvl] = s; } }; this.callFuncs = function(meshFunc, linesFunc) { for (let lvl = 1; lvl < levels.length; ++lvl) { if (posbuf[lvl] && meshFunc) meshFunc(lvl, posbuf[lvl], normindx); } if (lpos && linesFunc) { if (nsegments*6 !== lindx) console.error(`SURF lines mismmatch nsegm=${nsegments} lindx=${lindx} diff=${nsegments*6 - lindx}`); linesFunc(false, lpos); } if (grid && linesFunc) { if (ngridsegments*6 !== gindx) console.error(`SURF grid draw mismatch ngridsegm=${ngridsegments} gindx=${gindx} diff=${ngridsegments*6 - gindx}`); linesFunc(true, grid); } }; } } /** @summary Build 3d surface * @desc Make it independent from three.js to be able reuse it for 2D case * @private */ function buildSurf3D(histo, handle, ilevels, meshFunc, linesFunc) { const main_grz = handle.grz, arrx = handle.original ? handle.origx : handle.grx, arry = handle.original ? handle.origy : handle.gry, triangles = new Triangles3DHandler(ilevels, handle.grz, handle.grz_min, handle.grz_max, handle.dolines, handle.donormals, handle.dogrid); let i, j, x1, x2, y1, y2, z11, z12, z21, z22; triangles.createNormIndex(handle); for (triangles.loop = 0; triangles.loop < 2; ++triangles.loop) { triangles.createBuffers(); for (i = handle.i1; i < handle.i2-1; ++i) { x1 = handle.original ? 0.5 * (arrx[i] + arrx[i+1]) : arrx[i]; x2 = handle.original ? 0.5 * (arrx[i+1] + arrx[i+2]) : arrx[i+1]; for (j = handle.j1; j < handle.j2-1; ++j) { y1 = handle.original ? 0.5 * (arry[j] + arry[j+1]) : arry[j]; y2 = handle.original ? 0.5 * (arry[j+1] + arry[j+2]) : arry[j+1]; z11 = main_grz(histo.getBinContent(i+1, j+1)); z12 = main_grz(histo.getBinContent(i+1, j+2)); z21 = main_grz(histo.getBinContent(i+2, j+1)); z22 = main_grz(histo.getBinContent(i+2, j+2)); triangles.addMainTriangle(x1, y1, z11, x2, y2, z22, x1, y2, z12, true, handle, i, j); triangles.addMainTriangle(x1, y1, z11, x2, y1, z21, x2, y2, z22, false, handle, i, j); triangles.addLineSegment(x1, y2, z12, x1, y1, z11); triangles.addLineSegment(x1, y1, z11, x2, y1, z21); if (i === handle.i2 - 2) triangles.addLineSegment(x2, y1, z21, x2, y2, z22); if (j === handle.j2 - 2) triangles.addLineSegment(x1, y2, z12, x2, y2, z22); } } } triangles.callFuncs(meshFunc, linesFunc); } /** * @summary Painter for TH2 classes * @private */ class TH2Painter extends THistPainter { /** @summary constructor * @param {object} histo - histogram object */ constructor(dom, histo) { super(dom, histo); this.wheel_zoomy = true; } /** @summary cleanup painter */ cleanup() { delete this.tt_handle; super.cleanup(); } /** @summary Returns histogram * @desc Also assigns custom getBinContent method for TProfile2D if PROJXY options specified */ getHisto() { const histo = super.getHisto(); if (histo?._typename === clTProfile2D) { if (!histo.$getBinContent) histo.$getBinContent = histo.getBinContent; switch (this.options?.Profile2DProj) { case 'B': histo.getBinContent = histo.getBinEntries; break; case 'C=E': histo.getBinContent = function(i, j) { return this.getBinError(this.getBin(i, j)); }; break; case 'W': histo.getBinContent = function(i, j) { return this.$getBinContent(i, j) * this.getBinEntries(i, j); }; break; default: histo.getBinContent = histo.$getBinContent; break; } } return histo; } /** @summary Toggle projection */ toggleProjection(kind, width) { if ((kind === 'Projections') || (kind === 'Off')) kind = ''; let widthX = width, widthY = width; if (isStr(kind) && (kind.indexOf('XY') === 0)) { const ws = (kind.length > 2) ? kind.slice(2) : ''; kind = 'XY'; widthX = widthY = parseInt(ws) || 1; } else if (isStr(kind) && (kind.length > 1)) { const ps = kind.indexOf('_'); if ((ps > 0) && (kind[0] === 'X') && (kind[ps+1] === 'Y')) { widthX = parseInt(kind.slice(1, ps)) || 1; widthY = parseInt(kind.slice(ps+2)) || 1; kind = 'XY'; } else if ((ps > 0) && (kind[0] === 'Y') && (kind[ps+1] === 'X')) { widthY = parseInt(kind.slice(1, ps)) || 1; widthX = parseInt(kind.slice(ps+2)) || 1; kind = 'XY'; } else { widthX = widthY = parseInt(kind.slice(1)) || 1; kind = kind[0]; } } if (!widthX && !widthY) widthX = widthY = 1; if (kind && (this.is_projection === kind)) { if ((this.projection_widthX === widthX) && (this.projection_widthY === widthY)) kind = ''; else { this.projection_widthX = widthX; this.projection_widthY = widthY; return; } } delete this.proj_hist; const new_proj = (this.is_projection === kind) ? '' : kind; this.projection_widthX = widthX; this.projection_widthY = widthY; this.is_projection = ''; // avoid projection handling until area is created return this.provideSpecialDrawArea(new_proj).then(() => { this.is_projection = new_proj; return this.redrawProjection(); }); } /** @summary Redraw projection */ async redrawProjection(ii1, ii2, jj1, jj2) { if (!this.is_projection) return false; if (jj2 === undefined) { if (!this.tt_handle) return; ii1 = Math.round((this.tt_handle.i1 + this.tt_handle.i2)/2); ii2 = ii1+1; jj1 = Math.round((this.tt_handle.j1 + this.tt_handle.j2)/2); jj2 = jj1+1; } const canp = this.getCanvPainter(); if (canp && !canp._readonly && (this.snapid !== undefined)) { // this is when projection should be created on the server side if (((this.is_projection === 'X') || (this.is_projection === 'XY')) && !canp.websocketTimeout('projX')) { if (canp.sendWebsocket(`EXECANDSEND:DXPROJ:${this.snapid}:ProjectionX("_projx",${jj1+1},${jj2},"")`)) canp.websocketTimeout('projX', 1000); } if (((this.is_projection === 'Y') || (this.is_projection === 'XY')) && !canp.websocketTimeout('projY')) { if (canp.sendWebsocket(`EXECANDSEND:DYPROJ:${this.snapid}:ProjectionY("_projy",${ii1+1},${ii2},"")`)) canp.websocketTimeout('projY', 1000); } return true; } if (this.doing_projection) return false; this.doing_projection = true; const histo = this.getHisto(), createXProject = () => { const p = createHistogram(clTH1D, this.nbinsx); Object.assign(p.fXaxis, histo.fXaxis); p.fName = 'xproj'; p.fTitle = 'X projection'; return p; }, createYProject = () => { const p = createHistogram(clTH1D, this.nbinsy); Object.assign(p.fXaxis, histo.fYaxis); p.fName = 'yproj'; p.fTitle = 'Y projection'; return p; }, fillProjectHist = (kind, p) => { let first = 0, last = -1; if (kind === 'X') { for (let i = 0; i < this.nbinsx; ++i) { let sum = 0; for (let j = jj1; j < jj2; ++j) sum += histo.getBinContent(i+1, j+1); p.setBinContent(i+1, sum); } p.fTitle = 'X projection ' + (jj1+1 === jj2 ? `bin ${jj2}` : `bins [${jj1+1} .. ${jj2}]`); if (this.tt_handle) { first = this.tt_handle.i1+1; last = this.tt_handle.i2; } } else { for (let j = 0; j < this.nbinsy; ++j) { let sum = 0; for (let i = ii1; i < ii2; ++i) sum += histo.getBinContent(i+1, j+1); p.setBinContent(j+1, sum); } p.fTitle = 'Y projection ' + (ii1+1 === ii2 ? `bin ${ii2}` : `bins [${ii1+1} .. ${ii2}]`); if (this.tt_handle) { first = this.tt_handle.j1+1; last = this.tt_handle.j2; } } if (first < last) { p.fXaxis.fFirst = first; p.fXaxis.fLast = last; p.fXaxis.SetBit(EAxisBits.kAxisRange, (first !== 1) || (last !== p.fXaxis.fNbins)); } // reset statistic before display p.fEntries = 0; p.fTsumw = 0; }; if (!this.proj_hist) { switch (this.is_projection) { case 'X': this.proj_hist = createXProject(); break; case 'XY': this.proj_hist = createXProject(); this.proj_hist2 = createYProject(); break; default: this.proj_hist = createYProject(); } } if (this.is_projection === 'XY') { fillProjectHist('X', this.proj_hist); fillProjectHist('Y', this.proj_hist2); return this.drawInSpecialArea(this.proj_hist, '', 'X') .then(() => this.drawInSpecialArea(this.proj_hist2, '', 'Y')) .then(res => { delete this.doing_projection; return res; }); } fillProjectHist(this.is_projection, this.proj_hist); return this.drawInSpecialArea(this.proj_hist).then(res => { delete this.doing_projection; return res; }); } /** @summary Execute TH2 menu command * @desc Used to catch standard menu items and provide local implementation */ executeMenuCommand(method, args) { if (super.executeMenuCommand(method, args)) return true; if ((method.fName === 'SetShowProjectionX') || (method.fName === 'SetShowProjectionY')) { this.toggleProjection(method.fName[17], args && parseInt(args) ? parseInt(args) : 1); return true; } if (method.fName === 'SetShowProjectionXY') { this.toggleProjection('X' + args.replaceAll(',', '_Y')); return true; } return false; } /** @summary Fill histogram context menu */ fillHistContextMenu(menu) { if (!this.isTH2Poly() && this.getPadPainter()?.iscan) { let kind = this.is_projection || ''; if (kind) kind += this.projection_widthX; if ((this.projection_widthX !== this.projection_widthY) && (this.is_projection === 'XY')) kind = `X${this.projection_widthX}_Y${this.projection_widthY}`; const kinds = ['X1', 'X2', 'X3', 'X5', 'X10', 'Y1', 'Y2', 'Y3', 'Y5', 'Y10', 'XY1', 'XY2', 'XY3', 'XY5', 'XY10']; if (kind) kinds.unshift('Off'); menu.sub('Projections', () => menu.input('Input projection kind X1 or XY2 or X3_Y4', kind, 'string').then(val => this.toggleProjection(val))); for (let k = 0; k < kinds.length; ++k) menu.addchk(kind === kinds[k], kinds[k], kinds[k], arg => this.toggleProjection(arg)); menu.endsub(); } if (!this.isTH2Poly()) menu.add('Auto zoom-in', () => this.autoZoom()); const opts = this.getSupportedDrawOptions(); menu.addDrawMenu('Draw with', opts, arg => { if (arg.indexOf(kInspect) === 0) return this.showInspector(arg); const oldProject = this.options.Project; this.decodeOptions(arg); if ((oldProject === this.options.Project) || this.mode3d) this.interactiveRedraw('pad', 'drawopt'); else this.toggleProjection(this.options.Project); }); if (this.options.Color || this.options.Contour || this.options.Hist || this.options.Surf || this.options.Lego === 12 || this.options.Lego === 14) this.fillPaletteMenu(menu, true); } /** @summary Process click on histogram-defined buttons */ clickButton(funcname) { const res = super.clickButton(funcname); if (res) return res; if (this.isMainPainter()) { switch (funcname) { case 'ToggleColor': return this.toggleColor(); case 'Toggle3D': return this.toggleMode3D(); } } // all methods here should not be processed further return false; } /** @summary Fill pad toolbar with histogram-related functions */ fillToolbar() { super.fillToolbar(true); const pp = this.getPadPainter(); if (!pp) return; if (!this.isTH2Poly() && !this.options.Axis) pp.addPadButton('th2color', 'Toggle color', 'ToggleColor'); if (!this.options.Axis) pp.addPadButton('th2colorz', 'Toggle color palette', 'ToggleColorZ'); pp.addPadButton('th2draw3d', 'Toggle 3D mode', 'Toggle3D'); pp.showPadButtons(); } /** @summary Toggle color drawing mode */ toggleColor() { if (this.options.Mode3D) { this.options.Mode3D = false; this.options.Color = true; } else { this.options.Color = !this.options.Color; this.options.Scat = !this.options.Color; } this._can_move_colz = true; // indicate that next redraw can move Z scale this.copyOptionsToOthers(); return this.interactiveRedraw('pad', 'drawopt'); } /** @summary Perform automatic zoom inside non-zero region of histogram */ autoZoom() { if (this.isTH2Poly()) return; // not implemented const i1 = this.getSelectIndex('x', 'left', -1), i2 = this.getSelectIndex('x', 'right', 1), j1 = this.getSelectIndex('y', 'left', -1), j2 = this.getSelectIndex('y', 'right', 1), histo = this.getObject(); if ((i1 === i2) || (j1 === j2)) return; // first find minimum let min = histo.getBinContent(i1 + 1, j1 + 1); for (let i = i1; i < i2; ++i) { for (let j = j1; j < j2; ++j) min = Math.min(min, histo.getBinContent(i + 1, j + 1)); } if (min > 0) return; // if all points positive, no chance for auto-scale let ileft = i2, iright = i1, jleft = j2, jright = j1; for (let i = i1; i < i2; ++i) { for (let j = j1; j < j2; ++j) { if (histo.getBinContent(i + 1, j + 1) > min) { if (i < ileft) ileft = i; if (i >= iright) iright = i + 1; if (j < jleft) jleft = j; if (j >= jright) jright = j + 1; } } } let xmin, xmax, ymin, ymax, isany = false; if ((ileft === iright-1) && (ileft > i1+1) && (iright < i2-1)) { ileft--; iright++; } if ((jleft === jright-1) && (jleft > j1+1) && (jright < j2-1)) { jleft--; jright++; } if ((ileft > i1 || iright < i2) && (ileft < iright - 1)) { xmin = histo.fXaxis.GetBinLowEdge(ileft+1); xmax = histo.fXaxis.GetBinLowEdge(iright+1); isany = true; } if ((jleft > j1 || jright < j2) && (jleft < jright - 1)) { ymin = histo.fYaxis.GetBinLowEdge(jleft+1); ymax = histo.fYaxis.GetBinLowEdge(jright+1); isany = true; } if (isany) return this.getFramePainter().zoom(xmin, xmax, ymin, ymax); } /** @summary Scan TH2 histogram content */ scanContent(when_axis_changed) { // no need to re-scan histogram while result does not depend from axis selection if (when_axis_changed && this.nbinsx && this.nbinsy) return; const histo = this.getObject(); let i, j; this.extractAxesProperties(2); if (this.isTH2Poly()) { this.gminposbin = null; this.gminbin = this.gmaxbin = 0; for (let n = 0, len = histo.fBins.arr.length; n < len; ++n) { const bin_content = histo.fBins.arr[n].fContent; if (n === 0) this.gminbin = this.gmaxbin = bin_content; if (bin_content < this.gminbin) this.gminbin = bin_content; else if (bin_content > this.gmaxbin) this.gmaxbin = bin_content; if ((bin_content > 0) && ((this.gminposbin === null) || (this.gminposbin > bin_content))) this.gminposbin = bin_content; } } else { // global min/max, used at the moment in 3D drawing this.gminbin = this.gmaxbin = histo.getBinContent(1, 1); this.gminposbin = null; for (i = 0; i < this.nbinsx; ++i) { for (j = 0; j < this.nbinsy; ++j) { const bin_content = histo.getBinContent(i+1, j+1); if (bin_content < this.gminbin) this.gminbin = bin_content; else if (bin_content > this.gmaxbin) this.gmaxbin = bin_content; if (bin_content > 0) { if ((this.gminposbin === null) || (this.gminposbin > bin_content)) this.gminposbin = bin_content; } } } } // this value used for logz scale drawing if ((this.gminposbin === null) && (this.gmaxbin > 0)) this.gminposbin = this.gmaxbin*1e-4; let is_content = (this.gmaxbin !== 0) || (this.gminbin !== 0); // for TProfile2D show empty bin if there are entries for it if (!is_content && (histo._typename === clTProfile2D)) { for (i = 0; i < this.nbinsx && !is_content; ++i) { for (j = 0; j < this.nbinsy; ++j) { if (histo.getBinEntries(i + 1, j + 1)) { is_content = true; break; } } } } if (this.options.Axis > 0) { // Paint histogram axis only this.draw_content = false; } else if (this.isTH2Poly()) { this.draw_content = is_content || this.options.Line || this.options.Fill || this.options.Mark; if (!this.draw_content && this.options.Zero) { this.draw_content = true; this.options.Line = 1; } } else this.draw_content = is_content || this.options.ShowEmpty; } /** @summary Count TH2 histogram statistic * @desc Optionally one could provide condition function to select special range */ countStat(cond, count_skew) { if (!isFunc(cond)) cond = this.options.cutg ? (x, y) => this.options.cutg.IsInside(x, y) : null; const histo = this.getHisto(), xaxis = histo.fXaxis, yaxis = histo.fYaxis, fp = this.getFramePainter(), funcs = this.getHistGrFuncs(fp), res = { name: histo.fName, entries: 0, eff_entries: 0, integral: 0, meanx: 0, meany: 0, rmsx: 0, rmsy: 0, matrix: [0, 0, 0, 0, 0, 0, 0, 0, 0], xmax: 0, ymax: 0, wmax: null, skewx: 0, skewy: 0, skewd: 0, kurtx: 0, kurty: 0, kurtd: 0 }, has_counted_stat = !fp.isAxisZoomed('x') && !fp.isAxisZoomed('y') && (Math.abs(histo.fTsumw) > 1e-300) && !cond; let stat_sum0 = 0, stat_sumw2 = 0, stat_sumx1 = 0, stat_sumy1 = 0, stat_sumx2 = 0, stat_sumy2 = 0, xside, yside, xx, yy, zz, xleft, xright, yleft, yright; if (this.isTH2Poly()) { const len = histo.fBins.arr.length; let i, bin, n, gr, ngr, numgraphs, numpoints; for (i = 0; i < len; ++i) { bin = histo.fBins.arr[i]; xside = (bin.fXmin > funcs.scale_xmax) ? 2 : (bin.fXmax < funcs.scale_xmin ? 0 : 1); yside = (bin.fYmin > funcs.scale_ymax) ? 2 : (bin.fYmax < funcs.scale_ymin ? 0 : 1); xx = yy = numpoints = 0; gr = bin.fPoly; numgraphs = 1; if (gr._typename === clTMultiGraph) { numgraphs = bin.fPoly.fGraphs.arr.length; gr = null; } for (ngr = 0; ngr < numgraphs; ++ngr) { if (!gr || (ngr > 0)) gr = bin.fPoly.fGraphs.arr[ngr]; for (n = 0; n < gr.fNpoints; ++n) { ++numpoints; xx += gr.fX[n]; yy += gr.fY[n]; } } if (numpoints > 1) { xx /= numpoints; yy /= numpoints; } zz = bin.fContent; res.entries += zz; res.matrix[yside * 3 + xside] += zz; if ((xside !== 1) || (yside !== 1) || (cond && !cond(xx, yy))) continue; if ((res.wmax === null) || (zz > res.wmax)) { res.wmax = zz; res.xmax = xx; res.ymax = yy; } if (!has_counted_stat) { stat_sum0 += zz; stat_sumw2 += zz * zz; stat_sumx1 += xx * zz; stat_sumy1 += yy * zz; stat_sumx2 += xx * xx * zz; stat_sumy2 += yy * yy * zz; } } } else { xleft = this.getSelectIndex('x', 'left'); xright = this.getSelectIndex('x', 'right'); yleft = this.getSelectIndex('y', 'left'); yright = this.getSelectIndex('y', 'right'); for (let xi = 0; xi <= this.nbinsx + 1; ++xi) { xside = (xi <= xleft) ? 0 : (xi > xright ? 2 : 1); xx = xaxis.GetBinCoord(xi - 0.5); for (let yi = 0; yi <= this.nbinsy + 1; ++yi) { yside = (yi <= yleft) ? 0 : (yi > yright ? 2 : 1); yy = yaxis.GetBinCoord(yi - 0.5); zz = histo.getBinContent(xi, yi); res.entries += zz; res.matrix[yside * 3 + xside] += zz; if ((xside !== 1) || (yside !== 1) || (cond && !cond(xx, yy))) continue; if ((res.wmax === null) || (zz > res.wmax)) { res.wmax = zz; res.xmax = xx; res.ymax = yy; } if (!has_counted_stat) { stat_sum0 += zz; stat_sumw2 += zz * zz; stat_sumx1 += xx * zz; stat_sumy1 += yy * zz; stat_sumx2 += xx**2 * zz; stat_sumy2 += yy**2 * zz; } } } } if (has_counted_stat) { stat_sum0 = histo.fTsumw; stat_sumw2 = histo.fTsumw2; stat_sumx1 = histo.fTsumwx; stat_sumx2 = histo.fTsumwx2; stat_sumy1 = histo.fTsumwy; stat_sumy2 = histo.fTsumwy2; } if (Math.abs(stat_sum0) > 1e-300) { res.meanx = stat_sumx1 / stat_sum0; res.meany = stat_sumy1 / stat_sum0; res.rmsx = Math.sqrt(Math.abs(stat_sumx2 / stat_sum0 - res.meanx**2)); res.rmsy = Math.sqrt(Math.abs(stat_sumy2 / stat_sum0 - res.meany**2)); } if (res.wmax === null) res.wmax = 0; res.integral = stat_sum0; if (histo.fEntries > 0) res.entries = histo.fEntries; res.eff_entries = stat_sumw2 ? stat_sum0*stat_sum0/stat_sumw2 : Math.abs(stat_sum0); if (count_skew && !this.isTH2Poly()) { let sumx3 = 0, sumy3 = 0, sumx4 = 0, sumy4 = 0, np = 0, w; for (let xi = xleft; xi < xright; ++xi) { xx = xaxis.GetBinCoord(xi + 0.5); for (let yi = yleft; yi < yright; ++yi) { yy = yaxis.GetBinCoord(yi + 0.5); if (cond && !cond(xx, yy)) continue; w = histo.getBinContent(xi + 1, yi + 1); np += w; sumx3 += w * Math.pow(xx - res.meanx, 3); sumy3 += w * Math.pow(yy - res.meany, 3); sumx4 += w * Math.pow(xx - res.meanx, 4); sumy4 += w * Math.pow(yy - res.meany, 4); } } const stddev3x = Math.pow(res.rmsx, 3), stddev3y = Math.pow(res.rmsy, 3), stddev4x = Math.pow(res.rmsx, 4), stddev4y = Math.pow(res.rmsy, 4); if (np * stddev3x !== 0) res.skewx = sumx3 / (np * stddev3x); if (np * stddev3y !== 0) res.skewy = sumy3 / (np * stddev3y); res.skewd = res.eff_entries > 0 ? Math.sqrt(6/res.eff_entries) : 0; if (np * stddev4x !== 0) res.kurtx = sumx4 / (np * stddev4x) - 3; if (np * stddev4y !== 0) res.kurty = sumy4 / (np * stddev4y) - 3; res.kurtd = res.eff_entries > 0 ? Math.sqrt(24/res.eff_entries) : 0; } return res; } /** @summary Fill TH2 statistic in stat box */ fillStatistic(stat, dostat, dofit) { // no need to refill statistic if histogram is dummy if (this.isIgnoreStatsFill()) return false; if (dostat === 1) dostat = 1111; const print_name = Math.floor(dostat % 10), print_entries = Math.floor(dostat / 10) % 10, print_mean = Math.floor(dostat / 100) % 10, print_rms = Math.floor(dostat / 1000) % 10, print_under = Math.floor(dostat / 10000) % 10, print_over = Math.floor(dostat / 100000) % 10, print_integral = Math.floor(dostat / 1000000) % 10, print_skew = Math.floor(dostat / 10000000) % 10, print_kurt = Math.floor(dostat / 100000000) % 10, data = this.countStat(undefined, (print_skew > 0) || (print_kurt > 0)); stat.clearPave(); if (print_name > 0) stat.addText(data.name); if (print_entries > 0) stat.addText('Entries = ' + stat.format(data.entries, 'entries')); if (print_mean > 0) { stat.addText('Mean x = ' + stat.format(data.meanx)); stat.addText('Mean y = ' + stat.format(data.meany)); } if (print_rms > 0) { stat.addText('Std Dev x = ' + stat.format(data.rmsx)); stat.addText('Std Dev y = ' + stat.format(data.rmsy)); } if (print_integral > 0) stat.addText('Integral = ' + stat.format(data.matrix[4], 'entries')); if (print_skew === 2) { stat.addText(`Skewness x = ${stat.format(data.skewx)} #pm ${stat.format(data.skewd)}`); stat.addText(`Skewness y = ${stat.format(data.skewy)} #pm ${stat.format(data.skewd)}`); } else if (print_skew > 0) { stat.addText(`Skewness x = ${stat.format(data.skewx)}`); stat.addText(`Skewness y = ${stat.format(data.skewy)}`); } if (print_kurt === 2) { stat.addText(`Kurtosis x = ${stat.format(data.kurtx)} #pm ${stat.format(data.kurtd)}`); stat.addText(`Kurtosis y = ${stat.format(data.kurty)} #pm ${stat.format(data.kurtd)}`); } else if (print_kurt > 0) { stat.addText(`Kurtosis x = ${stat.format(data.kurtx)}`); stat.addText(`Kurtosis y = ${stat.format(data.kurty)}`); } if ((print_under > 0) || (print_over > 0)) { const get = i => data.matrix[i].toFixed(0); stat.addText(`${get(6)} | ${get(7)} | ${get(7)}`); stat.addText(`${get(3)} | ${get(4)} | ${get(5)}`); stat.addText(`${get(0)} | ${get(1)} | ${get(2)}`); } if (dofit) stat.fillFunctionStat(this.findFunction(clTF2), dofit, 2); return true; } /** @summary Draw TH2 bins as colors */ drawBinsColor() { const histo = this.getHisto(), handle = this.prepareDraw(), cntr = this.getContour(), palette = this.getHistPalette(), entries = [], has_sumw2 = histo.fSumw2?.length, show_empty = this.options.ShowEmpty, can_merge_x = (this.options.Color !== 7) || ((handle.xbar1 === 0) && (handle.xbar2 === 1)), can_merge_y = (this.options.Color !== 7) || ((handle.ybar1 === 0) && (handle.ybar2 === 1)), colindx0 = cntr.getPaletteIndex(palette, 0); let dx, dy, x1, y2, binz, is_zero, colindx, last_entry = null, skip_zero = !this.options.Zero, skip_bin; const test_cutg = this.options.cutg, flush_last_entry = () => { last_entry.path += `h${dx}v${last_entry.y1-last_entry.y2}h${-dx}z`; last_entry = null; }; // check in the beginning if zero can be skipped if (!skip_zero && !show_empty && (colindx0 === null)) skip_zero = true; // special check for TProfile2D - empty bin with no entries shown if (skip_zero && (histo?._typename === clTProfile2D)) skip_zero = 1; // now start build for (let i = handle.i1; i < handle.i2; ++i) { dx = (handle.grx[i+1] - handle.grx[i]) || 1; if (can_merge_x) x1 = handle.grx[i]; else { x1 = Math.round(handle.grx[i] + dx*handle.xbar1); dx = Math.round(dx*(handle.xbar2 - handle.xbar1)) || 1; } for (let j = handle.j2 - 1; j >= handle.j1; --j) { binz = histo.getBinContent(i + 1, j + 1); is_zero = (binz === 0) && (!has_sumw2 || histo.fSumw2[histo.getBin(i + 1, j + 1)] === 0); skip_bin = is_zero && ((skip_zero === 1) ? !histo.getBinEntries(i + 1, j + 1) : skip_zero); if (skip_bin || (test_cutg && !test_cutg.IsInside(histo.fXaxis.GetBinCoord(i + 0.5), histo.fYaxis.GetBinCoord(j + 0.5)))) { if (last_entry) flush_last_entry(); continue; } colindx = cntr.getPaletteIndex(palette, binz); if (colindx === null) { if (is_zero && (show_empty || (skip_zero === 1))) colindx = colindx0 || 0; else { if (last_entry) flush_last_entry(); continue; } } dy = (handle.gry[j] - handle.gry[j+1]) || 1; if (can_merge_y) y2 = handle.gry[j+1]; else { y2 = Math.round(handle.gry[j] - dy*handle.ybar2); dy = Math.round(dy*(handle.ybar2 - handle.ybar1)) || 1; } const cmd1 = `M${x1},${y2}`; let entry = entries[colindx]; if (!entry) entry = entries[colindx] = { path: cmd1 }; else if (can_merge_y && (entry === last_entry)) { entry.y1 = y2 + dy; continue; } else { const ddx = x1 - entry.x1, ddy = y2 - entry.y2; if (ddx || ddy) { const cmd2 = `m${ddx},${ddy}`; entry.path += (cmd2.length < cmd1.length) ? cmd2 : cmd1; } } if (last_entry) flush_last_entry(); entry.x1 = x1; entry.y2 = y2; if (can_merge_y) { entry.y1 = y2 + dy; last_entry = entry; } else entry.path += `h${dx}v${dy}h${-dx}z`; } if (last_entry) flush_last_entry(); } entries.forEach((entry, ecolindx) => { if (entry) { this.draw_g.append('svg:path') .attr('fill', palette.getColor(ecolindx)) .attr('d', entry.path); } }); return handle; } /** @summary Draw TH2 bins as colors in polar coordinates */ drawBinsPolar() { const histo = this.getHisto(), handle = this.prepareDraw(), cntr = this.getContour(), palette = this.getHistPalette(), entries = [], has_sumw2 = histo.fSumw2?.length, show_empty = this.options.ShowEmpty, colindx0 = cntr.getPaletteIndex(palette, 0); let binz, is_zero, colindx, skip_zero = !this.options.Zero, skip_bin; const test_cutg = this.options.cutg; // check in the beginning if zero can be skipped if (!skip_zero && !show_empty && (colindx0 === null)) skip_zero = true; // special check for TProfile2D - empty bin with no entries shown if (skip_zero && (histo?._typename === clTProfile2D)) skip_zero = 1; handle.getBinPath = function(i, j) { const a1 = 2 * Math.PI * Math.max(0, this.grx[i]) / this.width, a2 = 2 * Math.PI * Math.min(this.grx[i + 1], this.width) / this.width, r2 = Math.min(this.gry[j], this.height) / this.height, r1 = Math.max(0, this.gry[j + 1]) / this.height, side = a2 - a1 > Math.PI ? 1 : 0; // handle very large sector // do not process bins outside visible range if ((a2 <= a1) || (r2 <= r1)) return ''; const x0 = this.width/2, y0 = this.height/2, rx1 = r1 * this.width/2, rx2 = r2 * this.width/2, ry1 = r1 * this.height/2, ry2 = r2 * this.height/2, x11 = x0 + rx1 * Math.cos(a1), x12 = x0 + rx1 * Math.cos(a2), y11 = y0 + ry1 * Math.sin(a1), y12 = y0 + ry1 * Math.sin(a2), x