From 375c9a4531e1e27e0f14517a97274fe581907658 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Wed, 18 Feb 2026 10:48:53 +0100 Subject: [PATCH 1/7] Move pad log selection to menu class Will be used from pave --- modules/gpad/TFramePainter.mjs | 18 +++--------------- modules/gui/menu.mjs | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/modules/gpad/TFramePainter.mjs b/modules/gpad/TFramePainter.mjs index f7930fc56..d9b5a2eba 100644 --- a/modules/gpad/TFramePainter.mjs +++ b/modules/gpad/TFramePainter.mjs @@ -2836,21 +2836,9 @@ class TFramePainter extends FrameInteractive { } menu.endsub(); - if (pad) { - const member = 'fLog' + kind[0]; - menu.sub('SetLog ' + kind[0], () => { - menu.input('Enter log kind: 0 - off, 1 - log10, 2 - log2, 3 - ln, ...', pad[member], 'int', 0, 10000).then(v => { - this.changeAxisLog(kind[0], v); - }); - }); - menu.addchk(pad[member] === 0, 'linear', () => this.changeAxisLog(kind[0], 0)); - menu.addchk(pad[member] === 1, 'log10', () => this.changeAxisLog(kind[0], 1)); - menu.addchk(pad[member] === 2, 'log2', () => this.changeAxisLog(kind[0], 2)); - menu.addchk(pad[member] === 3, 'ln', () => this.changeAxisLog(kind[0], 3)); - menu.addchk(pad[member] === 4, 'log4', () => this.changeAxisLog(kind[0], 4)); - menu.addchk(pad[member] === 8, 'log8', () => this.changeAxisLog(kind[0], 8)); - menu.endsub(); - } + if (pad) + menu.addPadLogMenu(kind[0], pad[`fLog${kind[0]}`], v => this.changeAxisLog(kind[0], v)); + menu.addchk(faxis.TestBit(EAxisBits.kMoreLogLabels), 'More log', flag => { faxis.SetBit(EAxisBits.kMoreLogLabels, flag); if (hist_painter?.getSnapId() && (kind.length === 1)) diff --git a/modules/gui/menu.mjs b/modules/gui/menu.mjs index 384878147..7e842c1e3 100644 --- a/modules/gui/menu.mjs +++ b/modules/gui/menu.mjs @@ -281,6 +281,20 @@ class JSRootMenu { this.endsub(); } + /** @summary Add log scale selection for pad + * @protected */ + addPadLogMenu(kind, value, func) { + this.sub('SetLog ' + kind, + () => this.input('Enter log kind: 0 - off, 1 - log10, 2 - log2, 3 - ln, ...', value, 'int', 0, 10000).then(func)); + this.addchk(value === 0, 'linear', () => func(0)); + this.addchk(value === 1, 'log10', () => func(1)); + this.addchk(value === 2, 'log2', () => func(2)); + this.addchk(value === 3, 'ln', () => func(3)); + this.addchk(value === 4, 'log4', () => func(4)); + this.addchk(value === 8, 'log8', () => func(8)); + this.endsub(); + } + /** @summary Add palette menu entries * @protected */ addPaletteMenu(curr, set_func) { From c69330bbf5d6e3f3de5045ef5147c7dcd3f0e5a9 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Wed, 18 Feb 2026 10:50:15 +0100 Subject: [PATCH 2/7] Provide separate context menu for palette with th3 In such case z scale is not used for the palette so z context menu does not work properly --- modules/hist/TPavePainter.mjs | 42 ++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/modules/hist/TPavePainter.mjs b/modules/hist/TPavePainter.mjs index 7602dd24f..0a02348d0 100644 --- a/modules/hist/TPavePainter.mjs +++ b/modules/hist/TPavePainter.mjs @@ -1,12 +1,13 @@ import { gStyle, browser, settings, clone, create, isObject, isFunc, isStr, BIT, clTPave, clTPaveText, clTPavesText, clTPaveStats, clTPaveLabel, clTPaveClass, clTDiamond, clTLegend, clTPaletteAxis, - clTText, clTLatex, clTLine, clTBox, kTitle, isNodeJs, nsSVG } from '../core.mjs'; + clTText, clTLatex, clTLine, clTBox, clTAxis, + kTitle, isNodeJs, nsSVG, urlClassPrefix } from '../core.mjs'; import { select as d3_select, rgb as d3_rgb, pointer as d3_pointer } from '../d3.mjs'; import { Prob } from '../base/math.mjs'; import { floatToString, makeTranslate, compressSVG, svgToImage, addHighlightStyle } from '../base/BasePainter.mjs'; import { ObjectPainter, EAxisBits } from '../base/ObjectPainter.mjs'; import { approximateLabelWidth } from '../base/latex.mjs'; -import { showPainterMenu } from '../gui/menu.mjs'; +import { showPainterMenu, createMenu } from '../gui/menu.mjs'; import { getColorExec } from '../gui/utils.mjs'; import { TAxisPainter } from '../gpad/TAxisPainter.mjs'; import { addDragHandler } from '../gpad/TFramePainter.mjs'; @@ -1047,6 +1048,7 @@ class TPavePainter extends ObjectPainter { let zmin = 0, zmax = 100, gzmin, gzmax, axis_transform, axis_second = 0; this.#palette_vertical = (palette.fX2NDC - palette.fX1NDC) < (palette.fY2NDC - palette.fY1NDC); + this.is_th3 = is_th3; axis.fTickSize = 0.03; // adjust axis ticks size @@ -1523,12 +1525,42 @@ class TPavePainter extends ObjectPainter { /** @summary Show pave context menu */ paveContextMenu(evnt) { - if (this.z_handle) { + if (!this.z_handle) + return showPainterMenu(evnt, this); + if (!this.is_th3) { const fp = this.getFramePainter(); if (isFunc(fp?.showContextMenu)) fp.showContextMenu('pal', evnt); - } else - showPainterMenu(evnt, this); + return; + } + + const pp = this.getPadPainter(), + pad = pp?.getRootPad(true), + faxis = this.z_handle.getObject(), + hist_painter = this.z_handle.hist_painter || this.getMainPainter(true); + + if (!pad || !hist_painter) + return; + + if (isFunc(evnt?.stopPropagation)) { + evnt.preventDefault(); + evnt.stopPropagation(); // disable main context menu + } + + createMenu(evnt, this).then(menu => { + menu.header('V axis', `${urlClassPrefix}${clTAxis}.html`); + + menu.addPadLogMenu('v', pad.fLogv || 0, v => { + pad.fLogv = v; + this.interactiveRedraw('pad', 'log'); + }); + + hist_painter.fillPaletteMenu(menu, false); + + menu.addTAxisMenu(EAxisBits, hist_painter || this, faxis, 'v', this.z_handle, null); + + menu.show(); + }); } /** @summary Returns true when stat box is drawn */ From 18f2563d6b16d1623850bccf5d5909c50c020c8e Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Wed, 18 Feb 2026 11:23:53 +0100 Subject: [PATCH 3/7] Use min/max values from contour when draw th3 palette --- modules/hist/TPavePainter.mjs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/hist/TPavePainter.mjs b/modules/hist/TPavePainter.mjs index 0a02348d0..245cbdfde 100644 --- a/modules/hist/TPavePainter.mjs +++ b/modules/hist/TPavePainter.mjs @@ -1035,10 +1035,10 @@ class TPavePainter extends ObjectPainter { height = pp.getPadHeight(), pad = pp.getRootPad(true), main = palette.$main_painter || this.getMainPainter(), + is_th3 = isFunc(main.getDimension) && (main.getDimension() === 3), fp = this.getFramePainter(), - contour = main.getContour(false), + contour = main.getContour(is_th3), levels = contour?.getLevels(), - is_th3 = isFunc(main.getDimension) && (main.getDimension() === 3), is_scatter = isFunc(main.getZaxis), log = pad?.fLogv ?? (is_th3 ? false : pad?.fLogz), draw_palette = main.getHistPalette(), @@ -1085,12 +1085,16 @@ class TPavePainter extends ObjectPainter { } } else if ((main.gmaxbin !== undefined) && (main.gminbin !== undefined)) { // this is case of TH2 (needs only for size adjustment) - zmin = main.gminbin; - zmax = main.gmaxbin; + gzmin = zmin = main.gminbin; + gzmax = zmax = main.gmaxbin; + if (contour?.colzmin !== undefined && contour?.colzmax !== undefined) { + zmin = contour.colzmin; + zmax = contour.colzmax; + } } else if ((main.hmin !== undefined) && (main.hmax !== undefined)) { // this is case of TH1 - zmin = main.hmin; - zmax = main.hmax; + gzmin = zmin = main.hmin; + gzmax = zmax = main.hmax; } g.selectAll('rect').style('fill', 'white'); From 003ac0fb74090b6f685b63554b3d422dc31a7f65 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Wed, 18 Feb 2026 12:51:31 +0100 Subject: [PATCH 4/7] Format code --- modules/hist/TH3Painter.mjs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/modules/hist/TH3Painter.mjs b/modules/hist/TH3Painter.mjs index a3cf71903..5dc63ef2c 100644 --- a/modules/hist/TH3Painter.mjs +++ b/modules/hist/TH3Painter.mjs @@ -469,19 +469,6 @@ class TH3Painter extends THistPainter { } else if (use_scale) use_scale = (this.gminbin || this.gmaxbin) ? 1 / Math.max(Math.abs(this.gminbin), Math.abs(this.gmaxbin)) : 1; - const get_bin_weight = content => { - if ((exclude_content >= 0) && (content < exclude_content)) - return 0; - if (!use_scale) - return 1; - if (logv) { - if (content <= 0) - return 0; - content = Math.log(content) - scale_offset; - } - return Math.pow(Math.abs(content * use_scale), 0.3333); - }; - // eslint-disable-next-line one-var const i1 = this.getSelectIndex('x', 'left', 0.5), i2 = this.getSelectIndex('x', 'right', 0), j1 = this.getSelectIndex('y', 'left', 0.5), @@ -495,7 +482,19 @@ class TH3Painter extends THistPainter { const cntr = use_colors ? this.getContour() : null, palette = use_colors ? this.getHistPalette() : null, bins_matrixes = [], bins_colors = [], bins_ids = [], negative_matrixes = [], bin_opacities = [], - transfer = (this.transferFunc && proivdeEvalPar(this.transferFunc, true)) ? this.transferFunc : null; + transfer = (this.transferFunc && proivdeEvalPar(this.transferFunc, true)) ? this.transferFunc : null, + get_bin_weight = content => { + if ((exclude_content >= 0) && (content < exclude_content)) + return 0; + if (!use_scale) + return 1; + if (logv) { + if (content <= 0) + return 0; + content = Math.log(content) - scale_offset; + } + return Math.pow(Math.abs(content * use_scale), 0.3333); + }; for (let i = i1; i < i2; ++i) { const grx1 = fp.grx(histo.fXaxis.GetBinLowEdge(i + 1)), From c7fa45d2558fabbeb4079136e2e4c5d76826d40c Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Wed, 18 Feb 2026 12:54:07 +0100 Subject: [PATCH 5/7] Correctly reset contour in TH3 painter --- modules/hist/TH3Painter.mjs | 3 +++ modules/hist2d/THistPainter.mjs | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/hist/TH3Painter.mjs b/modules/hist/TH3Painter.mjs index 5dc63ef2c..02578777e 100644 --- a/modules/hist/TH3Painter.mjs +++ b/modules/hist/TH3Painter.mjs @@ -396,6 +396,9 @@ class TH3Painter extends THistPainter { const histo = this.getHisto(), fp = this.getFramePainter(); + // ensure proper colors + this.resetContour(); + let use_lambert = false, use_helper = false, use_colors = false, use_opacity = 1, exclude_content = -1, logv = this.getPadPainter()?.getRootPad()?.fLogv, diff --git a/modules/hist2d/THistPainter.mjs b/modules/hist2d/THistPainter.mjs index de6c58781..da76c5fad 100644 --- a/modules/hist2d/THistPainter.mjs +++ b/modules/hist2d/THistPainter.mjs @@ -2282,6 +2282,12 @@ class THistPainter extends ObjectPainter { return cntr; } + /** @summary Reset contour object + * @private */ + resetContour() { + this.#contour = undefined; + } + /** @summary Return Z-scale ranges to create contour */ #getContourRanges(main, fp) { const o = this.getOptions(), @@ -2862,7 +2868,7 @@ class THistPainter extends ObjectPainter { this.maxbin = this.minbin = 0; // force recalculation of z levels - this.#contour = undefined; + this.resetContour(); if (args.zrange) Object.assign(res, this.#getContourRanges(this.getMainPainter(), this.getFramePainter())); From 9407926896470d4463d61d16364cd4c703baa956 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Wed, 18 Feb 2026 13:26:34 +0100 Subject: [PATCH 6/7] When zooming color for TH3 - change min/max values This is the only simple way to correctly handle such zooming --- modules/hist/TPavePainter.mjs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/modules/hist/TPavePainter.mjs b/modules/hist/TPavePainter.mjs index 245cbdfde..a458bcfd1 100644 --- a/modules/hist/TPavePainter.mjs +++ b/modules/hist/TPavePainter.mjs @@ -1,7 +1,7 @@ import { gStyle, browser, settings, clone, create, isObject, isFunc, isStr, BIT, clTPave, clTPaveText, clTPavesText, clTPaveStats, clTPaveLabel, clTPaveClass, clTDiamond, clTLegend, clTPaletteAxis, clTText, clTLatex, clTLine, clTBox, clTAxis, - kTitle, isNodeJs, nsSVG, urlClassPrefix } from '../core.mjs'; + kTitle, isNodeJs, nsSVG, urlClassPrefix, kNoZoom } from '../core.mjs'; import { select as d3_select, rgb as d3_rgb, pointer as d3_pointer } from '../d3.mjs'; import { Prob } from '../base/math.mjs'; import { floatToString, makeTranslate, compressSVG, svgToImage, addHighlightStyle } from '../base/BasePainter.mjs'; @@ -1266,6 +1266,17 @@ class TPavePainter extends ObjectPainter { zoom_rect.attr('x', Math.min(sel1, sel2)) .attr('width', Math.abs(sel2 - sel1)); } + }, zoomPalette = (z1, z2) => { + if (!this.is_th3) + return this.getFramePainter().zoomSingle('z', z1, z2, true); + const maino = this.getMainPainter().options; + if (z1 === z2) + maino.minimum = maino.maximum = kNoZoom; + else { + maino.minimum = z1; + maino.maximum = z2; + } + this.interactiveRedraw('pad'); }, endRectSel = evnt => { if (!doing_zoom) return; @@ -1277,10 +1288,13 @@ class TPavePainter extends ObjectPainter { zoom_rect = null; doing_zoom = false; + if (sel1 === sel2) + return; + const z1 = this.z_handle.revertPoint(sel1), z2 = this.z_handle.revertPoint(sel2); - this.getFramePainter().zoomSingle('z', Math.min(z1, z2), Math.max(z1, z2), true); + zoomPalette(Math.min(z1, z2), Math.max(z1, z2)); }, startRectSel = evnt => { // ignore when touch selection is activated if (doing_zoom) @@ -1315,7 +1329,7 @@ class TPavePainter extends ObjectPainter { if (settings.Zooming) { this.getG().selectAll('.axis_zoom') .on('mousedown', startRectSel) - .on('dblclick', () => this.getFramePainter().zoomSingle('z', 0, 0, true)); + .on('dblclick', () => zoomPalette(0, 0)); } if (settings.ZoomWheel) { @@ -1324,7 +1338,7 @@ class TPavePainter extends ObjectPainter { coord = this.#palette_vertical ? (1 - pos[1] / s_height) : pos[0] / s_width, item = this.z_handle.analyzeWheelEvent(evnt, coord); if (item?.changed) - this.getFramePainter().zoomSingle('z', item.min, item.max, true); + zoomPalette(item.min, item.max); }); } } From be7dc26fd784502cd0c6935aad8987ea19025b81 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Wed, 18 Feb 2026 13:34:02 +0100 Subject: [PATCH 7/7] Adjust context menu for V axis Labels are not used, but log bits can be important --- modules/gui/menu.mjs | 68 ++++++++++++++++++----------------- modules/hist/TPavePainter.mjs | 9 +++++ 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/modules/gui/menu.mjs b/modules/gui/menu.mjs index 7e842c1e3..27e02d384 100644 --- a/modules/gui/menu.mjs +++ b/modules/gui/menu.mjs @@ -740,38 +740,40 @@ class JSRootMenu { faxis.fNdivisions = val; painter.interactiveRedraw('pad', `exec:SetNdivisions(${val})`, kind); })); - this.sub('Labels'); - this.addchk(faxis.TestBit(EAxisBits.kCenterLabels), 'Center', - arg => { faxis.SetBit(EAxisBits.kCenterLabels, arg); painter.interactiveRedraw('pad', `exec:CenterLabels(${arg})`, kind); }); - this.addchk(faxis.TestBit(EAxisBits.kLabelsVert), 'Rotate', - arg => { faxis.SetBit(EAxisBits.kLabelsVert, arg); painter.interactiveRedraw('pad', `exec:SetBit(TAxis::kLabelsVert,${arg})`, kind); }); - this.addColorMenu('Color', faxis.fLabelColor, - arg => { faxis.fLabelColor = arg; painter.interactiveRedraw('pad', getColorExec(arg, 'SetLabelColor'), kind); }); - this.addSizeMenu('Offset', -0.02, 0.1, 0.01, faxis.fLabelOffset, - arg => { faxis.fLabelOffset = arg; painter.interactiveRedraw('pad', `exec:SetLabelOffset(${arg})`, kind); }); - let a = faxis.fLabelSize >= 1; - this.addSizeMenu('Size', a ? 2 : 0.02, a ? 30 : 0.11, a ? 2 : 0.01, faxis.fLabelSize, - arg => { faxis.fLabelSize = arg; painter.interactiveRedraw('pad', `exec:SetLabelSize(${arg})`, kind); }); - - if (frame_painter && (axis_painter?.kind === kAxisLabels) && (faxis.fNbins > 20)) { - this.add('Find label', () => this.input('Label id').then(id => { - if (!id) - return; - for (let bin = 0; bin < faxis.fNbins; ++bin) { - const lbl = axis_painter.formatLabels(bin); - if (lbl === id) - return frame_painter.zoomSingle(kind, Math.max(0, bin - 4), Math.min(faxis.fNbins, bin + 5)); - } - }), 'Zoom into region around specific label'); - } - if (frame_painter && faxis.fLabels) { - const ignore = `${kind}_ignore_labels`; - this.addchk(!frame_painter[ignore], 'Custom', flag => { - frame_painter[ignore] = !flag; - painter.interactiveRedraw('pad'); - }, `Use of custom labels in axis ${kind}`); + if (kind !== 'v') { + this.sub('Labels'); + this.addchk(faxis.TestBit(EAxisBits.kCenterLabels), 'Center', + arg => { faxis.SetBit(EAxisBits.kCenterLabels, arg); painter.interactiveRedraw('pad', `exec:CenterLabels(${arg})`, kind); }); + this.addchk(faxis.TestBit(EAxisBits.kLabelsVert), 'Rotate', + arg => { faxis.SetBit(EAxisBits.kLabelsVert, arg); painter.interactiveRedraw('pad', `exec:SetBit(TAxis::kLabelsVert,${arg})`, kind); }); + this.addColorMenu('Color', faxis.fLabelColor, + arg => { faxis.fLabelColor = arg; painter.interactiveRedraw('pad', getColorExec(arg, 'SetLabelColor'), kind); }); + this.addSizeMenu('Offset', -0.02, 0.1, 0.01, faxis.fLabelOffset, + arg => { faxis.fLabelOffset = arg; painter.interactiveRedraw('pad', `exec:SetLabelOffset(${arg})`, kind); }); + const a = faxis.fLabelSize >= 1; + this.addSizeMenu('Size', a ? 2 : 0.02, a ? 30 : 0.11, a ? 2 : 0.01, faxis.fLabelSize, + arg => { faxis.fLabelSize = arg; painter.interactiveRedraw('pad', `exec:SetLabelSize(${arg})`, kind); }); + + if (frame_painter && (axis_painter?.kind === kAxisLabels) && (faxis.fNbins > 20)) { + this.add('Find label', () => this.input('Label id').then(id => { + if (!id) + return; + for (let bin = 0; bin < faxis.fNbins; ++bin) { + const lbl = axis_painter.formatLabels(bin); + if (lbl === id) + return frame_painter.zoomSingle(kind, Math.max(0, bin - 4), Math.min(faxis.fNbins, bin + 5)); + } + }), 'Zoom into region around specific label'); + } + if (frame_painter && faxis.fLabels) { + const ignore = `${kind}_ignore_labels`; + this.addchk(!frame_painter[ignore], 'Custom', flag => { + frame_painter[ignore] = !flag; + painter.interactiveRedraw('pad'); + }, `Use of custom labels in axis ${kind}`); + } + this.endsub(); } - this.endsub(); this.sub('Title'); this.add('SetTitle', () => { @@ -798,8 +800,8 @@ class JSRootMenu { }); this.addSizeMenu('Offset', 0, 3, 0.2, faxis.fTitleOffset, arg => { faxis.fTitleOffset = arg; painter.interactiveRedraw('pad', `exec:SetTitleOffset(${arg})`, kind); }); - a = faxis.fTitleSize >= 1; - this.addSizeMenu('Size', a ? 2 : 0.02, a ? 30 : 0.11, a ? 2 : 0.01, faxis.fTitleSize, + const p = faxis.fTitleSize >= 1; + this.addSizeMenu('Size', p ? 2 : 0.02, p ? 30 : 0.11, p ? 2 : 0.01, faxis.fTitleSize, arg => { faxis.fTitleSize = arg; painter.interactiveRedraw('pad', `exec:SetTitleSize(${arg})`, kind); }); this.endsub(); diff --git a/modules/hist/TPavePainter.mjs b/modules/hist/TPavePainter.mjs index a458bcfd1..7d585e530 100644 --- a/modules/hist/TPavePainter.mjs +++ b/modules/hist/TPavePainter.mjs @@ -1573,6 +1573,15 @@ class TPavePainter extends ObjectPainter { this.interactiveRedraw('pad', 'log'); }); + menu.addchk(faxis.TestBit(EAxisBits.kMoreLogLabels), 'More log', flag => { + faxis.SetBit(EAxisBits.kMoreLogLabels, flag); + this.interactiveRedraw('pad'); + }); + menu.addchk(faxis.TestBit(EAxisBits.kNoExponent), 'No exponent', flag => { + faxis.SetBit(EAxisBits.kNoExponent, flag); + this.interactiveRedraw('pad'); + }); + hist_painter.fillPaletteMenu(menu, false); menu.addTAxisMenu(EAxisBits, hist_painter || this, faxis, 'v', this.z_handle, null);