UNPKG

jsroot

Version:
1,187 lines (967 loc) 42.4 kB
import { gStyle, isStr, kNoZoom, kInspect } from '../core.mjs'; import { rgb as d3_rgb } from '../d3.mjs'; import { floatToString, TRandom, addHighlightStyle } from '../base/BasePainter.mjs'; import { RHistPainter } from './RHistPainter.mjs'; import { ensureRCanvas } from '../gpad/RCanvasPainter.mjs'; import { buildHist2dContour } from '../hist2d/TH2Painter.mjs'; /** * @summary Painter for RH2 classes * * @private */ class RH2Painter extends RHistPainter { /** @summary constructor * @param {object|string} dom - DOM element or id * @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 dimension */ getDimension() { return 2; } /** @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); } 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 this.provideSpecialDrawArea(new_proj).then(() => { this.is_projection = new_proj; return this.redrawProjection(); }); } /** @summary Redraw projections */ redrawProjection(/* ii1, ii2, jj1, jj2 */) { // do nothing for the moment // if (!this.is_projection) return; } /** @summary Execute menu command */ 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; } return false; } /** @summary Fill histogram context menu */ fillHistContextMenu(menu) { if (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(); } 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); this.decodeOptions(arg); this.interactiveRedraw('pad', 'drawopt'); }); if (this.options.Color) this.fillPaletteMenu(menu); } /** @summary Process click on histogram-defined buttons */ clickButton(funcname) { const res = super.clickButton(funcname); if (res) return res; 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 RH2-related functions */ fillToolbar() { super.fillToolbar(true); const pp = this.getPadPainter(); if (!pp) return; pp.addPadButton('th2color', 'Toggle color', 'ToggleColor'); 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; return this.redraw(); } /** @summary Perform automatic zoom inside non-zero region of histogram */ autoZoom() { 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.getHisto(), xaxis = this.getAxis('x'), yaxis = this.getAxis('y'); 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 = xaxis.GetBinCoord(ileft); xmax = xaxis.GetBinCoord(iright); isany = true; } if ((jleft > j1 || jright < j2) && (jleft < jright - 1)) { ymin = yaxis.GetBinCoord(jleft); ymax = yaxis.GetBinCoord(jright); isany = true; } if (isany) return this.getFramePainter().zoom(xmin, xmax, ymin, ymax); } /** @summary Scan content of 2-dim histogram */ 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.getHisto(); this.extractAxesProperties(2); if (this.isDisplayItem()) { // take min/max values from the display item this.gminbin = histo.fContMin; this.gminposbin = histo.fContMinPos > 0 ? histo.fContMinPos : null; this.gmaxbin = histo.fContMax; } else { // global min/max, used at the moment in 3D drawing this.gminbin = this.gmaxbin = histo.getBinContent(1, 1); this.gminposbin = null; for (let i = 0; i < this.nbinsx; ++i) { for (let 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.zmin = this.gminbin; this.zmax = this.gmaxbin; // this value used for logz scale drawing if ((this.gminposbin === null) && (this.gmaxbin > 0)) this.gminposbin = this.gmaxbin*1e-4; if (this.options.Axis > 0) // Paint histogram axis only this.draw_content = false; else this.draw_content = (this.gmaxbin !== 0) || (this.gminbin !== 0); } /** @summary Count statistic */ countStat(cond) { const histo = this.getHisto(), res = { name: 'histo', 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 }, xleft = this.getSelectIndex('x', 'left'), xright = this.getSelectIndex('x', 'right'), yleft = this.getSelectIndex('y', 'left'), yright = this.getSelectIndex('y', 'right'), xaxis = this.getAxis('x'), yaxis = this.getAxis('y'); let stat_sum0 = 0, stat_sumx1 = 0, stat_sumy1 = 0, stat_sumx2 = 0, stat_sumy2 = 0, xside, yside, xx, yy, zz, xi, yi; // TODO: account underflow/overflow bins, now stored in different array and only by histogram itself for (xi = 1; xi <= this.nbinsx; ++xi) { xside = (xi <= xleft+1) ? 0 : (xi > xright+1 ? 2 : 1); xx = xaxis.GetBinCoord(xi - 0.5); for (yi = 1; yi <= this.nbinsy; ++yi) { yside = (yi <= yleft+1) ? 0 : (yi > yright+1 ? 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)) continue; if (cond && !cond(xx, yy)) continue; if ((res.wmax === null) || (zz > res.wmax)) { res.wmax = zz; res.xmax = xx; res.ymax = yy; } stat_sum0 += zz; stat_sumx1 += xx * zz; stat_sumy1 += yy * zz; stat_sumx2 += xx**2 * zz; stat_sumy2 += yy**2 * zz; } } 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; return res; } /** @summary Fill statistic into statistic box */ fillStatistic(stat, dostat /* , dofit */) { const data = this.countStat(), 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; stat.clearStat(); 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 > 0) { stat.addText('Skewness x = <undef>'); stat.addText('Skewness y = <undef>'); } if (print_kurt > 0) stat.addText('Kurt = <undef>'); if ((print_under > 0) || (print_over > 0)) { const m = data.matrix; stat.addText(m[6].toFixed(0) + ' | ' + m[7].toFixed(0) + ' | ' + m[7].toFixed(0)); stat.addText(m[3].toFixed(0) + ' | ' + m[4].toFixed(0) + ' | ' + m[5].toFixed(0)); stat.addText(m[0].toFixed(0) + ' | ' + m[1].toFixed(0) + ' | ' + m[2].toFixed(0)); } return true; } /** @summary Draw histogram bins as color */ drawBinsColor() { const histo = this.getHisto(), handle = this.prepareDraw(), di = handle.stepi, dj = handle.stepj, entries = [], can_merge = true; let colindx, cmd1, cmd2, i, j, binz, dx, dy, entry, last_entry; const flush_last_entry = () => { last_entry.path += `h${dx}v${last_entry.y2-last_entry.y}h${-dx}z`; last_entry.dy = 0; last_entry = null; }; // now start build for (i = handle.i1; i < handle.i2; i += di) { dx = (handle.grx[i+di] - handle.grx[i]) || 1; for (j = handle.j1; j < handle.j2; j += dj) { binz = histo.getBinContent(i+1, j+1); colindx = handle.palette.getContourIndex(binz); if (binz === 0) { if (!this.options.Zero) colindx = null; else if ((colindx === null) && this._show_empty_bins) colindx = 0; } if (colindx === null) { if (last_entry) flush_last_entry(); continue; } cmd1 = `M${handle.grx[i]},${handle.gry[j]}`; dy = (handle.gry[j+dj] - handle.gry[j]) || -1; entry = entries[colindx]; if (entry === undefined) entry = entries[colindx] = { path: cmd1 }; else if (can_merge && (entry === last_entry)) { entry.y2 = handle.gry[j] + dy; continue; } else { cmd2 = `m${handle.grx[i]-entry.x},${handle.gry[j]-entry.y}`; entry.path += (cmd2.length < cmd1.length) ? cmd2 : cmd1; } if (last_entry) flush_last_entry(); entry.x = handle.grx[i]; entry.y = handle.gry[j]; if (can_merge) { entry.y2 = handle.gry[j] + dy; last_entry = entry; } else entry.path += `h${dx}v${dy}h${-dx}z`; } if (last_entry) flush_last_entry(); } entries.forEach((entry2, ecolindx) => { if (entry2) { this.draw_g .append('svg:path') .attr('d', entry2.path) .style('fill', handle.palette.getColor(ecolindx)); } }); this.updatePaletteDraw(); return handle; } /** @summary Draw histogram bins as contour */ drawBinsContour(funcs, frame_w, frame_h) { const handle = this.prepareDraw({ rounding: false, extra: 100 }), main = this.getFramePainter(), palette = main.getHistPalette(), levels = palette.getContour(), func = main.getProjectionFunc(), BuildPath = (xp, yp, iminus, iplus, do_close) => { let cmd = '', last, pnt, first, isany; for (let i = iminus; i <= iplus; ++i) { if (func) { pnt = func(xp[i], yp[i]); pnt.x = Math.round(funcs.grx(pnt.x)); pnt.y = Math.round(funcs.gry(pnt.y)); } else pnt = { x: Math.round(xp[i]), y: Math.round(yp[i]) }; if (!cmd) { cmd = `M${pnt.x},${pnt.y}`; first = pnt; } else if ((i === iplus) && first && (pnt.x === first.x) && (pnt.y === first.y)) { if (!isany) return ''; // all same points cmd += 'z'; do_close = false; } else if ((pnt.x !== last.x) && (pnt.y !== last.y)) { cmd += `l${pnt.x - last.x},${pnt.y - last.y}`; isany = true; } else if (pnt.x !== last.x) { cmd += `h${pnt.x - last.x}`; isany = true; } else if (pnt.y !== last.y) { cmd += `v${pnt.y - last.y}`; isany = true; } last = pnt; } if (do_close) cmd += 'z'; return cmd; }; 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.getColor(0)); } buildHist2dContour(this.getHisto(), handle, levels, palette, (colindx, xp, yp, iminus, iplus) => { 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: (colindx%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'); if (!dd) return; const elem = this.draw_g .append('svg:path') .attr('d', dd) .style('fill', fillcolor); if (lineatt) elem.call(lineatt.func); } ); handle.hide_only_zeros = true; // text drawing suppress only zeros return handle; } /** @summary Create poly bin */ createPolyBin() { // see how TH2Painter is implemented return ''; } /** @summary Draw RH2 bins as text */ async drawBinsText(handle) { if (!handle) handle = this.prepareDraw({ rounding: false }); const histo = this.getHisto(), textFont = this.v7EvalFont('text', { size: 20, color: 'black', align: 22 }), text_offset = this.options.BarOffset || 0, text_g = this.draw_g.append('svg:g').attr('class', 'th2_text'), di = handle.stepi, dj = handle.stepj, profile2d = false; return this.startTextDrawingAsync(textFont, 'font', text_g).then(() => { for (let i = handle.i1; i < handle.i2; i += di) { for (let j = handle.j1; j < handle.j2; j += dj) { let binz = histo.getBinContent(i+1, j+1); if ((binz === 0) && !this._show_empty_bins) continue; const binw = handle.grx[i+di] - handle.grx[i], binh = handle.gry[j] - handle.gry[j+dj]; if (profile2d) binz = histo.getBinEntries(i+1, j+1); const text = (binz === Math.round(binz)) ? binz.toString() : floatToString(binz, gStyle.fPaintTextFormat); let x, y, width, height; if (textFont.angle) { x = Math.round(handle.grx[i] + binw*0.5); y = Math.round(handle.gry[j+dj] + binh*(0.5 + text_offset)); width = height = 0; } else { x = Math.round(handle.grx[i] + binw*0.1); y = Math.round(handle.gry[j+dj] + 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, text, latex: 0, draw_g: text_g }); } } handle.hide_only_zeros = true; // text drawing suppress only zeros return this.finishTextDrawing(text_g, true); }).then(() => handle); } /** @summary Draw RH2 bins as arrows */ drawBinsArrow() { const histo = this.getHisto(), handle = this.prepareDraw({ rounding: false }), scale_x = (handle.grx[handle.i2] - handle.grx[handle.i1])/(handle.i2 - handle.i1 + 1-0.03)/2, scale_y = (handle.gry[handle.j2] - handle.gry[handle.j1])/(handle.j2 - handle.j1 + 1-0.03)/2, di = handle.stepi, dj = handle.stepj, makeLine = (dx, dy) => dx ? (dy ? `l${dx},${dy}` : `h${dx}`) : (dy ? `v${dy}` : ''); let cmd = '', i, j, dn = 1e-30, dx, dy, xc, yc, dxn, dyn, x1, x2, y1, y2, anr, si, co; for (let loop = 0; loop < 2; ++loop) { for (i = handle.i1; i < handle.i2; i += di) { for (j = handle.j1; j < handle.j2; j += dj) { if (i === handle.i1) dx = histo.getBinContent(i+1+di, j+1) - histo.getBinContent(i+1, j+1); else if (i >= handle.i2-di) dx = histo.getBinContent(i+1, j+1) - histo.getBinContent(i+1-di, j+1); else dx = 0.5*(histo.getBinContent(i+1+di, j+1) - histo.getBinContent(i+1-di, j+1)); if (j === handle.j1) dy = histo.getBinContent(i+1, j+1+dj) - histo.getBinContent(i+1, j+1); else if (j >= handle.j2-dj) dy = histo.getBinContent(i+1, j+1) - histo.getBinContent(i+1, j+1-dj); else dy = 0.5*(histo.getBinContent(i+1, j+1+dj) - histo.getBinContent(i+1, j+1-dj)); if (loop === 0) dn = Math.max(dn, Math.abs(dx), Math.abs(dy)); else { xc = (handle.grx[i] + handle.grx[i+di])/2; yc = (handle.gry[j] + handle.gry[j+dj])/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 !== 0) || (dy !== 0)) { cmd += 'M'+Math.round(x1)+','+Math.round(y1) + makeLine(dx, dy); if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { anr = Math.sqrt(2/(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 RH2 bins as boxes */ drawBinsBox() { const histo = this.getHisto(), handle = this.prepareDraw({ rounding: false }), main = this.getFramePainter(); if (main.maxbin === main.minbin) { main.maxbin = this.gmaxbin; main.minbin = this.gminbin; main.minposbin = this.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), di = handle.stepi, dj = handle.stepj; let i, j, binz, absz, res = '', cross = '', btn1 = '', btn2 = '', zdiff, dgrx, dgry, xx, yy, ww, hh, xyfactor, uselogz = false, logmin = 0; if (main.logz && (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 += di) { for (j = handle.j1; j < handle.j2; j += dj) { binz = histo.getBinContent(i + 1, j + 1); absz = Math.abs(binz); if ((absz === 0) || (absz < absmin)) 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+di] - handle.grx[i]; hh = handle.gry[j] - handle.gry[j+dj]; dgrx = zdiff * ww; dgry = zdiff * hh; xx = Math.round(handle.grx[i] + dgrx); yy = Math.round(handle.gry[j+dj] + 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}M${xx+ww},${yy}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`; btn2 += (binz < 0) ? side1 : side2; btn1 += (binz < 0) ? side2 : side1; } } } 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); } return handle; } /** @summary Draw RH2 bins as scatter plot */ drawBinsScatter() { const histo = this.getHisto(), handle = this.prepareDraw({ rounding: true, pixel_density: true, scatter_plot: true }), colPaths = [], currx = [], curry = [], cell_w = [], cell_h = [], scale = this.options.ScatCoef * ((this.gmaxbin) > 2000 ? 2000 / this.gmaxbin : 1), di = handle.stepi, dj = handle.stepj, rnd = new TRandom(handle.sumz); let colindx, cmd1, cmd2, i, j, binz, cw, ch, factor = 1; if (scale*handle.sumz < 1e5) { // one can use direct drawing of scatter plot without any patterns this.createv7AttMarker(); this.markeratt.resetPos(); let path = '', k, npix; for (i = handle.i1; i < handle.i2; i += di) { cw = handle.grx[i+di] - handle.grx[i]; for (j = handle.j1; j < handle.j2; j += dj) { ch = handle.gry[j] - handle.gry[j+dj]; binz = histo.getBinContent(i + 1, j + 1); npix = Math.round(scale*binz); if (npix <= 0) continue; for (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; // now start build for (i = handle.i1; i < handle.i2; i += di) { for (j = handle.j1; j < handle.j2; j += dj) { binz = histo.getBinContent(i + 1, j + 1); if ((binz <= 0) || (binz < this.minbin)) continue; cw = handle.grx[i+di] - handle.grx[i]; ch = handle.gry[j] - handle.gry[j+dj]; if (cw*ch <= 0) continue; colindx = handle.palette.getContourIndex(binz/cw/ch); if (colindx < 0) continue; cmd1 = `M${handle.grx[i]},${handle.gry[j+dj]}`; 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+dj]-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+dj]; colPaths[colindx] += `v${ch}h${cw}v${-ch}z`; } } const layer = this.getFrameSvg().selectChild('.main_layer'); let defs = layer.selectChild('def'); if (defs.empty() && (colPaths.length > 0)) defs = layer.insert('svg:defs', ':first-child'); this.createv7AttMarker(); const cntr = handle.palette.getContour(); for (colindx = 0; colindx < colPaths.length; ++colindx) { if ((colPaths[colindx] !== undefined) && (colindx<cntr.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[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 RH2 bins in 2D mode */ async draw2DBins() { if (!this.draw_content) { this.removeG(); return false; } this.createHistDrawAttributes(); this.createG(true); const pmain = this.getFramePainter(), rect = pmain.getFrameRect(), funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y); let handle = null, pr = null; if (this.options.Scat) handle = this.drawBinsScatter(); else 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.Contour > 0) handle = this.drawBinsContour(funcs, rect.width, rect.height); if (this.options.Text) pr = this.drawBinsText(handle); if (!handle && !pr) handle = this.drawBinsColor(); if (!pr) pr = Promise.resolve(handle); return pr.then(h => { this.tt_handle = h; return this; }); } /** @summary Provide text information (tooltips) for histogram bin */ getBinTooltips(i, j) { const lines = [], histo = this.getHisto(); let binz = histo.getBinContent(i+1, j+1), di = 1, dj = 1; if (this.isDisplayItem()) { di = histo.stepx || 1; dj = histo.stepy || 1; } lines.push(this.getObjectHint() || 'histo<2>', 'x = ' + this.getAxisBinTip('x', i, di), 'y = ' + this.getAxisBinTip('y', j, dj), `bin = ${i+1}, ${j+1}`); if (histo.$baseh) binz -= histo.$baseh.getBinContent(i+1, j+1); const lbl = 'entries = ' + ((di > 1) || (dj > 1) ? '~' : ''); if (binz === Math.round(binz)) lines.push(lbl + binz); else lines.push(lbl + floatToString(binz, gStyle.fStatFormat)); return lines; } /** @summary Provide text information (tooltips) for poly bin */ getPolyBinTooltips() { // see how TH2Painter is implemented return []; } /** @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 - see TH2Painter return null; } let i, j, binz = 0, colindx = null; // search bins position for (i = h.i1; i < h.i2; ++i) if ((pnt.x>=h.grx[i]) && (pnt.x<=h.grx[i+1])) break; 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)) { binz = histo.getBinContent(i+1, j+1); if (this.is_projection) colindx = 0; // just to avoid hide else if (h.hide_only_zeros) colindx = (binz === 0) && !this._show_empty_bins ? null : 0; else { colindx = h.palette.getContourIndex(binz); if ((colindx === null) && (binz === 0) && this._show_empty_bins) colindx = 0; } } if (colindx === null) { ttrect.remove(); return null; } const res = { name: 'histo', title: histo.fTitle || 'title', 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 = h.palette.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); } const pmain = this.getFramePainter(); let 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], 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 = pmain.getFrameWidth(); y1 = h.gry[j2]; y2 = h.gry[j1]; binid = j1*777 + j2*333; } else if (this.is_projection === 'Y') { y1 = 0; y2 = pmain.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${pmain.getFrameWidth()}V${y2}H${x2}V${pmain.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', 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 (axis === 'z') return true; const obj = this.getAxis(axis); return obj.FindBin(max, 0.5) - obj.FindBin(min, 0) > 1; } /** @summary Performs 2D drawing of histogram * @return {Promise} when ready */ async draw2D(reason) { this.clear3DScene(); return this.drawFrameAxes().then(res => { return res ? this.drawingBins(reason) : false; }).then(res => { if (res) return this.draw2DBins().then(() => this.addInteractivity()); }).then(() => this); } /** @summary Performs 3D drawing of histogram * @return {Promise} when ready */ async draw3D(reason) { console.log('3D drawing is disabled, load ./hist/RH1Painter.mjs'); return this.draw2D(reason); } /** @summary Call drawing function depending from 3D mode */ async callDrawFunc(reason) { const main = this.getFramePainter(); if (main && (main.mode3d !== this.options.Mode3D) && !this.isMainPainter()) this.options.Mode3D = main.mode3d; return this.options.Mode3D ? this.draw3D(reason) : this.draw2D(reason); } /** @summary Redraw histogram */ async redraw(reason) { return this.callDrawFunc(reason); } /** @summary Draw histogram using painter instance * @private */ static async _draw(painter /* , opt */) { return ensureRCanvas(painter).then(() => { painter.setAsMainPainter(); painter.options = { Hist: false, Error: false, Zero: false, Mark: false, Line: false, Fill: false, Lego: 0, Surf: 0, Text: true, TextAngle: 0, TextKind: '', BaseLine: false, Mode3D: false, AutoColor: 0, Color: false, Scat: false, ScatCoef: 1, Box: false, BoxStyle: 0, Arrow: false, Contour: 0, Proj: 0, BarOffset: 0, BarWidth: 1, minimum: kNoZoom, maximum: kNoZoom, FrontBox: false, BackBox: false }; const kind = painter.v7EvalAttr('kind', ''), sub = painter.v7EvalAttr('sub', 0), o = painter.options; o.Text = painter.v7EvalAttr('drawtext', false); switch (kind) { case 'lego': o.Lego = sub > 0 ? 10+sub : 12; o.Mode3D = true; break; case 'surf': o.Surf = sub > 0 ? 10+sub : 1; o.Mode3D = true; break; case 'box': o.Box = true; o.BoxStyle = 10 + sub; break; case 'err': o.Error = true; o.Mode3D = true; break; case 'cont': o.Contour = sub > 0 ? 10+sub : 1; break; case 'arr': o.Arrow = true; break; case 'scat': o.Scat = true; break; case 'col': o.Color = true; break; default: if (!o.Text) o.Color = true; } // here we deciding how histogram will look like and how will be shown // painter.decodeOptions(opt); painter._show_empty_bins = false; painter.scanContent(); return painter.callDrawFunc(); }); } /** @summary draw RH2 object */ static async draw(dom, obj, opt) { // create painter and add it to canvas return RH2Painter._draw(new RH2Painter(dom, obj), opt); } } // class RH2Painter export { RH2Painter };