hist2d/RH1Painter.mjs

  1. import { gStyle, settings, constants, kInspect } from '../core.mjs';
  2. import { rgb as d3_rgb } from '../d3.mjs';
  3. import { floatToString, DrawOptions, buildSvgCurve, addHighlightStyle } from '../base/BasePainter.mjs';
  4. import { RHistPainter } from './RHistPainter.mjs';
  5. import { ensureRCanvas } from '../gpad/RCanvasPainter.mjs';
  6. /**
  7. * @summary Painter for RH1 classes
  8. *
  9. * @private
  10. */
  11. class RH1Painter extends RHistPainter {
  12. /** @summary Constructor
  13. * @param {object|string} dom - DOM element or id
  14. * @param {object} histo - histogram object */
  15. constructor(dom, histo) {
  16. super(dom, histo);
  17. this.wheel_zoomy = false;
  18. }
  19. /** @summary Scan content */
  20. scanContent(when_axis_changed) {
  21. // if when_axis_changed === true specified, content will be scanned after axis zoom changed
  22. const histo = this.getHisto();
  23. if (!histo) return;
  24. if (!this.nbinsx && when_axis_changed) when_axis_changed = false;
  25. if (!when_axis_changed)
  26. this.extractAxesProperties(1);
  27. let hmin = 0, hmin_nz = 0, hmax = 0, hsum = 0;
  28. if (this.isDisplayItem()) {
  29. // take min/max values from the display item
  30. hmin = histo.fContMin;
  31. hmin_nz = histo.fContMinPos;
  32. hmax = histo.fContMax;
  33. hsum = hmax;
  34. } else {
  35. const left = this.getSelectIndex('x', 'left'),
  36. right = this.getSelectIndex('x', 'right');
  37. if (when_axis_changed)
  38. if ((left === this.scan_xleft) && (right === this.scan_xright)) return;
  39. this.scan_xleft = left;
  40. this.scan_xright = right;
  41. let first = true, value, err;
  42. for (let i = 0; i < this.nbinsx; ++i) {
  43. value = histo.getBinContent(i+1);
  44. hsum += value;
  45. if ((i<left) || (i>=right)) continue;
  46. if (value > 0)
  47. if ((hmin_nz === 0) || (value<hmin_nz)) hmin_nz = value;
  48. if (first) {
  49. hmin = hmax = value;
  50. first = false;
  51. }
  52. err = 0;
  53. hmin = Math.min(hmin, value - err);
  54. hmax = Math.max(hmax, value + err);
  55. }
  56. }
  57. this.stat_entries = hsum;
  58. this.hmin = hmin;
  59. this.hmax = hmax;
  60. this.ymin_nz = hmin_nz; // value can be used to show optimal log scale
  61. if ((this.nbinsx === 0) || ((Math.abs(hmin) < 1e-300) && (Math.abs(hmax) < 1e-300)))
  62. this.draw_content = false;
  63. else
  64. this.draw_content = true;
  65. if (this.draw_content) {
  66. if (hmin >= hmax) {
  67. if (hmin === 0) {
  68. this.ymin = 0;
  69. this.ymax = 1;
  70. } else if (hmin < 0) {
  71. this.ymin = 2 * hmin;
  72. this.ymax = 0;
  73. } else {
  74. this.ymin = 0;
  75. this.ymax = hmin * 2;
  76. }
  77. } else {
  78. const dy = (hmax - hmin) * 0.05;
  79. this.ymin = hmin - dy;
  80. if ((this.ymin < 0) && (hmin >= 0)) this.ymin = 0;
  81. this.ymax = hmax + dy;
  82. }
  83. }
  84. }
  85. /** @summary Count statistic */
  86. countStat(cond) {
  87. const histo = this.getHisto(), xaxis = this.getAxis('x'),
  88. left = this.getSelectIndex('x', 'left'),
  89. right = this.getSelectIndex('x', 'right'),
  90. stat_sumwy = 0, stat_sumwy2 = 0,
  91. res = { name: 'histo', meanx: 0, meany: 0, rmsx: 0, rmsy: 0, integral: 0, entries: this.stat_entries, xmax: 0, wmax: 0 };
  92. let stat_sumw = 0, stat_sumwx = 0, stat_sumwx2 = 0,
  93. i, xmax = null, wmax = null;
  94. for (i = left; i < right; ++i) {
  95. const xx = xaxis.GetBinCoord(i+0.5);
  96. if (cond && !cond(xx)) continue;
  97. const w = histo.getBinContent(i + 1);
  98. if ((xmax === null) || (w > wmax)) { xmax = xx; wmax = w; }
  99. stat_sumw += w;
  100. stat_sumwx += w * xx;
  101. stat_sumwx2 += w * xx**2;
  102. }
  103. res.integral = stat_sumw;
  104. if (Math.abs(stat_sumw) > 1e-300) {
  105. res.meanx = stat_sumwx / stat_sumw;
  106. res.meany = stat_sumwy / stat_sumw;
  107. res.rmsx = Math.sqrt(Math.abs(stat_sumwx2 / stat_sumw - res.meanx**2));
  108. res.rmsy = Math.sqrt(Math.abs(stat_sumwy2 / stat_sumw - res.meany**2));
  109. }
  110. if (xmax !== null) {
  111. res.xmax = xmax;
  112. res.wmax = wmax;
  113. }
  114. return res;
  115. }
  116. /** @summary Fill statistic */
  117. fillStatistic(stat, dostat /* , dofit */) {
  118. const histo = this.getHisto(),
  119. data = this.countStat(),
  120. print_name = dostat % 10,
  121. print_entries = Math.floor(dostat / 10) % 10,
  122. print_mean = Math.floor(dostat / 100) % 10,
  123. print_rms = Math.floor(dostat / 1000) % 10,
  124. print_under = Math.floor(dostat / 10000) % 10,
  125. print_over = Math.floor(dostat / 100000) % 10,
  126. print_integral = Math.floor(dostat / 1000000) % 10,
  127. print_skew = Math.floor(dostat / 10000000) % 10,
  128. print_kurt = Math.floor(dostat / 100000000) % 10;
  129. // make empty at the beginning
  130. stat.clearStat();
  131. if (print_name > 0)
  132. stat.addText(data.name);
  133. if (print_entries > 0)
  134. stat.addText('Entries = ' + stat.format(data.entries, 'entries'));
  135. if (print_mean > 0)
  136. stat.addText('Mean = ' + stat.format(data.meanx));
  137. if (print_rms > 0)
  138. stat.addText('Std Dev = ' + stat.format(data.rmsx));
  139. if (print_under > 0)
  140. stat.addText('Underflow = ' + stat.format(histo.getBinContent(0), 'entries'));
  141. if (print_over > 0)
  142. stat.addText('Overflow = ' + stat.format(histo.getBinContent(this.nbinsx+1), 'entries'));
  143. if (print_integral > 0)
  144. stat.addText('Integral = ' + stat.format(data.integral, 'entries'));
  145. if (print_skew > 0)
  146. stat.addText('Skew = <not avail>');
  147. if (print_kurt > 0)
  148. stat.addText('Kurt = <not avail>');
  149. return true;
  150. }
  151. /** @summary Get baseline for bar drawings
  152. * @private */
  153. getBarBaseline(funcs, height) {
  154. let gry = funcs.swap_xy ? 0 : height;
  155. if (Number.isFinite(this.options.BaseLine) && (this.options.BaseLine >= funcs.scale_ymin))
  156. gry = Math.round(funcs.gry(this.options.BaseLine));
  157. return gry;
  158. }
  159. /** @summary Draw histogram as bars */
  160. async drawBars(handle, funcs, width, height) {
  161. this.createG(true);
  162. const left = handle.i1, right = handle.i2, di = handle.stepi,
  163. pmain = this.getFramePainter(),
  164. histo = this.getHisto(), xaxis = this.getAxis('x');
  165. let i, x1, x2, grx1, grx2, y, gry1, w,
  166. bars = '', barsl = '', barsr = '';
  167. const gry2 = this.getBarBaseline(funcs, height);
  168. for (i = left; i < right; i += di) {
  169. x1 = xaxis.GetBinCoord(i);
  170. x2 = xaxis.GetBinCoord(i+di);
  171. if (funcs.logx && (x2 <= 0)) continue;
  172. grx1 = Math.round(funcs.grx(x1));
  173. grx2 = Math.round(funcs.grx(x2));
  174. y = histo.getBinContent(i+1);
  175. if (funcs.logy && (y < funcs.scale_ymin)) continue;
  176. gry1 = Math.round(funcs.gry(y));
  177. w = grx2 - grx1;
  178. grx1 += Math.round(this.options.BarOffset*w);
  179. w = Math.round(this.options.BarWidth*w);
  180. if (pmain.swap_xy)
  181. bars += `M${gry2},${grx1}h${gry1-gry2}v${w}h${gry2-gry1}z`;
  182. else
  183. bars += `M${grx1},${gry1}h${w}v${gry2-gry1}h${-w}z`;
  184. if (this.options.BarStyle > 0) {
  185. grx2 = grx1 + w;
  186. w = Math.round(w / 10);
  187. if (pmain.swap_xy) {
  188. barsl += `M${gry2},${grx1}h${gry1-gry2}v${w}h${gry2-gry1}z`;
  189. barsr += `M${gry2},${grx2}h${gry1-gry2}v${-w}h${gry2-gry1}z`;
  190. } else {
  191. barsl += `M${grx1},${gry1}h${w}v${gry2-gry1}h${-w}z`;
  192. barsr += `M${grx2},${gry1}h${-w}v${gry2-gry1}h${w}z`;
  193. }
  194. }
  195. }
  196. if (this.fillatt.empty()) this.fillatt.setSolidColor('blue');
  197. if (bars) {
  198. this.draw_g.append('svg:path')
  199. .attr('d', bars)
  200. .call(this.fillatt.func);
  201. }
  202. if (barsl) {
  203. this.draw_g.append('svg:path')
  204. .attr('d', barsl)
  205. .call(this.fillatt.func)
  206. .style('fill', d3_rgb(this.fillatt.color).brighter(0.5).formatRgb());
  207. }
  208. if (barsr) {
  209. this.draw_g.append('svg:path')
  210. .attr('d', barsr)
  211. .call(this.fillatt.func)
  212. .style('fill', d3_rgb(this.fillatt.color).darker(0.5).formatRgb());
  213. }
  214. return true;
  215. }
  216. /** @summary Draw histogram as filled errors */
  217. async drawFilledErrors(handle, funcs /* , width, height */) {
  218. this.createG(true);
  219. const left = handle.i1, right = handle.i2, di = handle.stepi,
  220. histo = this.getHisto(), xaxis = this.getAxis('x'),
  221. bins1 = [], bins2 = [];
  222. let i, x, grx, y, yerr, gry;
  223. for (i = left; i < right; i += di) {
  224. x = xaxis.GetBinCoord(i+0.5);
  225. if (funcs.logx && (x <= 0)) continue;
  226. grx = Math.round(funcs.grx(x));
  227. y = histo.getBinContent(i+1);
  228. yerr = histo.getBinError(i+1);
  229. if (funcs.logy && (y-yerr < funcs.scale_ymin)) continue;
  230. gry = Math.round(funcs.gry(y + yerr));
  231. bins1.push({ grx, gry });
  232. gry = Math.round(funcs.gry(y - yerr));
  233. bins2.unshift({ grx, gry });
  234. }
  235. const path1 = buildSvgCurve(bins1, { line: this.options.ErrorKind !== 4 }),
  236. path2 = buildSvgCurve(bins2, { line: this.options.ErrorKind !== 4, cmd: 'L' });
  237. if (this.fillatt.empty()) this.fillatt.setSolidColor('blue');
  238. this.draw_g.append('svg:path')
  239. .attr('d', path1 + path2 + 'Z')
  240. .call(this.fillatt.func);
  241. return true;
  242. }
  243. /** @summary Draw 1D histogram as SVG */
  244. async draw1DBins() {
  245. const pmain = this.getFramePainter(),
  246. rect = pmain.getFrameRect();
  247. if (!this.draw_content || (rect.width <= 0) || (rect.height <= 0)) {
  248. this.removeG();
  249. return false;
  250. }
  251. this.createHistDrawAttributes();
  252. const handle = this.prepareDraw({ extra: 1, only_indexes: true }),
  253. funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y);
  254. if (this.options.Bar)
  255. return this.drawBars(handle, funcs, rect.width, rect.height);
  256. if ((this.options.ErrorKind === 3) || (this.options.ErrorKind === 4))
  257. return this.drawFilledErrors(handle, funcs, rect.width, rect.height);
  258. return this.drawHistBins(handle, funcs, rect.width, rect.height);
  259. }
  260. /** @summary Draw histogram bins */
  261. async drawHistBins(handle, funcs, width, height) {
  262. this.createG(true);
  263. const options = this.options,
  264. left = handle.i1,
  265. right = handle.i2,
  266. di = handle.stepi,
  267. histo = this.getHisto(),
  268. want_tooltip = !this.isBatchMode() && settings.Tooltip,
  269. xaxis = this.getAxis('x'),
  270. exclude_zero = !options.Zero,
  271. show_errors = options.Error,
  272. show_line = options.Line,
  273. show_text = options.Text;
  274. let show_markers = options.Mark,
  275. res = '', lastbin = false,
  276. startx, currx, curry, x, grx, y, gry, curry_min, curry_max, prevy, prevx, i, bestimin, bestimax,
  277. path_fill = null, path_err = null, path_marker = null, path_line = null,
  278. hints_err = null,
  279. endx = '', endy = '', dend = 0, my, yerr1, yerr2, bincont, binerr, mx1, mx2, midx,
  280. text_font, pr = Promise.resolve();
  281. if (show_errors && !show_markers && (this.v7EvalAttr('marker_style', 1) > 1))
  282. show_markers = true;
  283. if (options.ErrorKind === 2) {
  284. if (this.fillatt.empty()) show_markers = true;
  285. else path_fill = '';
  286. } else if (options.Error) {
  287. path_err = '';
  288. hints_err = want_tooltip ? '' : null;
  289. }
  290. if (show_line) path_line = '';
  291. if (show_markers) {
  292. // draw markers also when e2 option was specified
  293. this.createv7AttMarker();
  294. if (this.markeratt.size > 0) {
  295. // simply use relative move from point, can optimize in the future
  296. path_marker = '';
  297. this.markeratt.resetPos();
  298. } else
  299. show_markers = false;
  300. }
  301. if (show_text) {
  302. text_font = this.v7EvalFont('text', { size: 20, color: 'black', align: 22 });
  303. if (!text_font.angle && !options.TextKind) {
  304. const space = width / (right - left + 1);
  305. if (space < 3 * text_font.size) {
  306. text_font.setAngle(270);
  307. text_font.setSize(Math.round(space*0.7));
  308. }
  309. }
  310. pr = this.startTextDrawingAsync(text_font, 'font');
  311. }
  312. return pr.then(() => {
  313. // if there are too many points, exclude many vertical drawings at the same X position
  314. // instead define min and max value and made min-max drawing
  315. let use_minmax = ((right-left) > 3*width);
  316. if (options.ErrorKind === 1) {
  317. const lw = this.lineatt.width + gStyle.fEndErrorSize;
  318. endx = `m0,${lw}v${-2*lw}m0,${lw}`;
  319. endy = `m${lw},0h${-2*lw}m${lw},0`;
  320. dend = Math.floor((this.lineatt.width-1)/2);
  321. }
  322. const draw_markers = show_errors || show_markers;
  323. if (draw_markers || show_text || show_line) use_minmax = true;
  324. const draw_bin = besti => {
  325. bincont = histo.getBinContent(besti+1);
  326. if (!exclude_zero || (bincont !== 0)) {
  327. mx1 = Math.round(funcs.grx(xaxis.GetBinCoord(besti)));
  328. mx2 = Math.round(funcs.grx(xaxis.GetBinCoord(besti+di)));
  329. midx = Math.round((mx1+mx2)/2);
  330. my = Math.round(funcs.gry(bincont));
  331. yerr1 = yerr2 = 20;
  332. if (show_errors) {
  333. binerr = histo.getBinError(besti+1);
  334. yerr1 = Math.round(my - funcs.gry(bincont + binerr)); // up
  335. yerr2 = Math.round(funcs.gry(bincont - binerr) - my); // down
  336. }
  337. if (show_text && (bincont !== 0)) {
  338. const lbl = (bincont === Math.round(bincont)) ? bincont.toString() : floatToString(bincont, gStyle.fPaintTextFormat);
  339. if (text_font.angle)
  340. this.drawText({ align: 12, x: midx, y: Math.round(my - 2 - text_font.size / 5), text: lbl, latex: 0 });
  341. else
  342. this.drawText({ x: Math.round(mx1 + (mx2 - mx1) * 0.1), y: Math.round(my - 2 - text_font.size), width: Math.round((mx2 - mx1) * 0.8), height: text_font.size, text: lbl, latex: 0 });
  343. }
  344. if (show_line && (path_line !== null))
  345. path_line += ((path_line.length === 0) ? 'M' : 'L') + midx + ',' + my;
  346. if (draw_markers) {
  347. if ((my >= -yerr1) && (my <= height + yerr2)) {
  348. if (path_fill !== null)
  349. path_fill += `M${mx1},${my-yerr1}h${mx2-mx1}v${yerr1+yerr2+1}h${mx1-mx2}z`;
  350. if (path_marker !== null)
  351. path_marker += this.markeratt.create(midx, my);
  352. if (path_err !== null) {
  353. let edx = 5;
  354. if (this.options.errorX > 0) {
  355. edx = Math.round((mx2-mx1)*this.options.errorX);
  356. const mmx1 = midx - edx, mmx2 = midx + edx;
  357. path_err += `M${mmx1+dend},${my}${endx}h${mmx2-mmx1-2*dend}${endx}`;
  358. }
  359. path_err += `M${midx},${my-yerr1+dend}${endy}v${yerr1+yerr2-2*dend}${endy}`;
  360. if (hints_err !== null)
  361. hints_err += `M${midx-edx},${my-yerr1}h${2*edx}v${yerr1+yerr2}h${-2*edx}z`;
  362. }
  363. }
  364. }
  365. }
  366. };
  367. for (i = left; i <= right; i += di) {
  368. x = xaxis.GetBinCoord(i);
  369. if (funcs.logx && (x <= 0)) continue;
  370. grx = Math.round(funcs.grx(x));
  371. lastbin = (i > right - di);
  372. if (lastbin && (left < right))
  373. gry = curry;
  374. else {
  375. y = histo.getBinContent(i+1);
  376. gry = Math.round(funcs.gry(y));
  377. }
  378. if (res.length === 0) {
  379. bestimin = bestimax = i;
  380. prevx = startx = currx = grx;
  381. prevy = curry_min = curry_max = curry = gry;
  382. res = 'M'+currx+','+curry;
  383. } else
  384. if (use_minmax) {
  385. if ((grx === currx) && !lastbin) {
  386. if (gry < curry_min) bestimax = i; else
  387. if (gry > curry_max) bestimin = i;
  388. curry_min = Math.min(curry_min, gry);
  389. curry_max = Math.max(curry_max, gry);
  390. curry = gry;
  391. } else {
  392. if (draw_markers || show_text || show_line) {
  393. if (bestimin === bestimax) draw_bin(bestimin); else
  394. if (bestimin < bestimax) { draw_bin(bestimin); draw_bin(bestimax); } else {
  395. draw_bin(bestimax); draw_bin(bestimin);
  396. }
  397. }
  398. // when several points as same X differs, need complete logic
  399. if (!draw_markers && ((curry_min !== curry_max) || (prevy !== curry_min))) {
  400. if (prevx !== currx)
  401. res += 'h'+(currx-prevx);
  402. if (curry === curry_min) {
  403. if (curry_max !== prevy)
  404. res += 'v' + (curry_max - prevy);
  405. if (curry_min !== curry_max)
  406. res += 'v' + (curry_min - curry_max);
  407. } else {
  408. if (curry_min !== prevy)
  409. res += 'v' + (curry_min - prevy);
  410. if (curry_max !== curry_min)
  411. res += 'v' + (curry_max - curry_min);
  412. if (curry !== curry_max)
  413. res += 'v' + (curry - curry_max);
  414. }
  415. prevx = currx;
  416. prevy = curry;
  417. }
  418. if (lastbin && (prevx !== grx))
  419. res += 'h'+(grx-prevx);
  420. bestimin = bestimax = i;
  421. curry_min = curry_max = curry = gry;
  422. currx = grx;
  423. }
  424. } else
  425. if ((gry !== curry) || lastbin) {
  426. if (grx !== currx) res += 'h'+(grx-currx);
  427. if (gry !== curry) res += 'v'+(gry-curry);
  428. curry = gry;
  429. currx = grx;
  430. }
  431. }
  432. const fill_for_interactive = !this.isBatchMode() && this.fillatt.empty() && options.Hist && settings.Tooltip && !draw_markers && !show_line;
  433. let h0 = height + 3;
  434. if (!fill_for_interactive) {
  435. const gry0 = Math.round(funcs.gry(0));
  436. if (gry0 <= 0)
  437. h0 = -3;
  438. else if (gry0 < height)
  439. h0 = gry0;
  440. }
  441. const close_path = `L${currx},${h0}H${startx}Z`;
  442. if (draw_markers || show_line) {
  443. if (path_fill) {
  444. this.draw_g.append('svg:path')
  445. .attr('d', path_fill)
  446. .call(this.fillatt.func);
  447. }
  448. if (path_err) {
  449. this.draw_g.append('svg:path')
  450. .attr('d', path_err)
  451. .call(this.lineatt.func);
  452. }
  453. if (hints_err) {
  454. this.draw_g.append('svg:path')
  455. .attr('d', hints_err)
  456. .style('fill', 'none')
  457. .style('pointer-events', this.isBatchMode() ? null : 'visibleFill');
  458. }
  459. if (path_line) {
  460. if (!this.fillatt.empty() && !options.Hist) {
  461. this.draw_g.append('svg:path')
  462. .attr('d', path_line + close_path)
  463. .call(this.fillatt.func);
  464. }
  465. this.draw_g.append('svg:path')
  466. .attr('d', path_line)
  467. .style('fill', 'none')
  468. .call(this.lineatt.func);
  469. }
  470. if (path_marker) {
  471. this.draw_g.append('svg:path')
  472. .attr('d', path_marker)
  473. .call(this.markeratt.func);
  474. }
  475. } else if (res && options.Hist) {
  476. this.draw_g.append('svg:path')
  477. .attr('d', res + ((!this.fillatt.empty() || fill_for_interactive) ? close_path : ''))
  478. .style('stroke-linejoin', 'miter')
  479. .call(this.lineatt.func)
  480. .call(this.fillatt.func);
  481. }
  482. return show_text ? this.finishTextDrawing() : true;
  483. });
  484. }
  485. /** @summary Provide text information (tooltips) for histogram bin */
  486. getBinTooltips(bin) {
  487. const tips = [],
  488. name = this.getObjectHint(),
  489. pmain = this.getFramePainter(),
  490. histo = this.getHisto(),
  491. xaxis = this.getAxis('x'),
  492. di = this.isDisplayItem() ? histo.stepx : 1,
  493. x1 = xaxis.GetBinCoord(bin),
  494. x2 = xaxis.GetBinCoord(bin+di),
  495. xlbl = this.getAxisBinTip('x', bin, di);
  496. let cont = histo.getBinContent(bin+1);
  497. if (name) tips.push(name);
  498. if (this.options.Error || this.options.Mark) {
  499. tips.push(`x = ${xlbl}`, `y = ${pmain.axisAsText('y', cont)}`);
  500. if (this.options.Error) {
  501. if (xlbl[0] === '[') tips.push('error x = ' + ((x2 - x1) / 2).toPrecision(4));
  502. tips.push('error y = ' + histo.getBinError(bin + 1).toPrecision(4));
  503. }
  504. } else {
  505. tips.push(`bin = ${bin+1}`, `x = ${xlbl}`);
  506. if (histo.$baseh) cont -= histo.$baseh.getBinContent(bin+1);
  507. const lbl = 'entries = ' + (di > 1 ? '~' : '');
  508. if (cont === Math.round(cont))
  509. tips.push(lbl + cont);
  510. else
  511. tips.push(lbl + floatToString(cont, gStyle.fStatFormat));
  512. }
  513. return tips;
  514. }
  515. /** @summary Process tooltip event */
  516. processTooltipEvent(pnt) {
  517. let ttrect = this.draw_g?.selectChild('.tooltip_bin');
  518. if (!pnt || !this.draw_content || this.options.Mode3D || !this.draw_g) {
  519. ttrect?.remove();
  520. return null;
  521. }
  522. const pmain = this.getFramePainter(),
  523. funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y),
  524. width = pmain.getFrameWidth(),
  525. height = pmain.getFrameHeight(),
  526. histo = this.getHisto(), xaxis = this.getAxis('x'),
  527. left = this.getSelectIndex('x', 'left', -1),
  528. right = this.getSelectIndex('x', 'right', 2);
  529. let show_rect, grx1, grx2, gry1, gry2, gapx = 2,
  530. l = left, r = right;
  531. function GetBinGrX(i) {
  532. const xx = xaxis.GetBinCoord(i);
  533. return (funcs.logx && (xx <= 0)) ? null : funcs.grx(xx);
  534. }
  535. function GetBinGrY(i) {
  536. const yy = histo.getBinContent(i + 1);
  537. if (funcs.logy && (yy < funcs.scale_ymin))
  538. return funcs.swap_xy ? -1000 : 10*height;
  539. return Math.round(funcs.gry(yy));
  540. }
  541. const pnt_x = funcs.swap_xy ? pnt.y : pnt.x,
  542. pnt_y = funcs.swap_xy ? pnt.x : pnt.y;
  543. while (l < r-1) {
  544. const m = Math.round((l+r)*0.5),
  545. xx = GetBinGrX(m);
  546. if ((xx === null) || (xx < pnt_x - 0.5))
  547. if (funcs.swap_xy) r = m; else l = m;
  548. else if (xx > pnt_x + 0.5)
  549. if (funcs.swap_xy) l = m; else r = m;
  550. else { l++; r--; }
  551. }
  552. let findbin = r = l;
  553. grx1 = GetBinGrX(findbin);
  554. if (funcs.swap_xy) {
  555. while ((l > left) && (GetBinGrX(l-1) < grx1 + 2)) --l;
  556. while ((r < right) && (GetBinGrX(r+1) > grx1 - 2)) ++r;
  557. } else {
  558. while ((l > left) && (GetBinGrX(l-1) > grx1 - 2)) --l;
  559. while ((r < right) && (GetBinGrX(r+1) < grx1 + 2)) ++r;
  560. }
  561. if (l < r) {
  562. // many points can be assigned with the same cursor position
  563. // first try point around mouse y
  564. let best = height;
  565. for (let m = l; m <= r; m++) {
  566. const dist = Math.abs(GetBinGrY(m) - pnt_y);
  567. if (dist < best) { best = dist; findbin = m; }
  568. }
  569. // if best distance still too far from mouse position, just take from between
  570. if (best > height/10)
  571. findbin = Math.round(l + (r-l) / height * pnt_y);
  572. grx1 = GetBinGrX(findbin);
  573. }
  574. grx1 = Math.round(grx1);
  575. grx2 = Math.round(GetBinGrX(findbin+1));
  576. if (this.options.Bar) {
  577. const w = grx2 - grx1;
  578. grx1 += Math.round(this.options.BarOffset*w);
  579. grx2 = grx1 + Math.round(this.options.BarWidth*w);
  580. }
  581. if (grx1 > grx2)
  582. [grx1, grx2] = [grx2, grx1];
  583. if (this.isDisplayItem() && ((findbin <= histo.dx) || (findbin >= histo.dx + histo.nx))) {
  584. // special case when zoomed out of scale and bin is not available
  585. ttrect.remove();
  586. return null;
  587. }
  588. const midx = Math.round((grx1 + grx2)/2),
  589. midy = gry1 = gry2 = GetBinGrY(findbin);
  590. if (this.options.Bar) {
  591. show_rect = true;
  592. gapx = 0;
  593. gry1 = this.getBarBaseline(funcs, height);
  594. if (gry1 > gry2)
  595. [gry1, gry2] = [gry2, gry1];
  596. if (!pnt.touch && (pnt.nproc === 1))
  597. if ((pnt_y < gry1) || (pnt_y > gry2)) findbin = null;
  598. } else if (this.options.Error || this.options.Mark) {
  599. show_rect = true;
  600. let msize = 3;
  601. if (this.markeratt) msize = Math.max(msize, this.markeratt.getFullSize());
  602. if (this.options.Error) {
  603. const cont = histo.getBinContent(findbin+1),
  604. binerr = histo.getBinError(findbin+1);
  605. gry1 = Math.round(funcs.gry(cont + binerr)); // up
  606. gry2 = Math.round(funcs.gry(cont - binerr)); // down
  607. const dx = (grx2-grx1)*this.options.errorX;
  608. grx1 = Math.round(midx - dx);
  609. grx2 = Math.round(midx + dx);
  610. }
  611. // show at least 6 pixels as tooltip rect
  612. if (grx2 - grx1 < 2*msize) { grx1 = midx-msize; grx2 = midx+msize; }
  613. gry1 = Math.min(gry1, midy - msize);
  614. gry2 = Math.max(gry2, midy + msize);
  615. if (!pnt.touch && (pnt.nproc === 1))
  616. if ((pnt_y < gry1) || (pnt_y > gry2)) findbin = null;
  617. } else if (this.options.Line)
  618. show_rect = false;
  619. else {
  620. // if histogram alone, use old-style with rects
  621. // if there are too many points at pixel, use circle
  622. show_rect = (pnt.nproc === 1) && (right-left < width);
  623. if (show_rect) {
  624. gry2 = height;
  625. if (!this.fillatt.empty()) {
  626. gry2 = Math.min(height, Math.max(0, Math.round(funcs.gry(0))));
  627. if (gry2 < gry1)
  628. [gry1, gry2] = [gry2, gry1];
  629. }
  630. // for mouse events pointer should be between y1 and y2
  631. if (((pnt.y < gry1) || (pnt.y > gry2)) && !pnt.touch) findbin = null;
  632. }
  633. }
  634. if (findbin !== null) {
  635. // if bin on boundary found, check that x position is ok
  636. if ((findbin === left) && (grx1 > pnt_x + gapx)) findbin = null; else
  637. if ((findbin === right-1) && (grx2 < pnt_x - gapx)) findbin = null; else
  638. // if bars option used check that bar is not match
  639. if ((pnt_x < grx1 - gapx) || (pnt_x > grx2 + gapx)) findbin = null; else
  640. // exclude empty bin if empty bins suppressed
  641. if (!this.options.Zero && (histo.getBinContent(findbin+1) === 0)) findbin = null;
  642. }
  643. if ((findbin === null) || ((gry2 <= 0) || (gry1 >= height))) {
  644. ttrect.remove();
  645. return null;
  646. }
  647. const res = { name: 'histo', title: histo.fTitle,
  648. x: midx, y: midy, exact: true,
  649. color1: this.lineatt?.color ?? 'green',
  650. color2: this.fillatt?.getFillColorAlt('blue') ?? 'blue',
  651. lines: this.getBinTooltips(findbin) };
  652. if (pnt.disabled) {
  653. // case when tooltip should not highlight bin
  654. ttrect.remove();
  655. res.changed = true;
  656. } else if (show_rect) {
  657. if (ttrect.empty()) {
  658. ttrect = this.draw_g.append('svg:rect')
  659. .attr('class', 'tooltip_bin')
  660. .style('pointer-events', 'none')
  661. .call(addHighlightStyle);
  662. }
  663. res.changed = ttrect.property('current_bin') !== findbin;
  664. if (res.changed) {
  665. ttrect.attr('x', pmain.swap_xy ? gry1 : grx1)
  666. .attr('width', pmain.swap_xy ? gry2-gry1 : grx2-grx1)
  667. .attr('y', pmain.swap_xy ? grx1 : gry1)
  668. .attr('height', pmain.swap_xy ? grx2-grx1 : gry2-gry1)
  669. .style('opacity', '0.3')
  670. .property('current_bin', findbin);
  671. }
  672. res.exact = (Math.abs(midy - pnt_y) <= 5) || ((pnt_y>=gry1) && (pnt_y<=gry2));
  673. res.menu = res.exact; // one could show context menu
  674. // distance to middle point, use to decide which menu to activate
  675. res.menu_dist = Math.sqrt((midx-pnt_x)**2 + (midy-pnt_y)**2);
  676. } else {
  677. const radius = this.lineatt.width + 3;
  678. if (ttrect.empty()) {
  679. ttrect = this.draw_g.append('svg:circle')
  680. .attr('class', 'tooltip_bin')
  681. .style('pointer-events', 'none')
  682. .attr('r', radius)
  683. .call(this.lineatt.func)
  684. .call(this.fillatt.func);
  685. }
  686. res.exact = (Math.abs(midx - pnt.x) <= radius) && (Math.abs(midy - pnt.y) <= radius);
  687. res.menu = res.exact; // show menu only when mouse pointer exactly over the histogram
  688. res.menu_dist = Math.sqrt((midx-pnt.x)**2 + (midy-pnt.y)**2);
  689. res.changed = ttrect.property('current_bin') !== findbin;
  690. if (res.changed) {
  691. ttrect.attr('cx', midx)
  692. .attr('cy', midy)
  693. .property('current_bin', findbin);
  694. }
  695. }
  696. if (res.changed) {
  697. res.user_info = { obj: histo, name: 'histo',
  698. bin: findbin, cont: histo.getBinContent(findbin+1),
  699. grx: midx, gry: midy };
  700. }
  701. return res;
  702. }
  703. /** @summary Fill histogram context menu */
  704. fillHistContextMenu(menu) {
  705. menu.add('Auto zoom-in', () => this.autoZoom());
  706. const opts = this.getSupportedDrawOptions();
  707. menu.addDrawMenu('Draw with', opts, arg => {
  708. if (arg.indexOf(kInspect) === 0)
  709. return this.showInspector(arg);
  710. this.decodeOptions(arg); // obsolete, should be implemented differently
  711. if (this.options.need_fillcol && this.fillatt?.empty())
  712. this.fillatt.change(5, 1001);
  713. // redraw all objects
  714. this.interactiveRedraw('pad', 'drawopt');
  715. });
  716. }
  717. /** @summary Perform automatic zoom inside non-zero region of histogram */
  718. autoZoom() {
  719. let left = this.getSelectIndex('x', 'left', -1),
  720. right = this.getSelectIndex('x', 'right', 1);
  721. const dist = right - left, histo = this.getHisto(), xaxis = this.getAxis('x');
  722. if (dist === 0) return;
  723. // first find minimum
  724. let min = histo.getBinContent(left + 1);
  725. for (let indx = left; indx < right; ++indx)
  726. min = Math.min(min, histo.getBinContent(indx+1));
  727. if (min > 0) return; // if all points positive, no chance for auto-scale
  728. while ((left < right) && (histo.getBinContent(left+1) <= min)) ++left;
  729. while ((left < right) && (histo.getBinContent(right) <= min)) --right;
  730. // if singular bin
  731. if ((left === right-1) && (left > 2) && (right < this.nbinsx-2)) {
  732. --left; ++right;
  733. }
  734. if ((right - left < dist) && (left < right))
  735. return this.getFramePainter().zoom(xaxis.GetBinCoord(left), xaxis.GetBinCoord(right));
  736. }
  737. /** @summary Checks if it makes sense to zoom inside specified axis range */
  738. canZoomInside(axis, min, max) {
  739. const xaxis = this.getAxis('x');
  740. if ((axis === 'x') && (xaxis.FindBin(max, 0.5) - xaxis.FindBin(min, 0) > 1)) return true;
  741. if ((axis === 'y') && (Math.abs(max-min) > Math.abs(this.ymax-this.ymin)*1e-6)) return true;
  742. return false;
  743. }
  744. /** @summary Call appropriate draw function */
  745. async callDrawFunc(reason) {
  746. const main = this.getFramePainter();
  747. if (main && (main.mode3d !== this.options.Mode3D) && !this.isMainPainter())
  748. this.options.Mode3D = main.mode3d;
  749. return this.options.Mode3D ? this.draw3D(reason) : this.draw2D(reason);
  750. }
  751. /** @summary Draw in 2d */
  752. async draw2D(reason) {
  753. this.clear3DScene();
  754. return this.drawFrameAxes().then(res => {
  755. return res ? this.drawingBins(reason) : false;
  756. }).then(res => {
  757. if (res)
  758. return this.draw1DBins().then(() => this.addInteractivity());
  759. }).then(() => this);
  760. }
  761. /** @summary Draw in 3d */
  762. async draw3D(reason) {
  763. console.log('3D drawing is disabled, load ./hist/RH1Painter.mjs');
  764. return this.draw2D(reason);
  765. }
  766. /** @summary Redraw histogram */
  767. async redraw(reason) {
  768. return this.callDrawFunc(reason);
  769. }
  770. static async _draw(painter, opt) {
  771. return ensureRCanvas(painter).then(() => {
  772. painter.setAsMainPainter();
  773. painter.options = { Hist: false, Bar: false, BarStyle: 0,
  774. Error: false, ErrorKind: -1, errorX: gStyle.fErrorX,
  775. Zero: false, Mark: false,
  776. Line: false, Fill: false, Lego: 0, Surf: 0,
  777. Text: false, TextAngle: 0, TextKind: '', AutoColor: 0,
  778. BarOffset: 0, BarWidth: 1, BaseLine: false,
  779. Mode3D: false, FrontBox: false, BackBox: false };
  780. const d = new DrawOptions(opt);
  781. if (d.check('R3D_', true))
  782. painter.options.Render3D = constants.Render3D.fromString(d.part.toLowerCase());
  783. const kind = painter.v7EvalAttr('kind', 'hist'),
  784. sub = painter.v7EvalAttr('sub', 0),
  785. has_main = Boolean(painter.getMainPainter()),
  786. o = painter.options;
  787. o.Text = painter.v7EvalAttr('drawtext', false);
  788. o.BarOffset = painter.v7EvalAttr('baroffset', 0.0);
  789. o.BarWidth = painter.v7EvalAttr('barwidth', 1.0);
  790. o.second_x = has_main && painter.v7EvalAttr('secondx', false);
  791. o.second_y = has_main && painter.v7EvalAttr('secondy', false);
  792. switch (kind) {
  793. case 'bar': o.Bar = true; o.BarStyle = sub; break;
  794. case 'err': o.Error = true; o.ErrorKind = sub; break;
  795. case 'p': o.Mark = true; break;
  796. case 'l': o.Line = true; break;
  797. case 'lego': o.Lego = sub > 0 ? 10+sub : 12; o.Mode3D = true; break;
  798. default: o.Hist = true;
  799. }
  800. painter.scanContent();
  801. return painter.callDrawFunc();
  802. });
  803. }
  804. /** @summary draw RH1 object */
  805. static async draw(dom, histo, opt) {
  806. return RH1Painter._draw(new RH1Painter(dom, histo), opt);
  807. }
  808. } // class RH1Painter
  809. export { RH1Painter };