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 };