From 0000d4fa50eb2a3b703fa5c3452fbc5eb1571215 Mon Sep 17 00:00:00 2001
From: Sergey Linev <S.Linev@gsi.de>
Date: Thu, 23 Mar 2023 15:05:00 +0100
Subject: [PATCH] [jsroot] support projx2_y5 draw option

Allows to configure different bins width for X and Y projections
---
 js/build/jsroot.js                 | 162 +++++++++++++++++++----------
 js/changes.md                      |   2 +-
 js/modules/hist2d/RH2Painter.mjs   | 103 ++++++++++++------
 js/modules/hist2d/TH2Painter.mjs   |  57 ++++++----
 js/modules/hist2d/THistPainter.mjs |   4 +-
 5 files changed, 220 insertions(+), 108 deletions(-)

diff --git a/js/build/jsroot.js b/js/build/jsroot.js
index 3559f92cf76..5608d0a8e5f 100644
--- a/js/build/jsroot.js
+++ b/js/build/jsroot.js
@@ -73703,8 +73703,8 @@ class THistDrawOptions {
       if (this.Proj > 0) this.Contour = 14;
 
       if (d.check('PROJXY', true)) this.Project = 'XY' + d.partAsInt(0,1);
-      if (d.check('PROJX', true)) this.Project = 'X' + d.partAsInt(0,1);
-      if (d.check('PROJY', true)) this.Project = 'Y' + d.partAsInt(0,1);
+      if (d.check('PROJX', true)) this.Project = 'X' + d.part;
+      if (d.check('PROJY', true)) this.Project = 'Y' + d.part;
       if (d.check('PROJ')) this.Project = 'Y1';
 
       if (check3dbox) {
@@ -76953,22 +76953,37 @@ let TH2Painter$2 = class TH2Painter extends THistPainter {
       if ((kind == 'Projections') || (kind == 'Off'))
          kind = '';
 
+      let widthX = width, widthY = width;
+
       if (isStr(kind) && (kind.indexOf('XY') == 0)) {
-         if (kind.length > 2)
-            width = parseInt(kind.slice(2));
+         let ws = (kind.length > 2) ? kind.slice(2) : '';
          kind = 'XY';
+         widthX = widthY = parseInt(ws) || 1;
       } else if (isStr(kind) && (kind.length > 1)) {
-         width = parseInt(kind.slice(1));
-         kind = kind[0];
+         let 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 (!width) width = 1;
+      if (!widthX && !widthY)
+         widthX = widthY = 1;
 
       if (kind && (this.is_projection == kind)) {
-         if (this.projection_width === width) {
+         if ((this.projection_widthX === widthX) && (this.projection_widthY === widthY)) {
             kind = '';
          } else {
-            this.projection_width = width;
+            this.projection_widthX = widthX;
+            this.projection_widthY = widthY;
             return;
          }
       }
@@ -76976,7 +76991,8 @@ let TH2Painter$2 = class TH2Painter extends THistPainter {
       delete this.proj_hist;
 
       let new_proj = (this.is_projection === kind) ? '' : kind;
-      this.projection_width = width;
+      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(); });
@@ -77105,11 +77121,15 @@ let TH2Painter$2 = class TH2Painter extends THistPainter {
    /** @summary Fill histogram context menu */
    fillHistContextMenu(menu) {
       if (!this.isTH2Poly()) {
-         menu.add('sub:Projections', () => this.toggleProjection());
          let kind = this.is_projection || '';
-         if (kind) kind += this.projection_width;
+         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 (this.is_projection) kinds.push('Off');
+         if (kind) kinds.unshift('Off');
+
+         menu.add('sub:Projections', () => menu.input('Input projection kind X1 or XY2', 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.add('endsub:');
@@ -79550,14 +79570,15 @@ let TH2Painter$2 = class TH2Painter extends THistPainter {
          let binid = i*10000 + j, path;
 
          if (this.is_projection) {
-            let pw = this.projection_width || 1, dd = (pw - 1) / 2;
-            if ((this.is_projection.indexOf('X')) >= 0 && (pw > 1)) {
-               if (j2+dd >= h.j2) { j2 = Math.min(Math.round(j2+dd), h.j2); j1 = Math.max(j2 - pw, h.j1); }
-                             else { j1 = Math.max(Math.round(j1-dd), h.j1); j2 = Math.min(j1 + pw, h.j2); }
+            let 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); }
             }
-            if ((this.is_projection.indexOf('Y')) >= 0 && (pw > 1)) {
-               if (i2+dd >= h.i2) { i2 = Math.min(Math.round(i2+dd), h.i2); i1 = Math.max(i2 - pw, h.i1); }
-                             else { i1 = Math.max(Math.round(i1-dd), h.i1); i2 = Math.min(i1 + pw, h.i2); }
+            let 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); }
             }
          }
 
@@ -119367,20 +119388,40 @@ let RH2Painter$2 = class RH2Painter extends RHistPainter {
    /** @summary Toggle projection */
    toggleProjection(kind, width) {
 
-      if (kind == 'Projections') kind = '';
+      if ((kind == 'Projections') || (kind == 'Off'))
+         kind = '';
+
+      let widthX = width, widthY = width;
 
-      if (isStr(kind) && (kind.length > 1)) {
-          width = parseInt(kind.slice(1));
-          kind = kind[0];
+      if (isStr(kind) && (kind.indexOf('XY') == 0)) {
+         let ws = kind.length > 2 ? kind.slice(2) : '';
+         kind = 'XY';
+         widthX = widthY = parseInt(ws);
+      } else if (isStr(kind) && (kind.length > 1)) {
+         let 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 (!width) width = 1;
+      if (!widthX && !widthY)
+         widthX = widthY = 1;
 
-      if (kind && (this.is_projection==kind)) {
-         if (this.projection_width === width) {
+      if (kind && (this.is_projection == kind)) {
+         if ((this.projection_widthX === widthX) && (this.projection_widthY === widthY)) {
             kind = '';
          } else {
-            this.projection_width = width;
+            this.projection_widthX = widthX;
+            this.projection_widthY = widthY;
             return;
          }
       }
@@ -119388,8 +119429,9 @@ let RH2Painter$2 = class RH2Painter extends RHistPainter {
       delete this.proj_hist;
 
       let new_proj = (this.is_projection === kind) ? '' : kind;
-      this.is_projection = ''; // disable projection redraw until callback
-      this.projection_width = width;
+      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(); });
    }
@@ -119415,10 +119457,16 @@ let RH2Painter$2 = class RH2Painter extends RHistPainter {
 
    /** @summary Fill histogram context menu */
    fillHistContextMenu(menu) {
-      menu.add('sub:Projections', () => this.toggleProjection());
       let kind = this.is_projection || '';
-      if (kind) kind += this.projection_width;
-      let kinds = ['X1', 'X2', 'X3', 'X5', 'X10', 'Y1', 'Y2', 'Y3', 'Y5', 'Y10'];
+      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}`;
+
+      menu.add('sub:Projections', () => menu.input('Input projection kind X1 or XY2', kind, 'string').then(val => this.toggleProjection(val)));
+
+      let kinds = ['X1', 'X2', 'X3', 'X5', 'X10', 'Y1', 'Y2', 'Y3', 'Y5', 'Y10', 'XY1', 'XY2', 'XY3', 'XY5', 'XY10'];
+      if (kind) kinds.unshift('Off');
+
       for (let k = 0; k < kinds.length; ++k)
          menu.addchk(kind == kinds[k], kinds[k], kinds[k], arg => this.toggleProjection(arg));
       menu.add('endsub:');
@@ -120551,14 +120599,15 @@ let RH2Painter$2 = class RH2Painter extends RHistPainter {
                   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 (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:rect')
+            ttrect = this.draw_g.append('svg:path')
                                 .attr('class','tooltip_bin h1bin')
                                 .style('pointer-events','none');
 
@@ -120566,35 +120615,42 @@ let RH2Painter$2 = class RH2Painter extends RHistPainter {
              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;
+             binid = i*10000 + j,
+             pmain = this.getFramePainter(),
+             path;
 
-         if (this.is_projection == 'X') {
-            x1 = 0; x2 = this.getFramePainter().getFrameWidth();
-            if (this.projection_width > 1) {
-               let dd = (this.projection_width-1)/2;
-               if (j2+dd >= h.j2) { j2 = Math.min(Math.round(j2+dd), h.j2); j1 = Math.max(j2 - this.projection_width, h.j1); }
-                             else { j1 = Math.max(Math.round(j1-dd), h.j1); j2 = Math.min(j1 + this.projection_width, h.j2); }
+         if (this.is_projection) {
+            let 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); }
+            }
+            let 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 = this.getFramePainter().getFrameHeight();
-            if (this.projection_width > 1) {
-               let dd = (this.projection_width-1)/2;
-               if (i2+dd >= h.i2) { i2 = Math.min(Math.round(i2+dd), h.i2); i1 = Math.max(i2 - this.projection_width, h.i1); }
-                             else { i1 = Math.max(Math.round(i1-dd), h.i1); i2 = Math.min(i1 + this.projection_width, h.i2); }
-            }
-            x1 = h.grx[i1], x2 = h.grx[i2],
+            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('x', x1)
-                  .attr('width', x2 - x1)
-                  .attr('y', y1)
-                  .attr('height', y2 - y1)
+            ttrect.attr('d', path || `M${x1},${y1}H${x2}V${y2}H${x1}Z`)
                   .style('opacity', '0.7')
                   .property('current_bin', binid);
 
diff --git a/js/changes.md b/js/changes.md
index fcc97edb408..3bdf3ffb6c8 100644
--- a/js/changes.md
+++ b/js/changes.md
@@ -15,7 +15,7 @@
 12. Adjust TAxis title positioning to native ROOT behavior
 13. Let change physical node visibility in TGeo drawings
 14. Use TGaxis attributes from gStyle - fAxisMaxDigits, fStripDecimals and exponent offset
-15. Implement "projxy" draw option for TH2
+15. Implement "projxy" draw option for TH2 - like projxy3 or projx1_y5
 
 
 ## Changes in 7.3.0
diff --git a/js/modules/hist2d/RH2Painter.mjs b/js/modules/hist2d/RH2Painter.mjs
index 78ea6968841..ca20fab29c7 100644
--- a/js/modules/hist2d/RH2Painter.mjs
+++ b/js/modules/hist2d/RH2Painter.mjs
@@ -33,20 +33,40 @@ class RH2Painter extends RHistPainter {
    /** @summary Toggle projection */
    toggleProjection(kind, width) {
 
-      if (kind == 'Projections') kind = '';
-
-      if (isStr(kind) && (kind.length > 1)) {
-          width = parseInt(kind.slice(1));
-          kind = kind[0];
+      if ((kind == 'Projections') || (kind == 'Off'))
+         kind = '';
+
+      let widthX = width, widthY = width;
+
+      if (isStr(kind) && (kind.indexOf('XY') == 0)) {
+         let ws = kind.length > 2 ? kind.slice(2) : '';
+         kind = 'XY';
+         widthX = widthY = parseInt(ws);
+      } else if (isStr(kind) && (kind.length > 1)) {
+         let 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 (!width) width = 1;
+      if (!widthX && !widthY)
+         widthX = widthY = 1;
 
-      if (kind && (this.is_projection==kind)) {
-         if (this.projection_width === width) {
+      if (kind && (this.is_projection == kind)) {
+         if ((this.projection_widthX === widthX) && (this.projection_widthY === widthY)) {
             kind = '';
          } else {
-            this.projection_width = width;
+            this.projection_widthX = widthX;
+            this.projection_widthY = widthY;
             return;
          }
       }
@@ -54,8 +74,9 @@ class RH2Painter extends RHistPainter {
       delete this.proj_hist;
 
       let new_proj = (this.is_projection === kind) ? '' : kind;
-      this.is_projection = ''; // disable projection redraw until callback
-      this.projection_width = width;
+      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(); });
    }
@@ -81,10 +102,16 @@ class RH2Painter extends RHistPainter {
 
    /** @summary Fill histogram context menu */
    fillHistContextMenu(menu) {
-      menu.add('sub:Projections', () => this.toggleProjection());
       let kind = this.is_projection || '';
-      if (kind) kind += this.projection_width;
-      let kinds = ['X1', 'X2', 'X3', 'X5', 'X10', 'Y1', 'Y2', 'Y3', 'Y5', 'Y10'];
+      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}`;
+
+      menu.add('sub:Projections', () => menu.input('Input projection kind X1 or XY2', kind, 'string').then(val => this.toggleProjection(val)));
+
+      let kinds = ['X1', 'X2', 'X3', 'X5', 'X10', 'Y1', 'Y2', 'Y3', 'Y5', 'Y10', 'XY1', 'XY2', 'XY3', 'XY5', 'XY10'];
+      if (kind) kinds.unshift('Off');
+
       for (let k = 0; k < kinds.length; ++k)
          menu.addchk(kind == kinds[k], kinds[k], kinds[k], arg => this.toggleProjection(arg));
       menu.add('endsub:');
@@ -1229,14 +1256,15 @@ class RH2Painter extends RHistPainter {
                   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 (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:rect')
+            ttrect = this.draw_g.append('svg:path')
                                 .attr('class','tooltip_bin h1bin')
                                 .style('pointer-events','none');
 
@@ -1244,35 +1272,42 @@ class RH2Painter extends RHistPainter {
              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;
+             binid = i*10000 + j,
+             pmain = this.getFramePainter(),
+             path;
 
-         if (this.is_projection == 'X') {
-            x1 = 0; x2 = this.getFramePainter().getFrameWidth();
-            if (this.projection_width > 1) {
-               let dd = (this.projection_width-1)/2;
-               if (j2+dd >= h.j2) { j2 = Math.min(Math.round(j2+dd), h.j2); j1 = Math.max(j2 - this.projection_width, h.j1); }
-                             else { j1 = Math.max(Math.round(j1-dd), h.j1); j2 = Math.min(j1 + this.projection_width, h.j2); }
+         if (this.is_projection) {
+            let 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); }
+            }
+            let 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 = this.getFramePainter().getFrameHeight();
-            if (this.projection_width > 1) {
-               let dd = (this.projection_width-1)/2;
-               if (i2+dd >= h.i2) { i2 = Math.min(Math.round(i2+dd), h.i2); i1 = Math.max(i2 - this.projection_width, h.i1); }
-                             else { i1 = Math.max(Math.round(i1-dd), h.i1); i2 = Math.min(i1 + this.projection_width, h.i2); }
-            }
-            x1 = h.grx[i1], x2 = h.grx[i2],
+            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('x', x1)
-                  .attr('width', x2 - x1)
-                  .attr('y', y1)
-                  .attr('height', y2 - y1)
+            ttrect.attr('d', path || `M${x1},${y1}H${x2}V${y2}H${x1}Z`)
                   .style('opacity', '0.7')
                   .property('current_bin', binid);
 
diff --git a/js/modules/hist2d/TH2Painter.mjs b/js/modules/hist2d/TH2Painter.mjs
index 9e32a1ed1d0..877947ad6f2 100644
--- a/js/modules/hist2d/TH2Painter.mjs
+++ b/js/modules/hist2d/TH2Painter.mjs
@@ -36,22 +36,37 @@ class TH2Painter extends THistPainter {
       if ((kind == 'Projections') || (kind == 'Off'))
          kind = '';
 
+      let widthX = width, widthY = width;
+
       if (isStr(kind) && (kind.indexOf('XY') == 0)) {
-         if (kind.length > 2)
-            width = parseInt(kind.slice(2));
+         let ws = (kind.length > 2) ? kind.slice(2) : '';
          kind = 'XY';
+         widthX = widthY = parseInt(ws) || 1;
       } else if (isStr(kind) && (kind.length > 1)) {
-         width = parseInt(kind.slice(1));
-         kind = kind[0];
+         let 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 (!width) width = 1;
+      if (!widthX && !widthY)
+         widthX = widthY = 1;
 
       if (kind && (this.is_projection == kind)) {
-         if (this.projection_width === width) {
+         if ((this.projection_widthX === widthX) && (this.projection_widthY === widthY)) {
             kind = '';
          } else {
-            this.projection_width = width;
+            this.projection_widthX = widthX;
+            this.projection_widthY = widthY;
             return;
          }
       }
@@ -59,7 +74,8 @@ class TH2Painter extends THistPainter {
       delete this.proj_hist;
 
       let new_proj = (this.is_projection === kind) ? '' : kind;
-      this.projection_width = width;
+      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(); });
@@ -188,11 +204,15 @@ class TH2Painter extends THistPainter {
    /** @summary Fill histogram context menu */
    fillHistContextMenu(menu) {
       if (!this.isTH2Poly()) {
-         menu.add('sub:Projections', () => this.toggleProjection());
          let kind = this.is_projection || '';
-         if (kind) kind += this.projection_width;
+         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 (this.is_projection) kinds.push('Off');
+         if (kind) kinds.unshift('Off');
+
+         menu.add('sub:Projections', () => menu.input('Input projection kind X1 or XY2', 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.add('endsub:');
@@ -2638,14 +2658,15 @@ class TH2Painter extends THistPainter {
          let binid = i*10000 + j, path;
 
          if (this.is_projection) {
-            let pw = this.projection_width || 1, dd = (pw - 1) / 2;
-            if ((this.is_projection.indexOf('X')) >= 0 && (pw > 1)) {
-               if (j2+dd >= h.j2) { j2 = Math.min(Math.round(j2+dd), h.j2); j1 = Math.max(j2 - pw, h.j1); }
-                             else { j1 = Math.max(Math.round(j1-dd), h.j1); j2 = Math.min(j1 + pw, h.j2); }
+            let 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); }
             }
-            if ((this.is_projection.indexOf('Y')) >= 0 && (pw > 1)) {
-               if (i2+dd >= h.i2) { i2 = Math.min(Math.round(i2+dd), h.i2); i1 = Math.max(i2 - pw, h.i1); }
-                             else { i1 = Math.max(Math.round(i1-dd), h.i1); i2 = Math.min(i1 + pw, h.i2); }
+            let 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); }
             }
          }
 
diff --git a/js/modules/hist2d/THistPainter.mjs b/js/modules/hist2d/THistPainter.mjs
index 43ccb8d4277..34ac9a9b891 100644
--- a/js/modules/hist2d/THistPainter.mjs
+++ b/js/modules/hist2d/THistPainter.mjs
@@ -471,8 +471,8 @@ class THistDrawOptions {
       if (this.Proj > 0) this.Contour = 14;
 
       if (d.check('PROJXY', true)) this.Project = 'XY' + d.partAsInt(0,1);
-      if (d.check('PROJX', true)) this.Project = 'X' + d.partAsInt(0,1);
-      if (d.check('PROJY', true)) this.Project = 'Y' + d.partAsInt(0,1);
+      if (d.check('PROJX', true)) this.Project = 'X' + d.part;
+      if (d.check('PROJY', true)) this.Project = 'Y' + d.part;
       if (d.check('PROJ')) this.Project = 'Y1';
 
       if (check3dbox) {
-- 
GitLab