hist/TPavePainter.mjs

import { gStyle, browser, settings, clone, isObject, isFunc, isStr, BIT,
         clTPave, clTPaveText, clTPavesText, clTPaveStats, clTPaveLabel, clTPaveClass, clTDiamond, clTLegend, clTPaletteAxis,
         clTText, clTLatex, clTLine, clTBox, kTitle, isNodeJs, nsSVG } 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 { getColorExec } from '../gui/utils.mjs';
import { TAxisPainter } from '../gpad/TAxisPainter.mjs';
import { addDragHandler } from '../gpad/TFramePainter.mjs';
import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs';


const kTakeStyle = BIT(17), kPosTitle = 'postitle', kAutoPlace = 'autoplace', kDefaultDrawOpt = 'brNDC';

/** @summary Returns true if stat box on default place and can be adjusted
  * @private */
function isDefaultStatPosition(pt) {
   const test = (v1, v2) => (Math.abs(v1-v2) < 1e-3);
   return test(pt.fX1NDC, gStyle.fStatX - gStyle.fStatW) &&
          test(pt.fY1NDC, gStyle.fStatY - gStyle.fStatH) &&
          test(pt.fX2NDC, gStyle.fStatX) &&
          test(pt.fY2NDC, gStyle.fStatY);
}

/**
 * @summary painter for TPave-derived classes
 *
 * @private
 */


class TPavePainter extends ObjectPainter {

   /** @summary constructor
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} pave - TPave-based object */
   constructor(dom, pave, opt) {
      super(dom, pave, opt);
      this.Enabled = true;
      this.UseContextMenu = true;
   }

   /** @summary Auto place legend on the frame
     * @return {Promise} with boolean flag if position was changed  */
   async autoPlaceLegend(pt, pad, keep_origin) {
      const main_svg = this.getFrameSvg().selectChild('.main_layer');

      let svg_code = main_svg.node().outerHTML;

      svg_code = compressSVG(svg_code);

      svg_code = `<svg xmlns="${nsSVG}"` + svg_code.slice(4);

      const lm = pad?.fLeftMargin ?? gStyle.fPadLeftMargin,
            rm = pad?.fRightMargin ?? gStyle.fPadRightMargin,
            tm = pad?.fTopMargin ?? gStyle.fPadTopMargin,
            bm = pad?.fBottomMargin ?? gStyle.fPadBottomMargin;

      return svgToImage(svg_code).then(canvas => {
         if (!canvas) return false;

         let nX = 100, nY = 100;
         const context = canvas.getContext('2d'),
               arr = context.getImageData(0, 0, canvas.width, canvas.height).data,
               boxW = Math.floor(canvas.width / nX), boxH = Math.floor(canvas.height / nY),
               raster = new Array(nX*nY);

         if (arr.length !== canvas.width * canvas.height * 4) {
            console.log(`Image size missmatch in TLegend autoplace ${arr.length} expected ${canvas.width*canvas.height * 4}`);
            nX = nY = 0;
         }

         for (let ix = 0; ix < nX; ++ix) {
            const px1 = ix * boxW, px2 = px1 + boxW;
            for (let iy = 0; iy < nY; ++iy) {
               const py1 = iy * boxH, py2 = py1 + boxH;
               let filled = 0;

               for (let x = px1; (x < px2) && !filled; ++x) {
                  for (let y = py1; y < py2; ++y) {
                     const indx = (y * canvas.width + x) * 4;
                     if (arr[indx] || arr[indx+1] || arr[indx+2] || arr[indx+3]) {
                        filled = 1;
                        break;
                     }
                  }
                }
                raster[iy * nX + ix] = filled;
            }
         }

         const legWidth = 0.3 / Math.max(0.2, (1 - lm - rm)),
               legHeight = Math.min(0.5, Math.max(0.1, pt.fPrimitives.arr.length*0.05)) / Math.max(0.2, (1 - tm - bm)),
               needW = Math.round(legWidth * nX), needH = Math.round(legHeight * nY),

          test = (x, y) => {
            for (let ix = x; ix < x + needW; ++ix) {
               for (let iy = y; iy < y + needH; ++iy)
                  if (raster[iy * nX + ix]) return false;
            }
            return true;
         };

         for (let ix = 0; ix < (nX - needW); ++ix) {
            for (let iy = nY-needH - 1; iy >= 0; --iy) {
               if (test(ix, iy)) {
                  pt.fX1NDC = lm + ix / nX * (1 - lm - rm);
                  pt.fX2NDC = pt.fX1NDC + legWidth * (1 - lm - rm);
                  pt.fY2NDC = 1 - tm - iy/nY * (1 - bm - tm);
                  pt.fY1NDC = pt.fY2NDC - legHeight * (1 - bm - tm);
                  return true;
               }
            }
         }
      }).then(res => {
         if (res || keep_origin)
            return res;

         pt.fX1NDC = Math.max(lm ?? 0, pt.fX2NDC - 0.3);
         pt.fX2NDC = Math.min(pt.fX1NDC + 0.3, 1 - rm);
         const h0 = Math.max(pt.fPrimitives ? pt.fPrimitives.arr.length*0.05 : 0, 0.2);
         pt.fY2NDC = Math.min(1 - tm, pt.fY1NDC + h0);
         pt.fY1NDC = Math.max(pt.fY2NDC - h0, bm);
         return true;
      });
   }

   /** @summary Get draw option for the pave
     * @desc only stats using fOption directly, all other classes - stored in the pad */
   getPaveDrawOption() {
      let opt = this.getDrawOpt();
      if (this.isStats() || !opt)
         opt = this.getObject()?.fOption;
      return opt || kDefaultDrawOpt;
   }

   /** @summary Change pave draw option */
   setPaveDrawOption(opt) {
      if (this.isStats())
         this.getObject().fOption = opt;
      else
         this.storeDrawOpt(opt);
   }

   /** @summary Draw pave and content
     * @return {Promise} */
   async drawPave(arg) {
      if (!this.Enabled) {
         this.removeG();
         return this;
      }

      const pt = this.getObject(),
            opt = this.getPaveDrawOption().toUpperCase(),
            fp = this.getFramePainter(), pp = this.getPadPainter(),
            pad = pp.getRootPad(true);
      let interactive_element, width, height;

      if (pt.fInit === 0) {
         this.stored = Object.assign({}, pt); // store coordinates to use them when updating
         pt.fInit = 1;

         if ((pt._typename === clTPaletteAxis) && !pt.fX1 && !pt.fX2 && !pt.fY1 && !pt.fY2) {
            if (fp) {
               pt.fX1NDC = fp.fX2NDC + 0.01;
               pt.fX2NDC = Math.min(0.96, fp.fX2NDC + 0.06);
               pt.fY1NDC = fp.fY1NDC;
               pt.fY2NDC = fp.fY2NDC;
            } else {
               pt.fX2NDC = 0.8;
               pt.fX1NDC = 0.9;
               pt.fY1NDC = 0.1;
               pt.fY2NDC = 0.9;
            }
         } else if (pt.fOption.indexOf('NDC') >= 0) {
            // check if NDC was modified but fInit was not set
            // wired - ROOT checks fOption even when absolutely different draw option may be specified,
            // happens in stressGraphics.cxx, sg30 where stats box not initialized when call C->Update() in batch mode
            if (pt.fX1NDC < 1e-20 && pt.fX2NDC < 1e-20) {
               pt.fX1NDC = pt.fX1;
               pt.fX2NDC = pt.fX2;
            }
            if (pt.fY1NDC < 1e-20 && pt.fY2NDC < 1e-20) {
               pt.fY1NDC = pt.fY1;
               pt.fY2NDC = pt.fY2;
            }
         } else if (pad && (pad.fX1 === 0) && (pad.fX2 === 1) && (pad.fY1 === 0) && (pad.fY2 === 1) && isStr(arg) && (arg.indexOf('postpone') >= 0)) {
            // special case when pad not yet initialized
            pt.fInit = 0; // do not init until axes drawn
            pt.fX1NDC = pt.fY1NDC = 0.99;
            pt.fX2NDC = pt.fY2NDC = 1;
         } else if (pad) {
            if (pad.fLogx) {
               if (pt.fX1 > 0) pt.fX1 = Math.log10(pt.fX1);
               if (pt.fX2 > 0) pt.fX2 = Math.log10(pt.fX2);
            }
            if (pad.fLogy) {
               if (pt.fY1 > 0) pt.fY1 = Math.log10(pt.fY1);
               if (pt.fY2 > 0) pt.fY2 = Math.log10(pt.fY2);
            }
            pt.fX1NDC = (pt.fX1 - pad.fX1) / (pad.fX2 - pad.fX1);
            pt.fY1NDC = (pt.fY1 - pad.fY1) / (pad.fY2 - pad.fY1);
            pt.fX2NDC = (pt.fX2 - pad.fX1) / (pad.fX2 - pad.fX1);
            pt.fY2NDC = (pt.fY2 - pad.fY1) / (pad.fY2 - pad.fY1);
         } else {
            pt.fX1NDC = pt.fY1NDC = 0.1;
            pt.fX2NDC = pt.fY2NDC = 0.9;
         }
      }

      let promise = Promise.resolve(true);

      if ((pt._typename === clTLegend) && (this.AutoPlace || ((pt.fX1NDC === pt.fX2NDC) && (pt.fY1NDC === pt.fY2NDC)))) {
         promise = this.autoPlaceLegend(pt, pad).then(res => {
            delete this.AutoPlace;
            if (!res) {
               pt.fX1NDC = fp.fX2NDC - 0.2; pt.fX2NDC = fp.fX2NDC;
               pt.fY1NDC = fp.fY2NDC - 0.1; pt.fY2NDC = fp.fY2NDC;
            }
            return res;
         });
      }

      return promise.then(() => {
         // fill stats before drawing to have coordinates early
         if (this.isStats() && !this.NoFillStats && !pp._fast_drawing) {
            const main = pt.$main_painter || this.getMainPainter();

            if (isFunc(main?.fillStatistic)) {
               let dostat = pt.fOptStat, dofit = pt.fOptFit;
               if (pt.TestBit(kTakeStyle) || !Number.isInteger(dostat)) dostat = gStyle.fOptStat;
               if (pt.TestBit(kTakeStyle) || !Number.isInteger(dofit)) dofit = gStyle.fOptFit;

               // we take statistic from main painter
               if (main.fillStatistic(this, dostat, dofit)) {
                  // adjust the size of the stats box with the number of lines
                  let nlines = pt.fLines?.arr.length || 0;
                  const set_default = (nlines > 0) && !this.moved_interactive && isDefaultStatPosition(pt),
                        // in ROOT TH2 and TH3 always add full stats for fit parameters
                        extrah = this._has_fit && (this._fit_dim > 1) ? gStyle.fStatH : 0;
                  if (extrah) nlines -= this._fit_cnt;
                  let stath = gStyle.fStatH, statw = gStyle.fStatW;
                  if (this._has_fit)
                     statw = 1.8 * gStyle.fStatW;
                  if ((gStyle.fStatFontSize <= 0) || (gStyle.fStatFont % 10 === 3))
                     stath = nlines * 0.25 * gStyle.fStatH;
                  else if (gStyle.fStatFontSize < 1)
                     stath = nlines * gStyle.fStatFontSize;

                  if (set_default) {
                     // but fit parameters not used in full size calculations
                     pt.fX1NDC = Math.max(0.005, pt.fX2NDC - statw);
                     pt.fY1NDC = Math.max(0.005, pt.fY2NDC - stath - extrah);
                  } else {
                     // when some NDC values are set directly and not match with each other
                     if (pt.fY1NDC > pt.fY2NDC)
                        pt.fY2NDC = Math.min(0.995, pt.fY1NDC + stath + extrah);
                     if (pt.fX1NDC > pt.fX2NDC)
                        pt.fY2NDC = Math.min(0.995, pt.fX1NDC + statw);
                  }
               }
            }
         }

         const pad_rect = pp.getPadRect(),
               brd = pt.fBorderSize,
               noborder = opt.indexOf('NB') >= 0,
               dx = (opt.indexOf('L') >= 0) ? -1 : ((opt.indexOf('R') >= 0) ? 1 : 0),
               dy = (opt.indexOf('T') >= 0) ? -1 : ((opt.indexOf('B') >= 0) ? 1 : 0);

         // container used to recalculate coordinates
         this.createG();

         this._pave_x = Math.round(pt.fX1NDC * pad_rect.width);
         this._pave_y = Math.round((1.0 - pt.fY2NDC) * pad_rect.height);
         width = Math.round((pt.fX2NDC - pt.fX1NDC) * pad_rect.width);
         height = Math.round((pt.fY2NDC - pt.fY1NDC) * pad_rect.height);

         const arc_radius = opt.indexOf('ARC') >= 0 && (pt.fCornerRadius > 0) ? Math.round(Math.min(width, height) * pt.fCornerRadius) : 0;

         makeTranslate(this.draw_g, this._pave_x, this._pave_y);

         this.createAttLine({ attr: pt, width: (brd > 0) ? pt.fLineWidth : 0 });

         this.createAttFill({ attr: pt });
         // need to fill pave while
         if (this.fillatt.empty() && arc_radius)
            this.fillatt.setSolidColor(this.getColor(pt.fFillColor) || 'white');

         if (pt._typename === clTDiamond) {
            const h2 = Math.round(height/2), w2 = Math.round(width/2),
                  dpath = `l${w2},${-h2}l${w2},${h2}l${-w2},${h2}z`;

            if (!this.fillatt.empty())
               this.drawBorder(this.draw_g, width, height, 0, dpath);

            interactive_element = this.draw_g.append('svg:path')
                                      .attr('d', 'M0,'+h2 +dpath)
                                      .call(this.fillatt.func)
                                      .call(this.lineatt.func);

            const text_g = this.draw_g.append('svg:g');
            makeTranslate(text_g, Math.round(width/4), Math.round(height/4));

            return this.drawPaveText(w2, h2, arg, text_g);
         }

         if (pt.fNpaves) {
            for (let n = pt.fNpaves-1; n > 0; --n) {
               this.draw_g.append('svg:path')
                     .attr('d', `M${dx*4*n},${dy*4*n}h${width}v${height}h${-width}z`)
                     .call(this.fillatt.func)
                     .call(this.lineatt.func);
            }
         } else
            this.drawBorder(this.draw_g, width, height, arc_radius);

         if (!this.isBatchMode() || !this.fillatt.empty() || (!this.lineatt.empty() && !noborder)) {
            if (arc_radius) {
               interactive_element = this.draw_g.append('svg:rect')
                                          .attr('width', width)
                                          .attr('height', height)
                                          .attr('rx', arc_radius);
            } else {
               interactive_element = this.draw_g.append('svg:path')
                                                .attr('d', `M0,0H${width}V${height}H0Z`);
            }
            interactive_element.call(this.fillatt.func);
            if (!noborder)
               interactive_element.call(this.lineatt.func);
         }

         return isFunc(this.paveDrawFunc) ? this.paveDrawFunc(width, height, arg) : true;
      }).then(() => {
         if (this.isBatchMode() || (pt._typename === clTPave))
            return this;

         // here all kind of interactive settings
         interactive_element?.style('pointer-events', 'visibleFill')
                             .on('mouseenter', () => this.showObjectStatus());

         addDragHandler(this, { obj: pt, x: this._pave_x, y: this._pave_y, width, height,
                                minwidth: 10, minheight: 20, canselect: true,
                        redraw: () => { this.moved_interactive = true; this.interactiveRedraw(false, 'pave_moved'); this.drawPave(); },
                        ctxmenu: browser.touches && settings.ContextMenu && this.UseContextMenu });

         if (this.UseContextMenu && settings.ContextMenu)
             this.draw_g.on('contextmenu', evnt => this.paveContextMenu(evnt));

         if (this.isPalette())
            this.interactivePaletteAxis(width, height);

         return this;
      });
   }

   drawBorder(draw_g, width, height, arc_radius, diamond) {
      const pt = this.getObject(),
            opt = this.getPaveDrawOption().toUpperCase().replaceAll('ARC', '').replaceAll('NDC', ''),
            noborder = this.isPalette() || (opt.indexOf('NB') >= 0),
            dx = (opt.indexOf('L') >= 0) ? -1 : ((opt.indexOf('R') >= 0) ? 1 : 0),
            dy = (opt.indexOf('T') >= 0) ? -1 : ((opt.indexOf('B') >= 0) ? 1 : 0);

      if ((pt.fBorderSize < 2) || (pt.fShadowColor === 0) || (!dx && !dy) || noborder)
         return;

      const scol = this.getColor(pt.fShadowColor),
            brd = pt.fBorderSize,
            brd_width = !this.lineatt.empty() && (this.lineatt.width > 2) ? `${this.lineatt.width-1}px` : '1px';

      if (diamond) {
         draw_g.append('svg:path')
               .attr('d', `M0,${Math.round(height/2)+brd}${diamond}`)
               .style('fill', scol)
               .style('stroke', scol)
               .style('stroke-width', brd_width);
      } else if (arc_radius) {
         draw_g.append('svg:rect')
               .attr('width', width)
               .attr('height', height)
               .attr('rx', arc_radius)
               .attr('x', dx*brd)
               .attr('y', dy*brd)
               .style('fill', scol)
               .style('stroke', scol)
               .style('stroke-width', brd_width);
      } else {
         let spath;

         if ((dx < 0) && (dy < 0))
            spath = `M0,0v${height-brd-1}h${-brd+1}v${-height+2}h${width-2}v${brd-1}z`;
         else if ((dx < 0) && (dy > 0))
            spath = `M0,${height}v${brd+1-height}h${-brd+1}v${height-2}h${width-2}v${-brd+1}z`;
         else if ((dx > 0) && (dy < 0))
            spath = `M${brd+1},0v${-brd+1}h${width-2}v${height-2}h${-brd+1}v${brd+1-height}z`;
         else
            spath = `M${width},${brd+1}h${brd-1}v${height-2}h${-width+2}v${-brd+1}h${width-brd-2}z`;

         draw_g.append('svg:path')
               .attr('d', spath)
               .style('fill', scol)
               .style('stroke', scol)
               .style('stroke-width', brd_width);
      }
   }

   /** @summary Fill option object used in TWebCanvas */
   fillWebObjectOptions(res) {
      const pave = this.getObject();

      if (pave?.fInit) {
         res.fcust = 'pave';
         res.fopt = [pave.fX1NDC, pave.fY1NDC, pave.fX2NDC, pave.fY2NDC];

         if ((pave.fName === 'stats') && this.isStats()) {
             pave.fLines.arr.forEach(entry => {
                if ((entry._typename === clTText) || (entry._typename === clTLatex))
                   res.fcust += `;;${entry.fTitle}`;
             });
         }
      }

      return res;
   }

   /** @summary draw TPaveLabel object */
   async drawPaveLabel(width, height) {
      const pave = this.getObject();
      if (!pave.fLabel || !pave.fLabel.trim())
         return this;

      this.createAttText({ attr: pave, can_rotate: false });

      return this.startTextDrawingAsync(this.textatt.font, height/1.2)
                 .then(() => this.drawText(this.textatt.createArg({ width, height, text: pave.fLabel, norotate: true })))
                 .then(() => this.finishTextDrawing());
   }

   /** @summary draw TPaveStats object */
   drawPaveStats(width, height) {
      const pt = this.getObject(), lines = [], colors = [];
      let first_stat = 0, num_cols = 0, maxlen = 0;

      // extract only text
      for (let j = 0; j < pt.fLines.arr.length; ++j) {
         const entry = pt.fLines.arr[j];
         if ((entry._typename === clTText) || (entry._typename === clTLatex)) {
            lines.push(entry.fTitle);
            colors.push(entry.fTextColor);
          }
      }

      const nlines = lines.length;

      // adjust font size
      for (let j = 0; j < nlines; ++j) {
         const line = lines[j];
         if (j > 0) maxlen = Math.max(maxlen, line.length);
         if ((j === 0) || (line.indexOf('|') < 0)) continue;
         if (first_stat === 0) first_stat = j;
         const parts = line.split('|');
         if (parts.length > num_cols)
            num_cols = parts.length;
      }

      // for characters like 'p' or 'y' several more pixels required to stay in the box when drawn in last line
      const stepy = height / nlines, margin_x = pt.fMargin * width;
      let has_head = false;

      this.createAttText({ attr: pt, can_rotate: false });

      return this.startTextDrawingAsync(this.textatt.font, height/(nlines * 1.2)).then(() => {
         if (nlines === 1)
            this.drawText(this.textatt.createArg({ width, height, text: lines[0], latex: 1, norotate: true }));
         else {
            for (let j = 0; j < nlines; ++j) {
               const y = j*stepy,
                     color = (colors[j] > 1) ? this.getColor(colors[j]) : this.textatt.color;

               if (first_stat && (j >= first_stat)) {
                  const parts = lines[j].split('|');
                  for (let n = 0; n < parts.length; ++n) {
                     this.drawText({ align: 'middle', x: width * n / num_cols, y, latex: 0,
                                    width: width/num_cols, height: stepy, text: parts[n], color });
                  }
               } else if (lines[j].indexOf('=') < 0) {
                  if (j === 0) {
                     has_head = true;
                     const max_hlen = Math.max(maxlen, Math.round((width-2*margin_x)/stepy/0.65));
                     if (lines[j].length > max_hlen + 5)
                        lines[j] = lines[j].slice(0, max_hlen+2) + '...';
                  }
                  this.drawText({ align: (j === 0) ? 'middle' : 'start', x: margin_x, y,
                                 width: width-2*margin_x, height: stepy, text: lines[j], color });
               } else {
                  const parts = lines[j].split('='), args = [];

                  for (let n = 0; n < 2; ++n) {
                     const arg = {
                        align: (n === 0) ? 'start' : 'end', x: margin_x, y,
                        width: width - 2*margin_x, height: stepy, text: n > 0 ? parts[n].trimStart() : parts[n].trimEnd(), color,
                        _expected_width: width-2*margin_x, _args: args,
                        post_process(painter) {
                           if (this._args[0].ready && this._args[1].ready)
                              painter.scaleTextDrawing(1.05*(this._args[0].result_width+this._args[1].result_width)/this._expected_width, painter.draw_g);
                        }
                     };
                     args.push(arg);
                  }

                  for (let n = 0; n < 2; ++n)
                     this.drawText(args[n]);
               }
            }
         }

         let lpath = '';

         if ((pt.fBorderSize > 0) && has_head)
            lpath += `M0,${Math.round(stepy)}h${width}`;

         if ((first_stat > 0) && (num_cols > 1)) {
            for (let nrow = first_stat; nrow < nlines; ++nrow)
               lpath += `M0,${Math.round(nrow * stepy)}h${width}`;
            for (let ncol = 0; ncol < num_cols - 1; ++ncol)
               lpath += `M${Math.round(width / num_cols * (ncol + 1))},${Math.round(first_stat * stepy)}V${height}`;
         }

         if (lpath) this.draw_g.append('svg:path').attr('d', lpath).call(this.lineatt.func);

         // this.draw_g.classed('most_upper_primitives', true); // this primitive will remain on top of list

         return this.finishTextDrawing(undefined, (nlines > 1));
      });
   }

   /** @summary draw TPaveText object */
   async drawPaveText(width, height, _dummy_arg, text_g) {
      const pt = this.getObject(),
            arr = pt.fLines?.arr || [],
            nlines = arr.length,
            pp = this.getPadPainter(),
            pad_height = pp.getPadHeight(),
            draw_header = (pt.fLabel.length > 0),
            promises = [],
            margin_x = pt.fMargin * width,
            stepy = height / (nlines || 1),
            dflt_font_size = 0.85 * stepy;
      let max_font_size = 0;

      this.createAttText({ attr: pt, can_rotate: false });

      // for single line (typically title) limit font size
      if ((nlines === 1) && (this.textatt.size > 0))
         max_font_size = Math.max(3, this.textatt.getSize(pp));

      if (!text_g)
         text_g = this.draw_g;

      const fast = (nlines === 1) && pp._fast_drawing;
      let num_txt = 0, num_custom = 0, longest_line = 0, alt_text_size = 0;

      arr.forEach(entry => {
         if (((entry._typename !== clTText) && (entry._typename !== clTLatex)) || !entry.fTitle?.trim())
            return;
         num_txt++;
         if (entry.fX || entry.fY || entry.fTextSize)
            num_custom++;

         if (!entry.fTextSize && !this.textatt.size)
            longest_line = Math.max(longest_line, approximateLabelWidth(entry.fTitle, this.textatt.font, dflt_font_size));
      });

      if (longest_line) {
         alt_text_size = dflt_font_size;
         if (longest_line > 0.92 * width)
            alt_text_size *= (0.92 * width / longest_line);
         alt_text_size = Math.round(alt_text_size);
      }

      const pr = (num_txt > num_custom) ? this.startTextDrawingAsync(this.textatt.font, this.$postitle ? this.textatt.getSize(pp, 1, 0.05) : dflt_font_size, text_g, max_font_size) : Promise.resolve();

      return pr.then(() => {
         for (let nline = 0; nline < nlines; ++nline) {
            const entry = arr[nline], texty = nline*stepy;

            switch (entry._typename) {
               case clTText:
               case clTLatex: {
                  if (!entry.fTitle || !entry.fTitle.trim()) continue;

                  let color = entry.fTextColor ? this.getColor(entry.fTextColor) : '';
                  if (!color) color = this.textatt.color;
                  const align = entry.fTextAlign || this.textatt.align,
                        valign = align % 10,
                        halign = (align - valign) / 10;

                  if (entry.fX || entry.fY || entry.fTextSize) {
                     // individual positioning
                     const x = entry.fX ? entry.fX*width : (halign === 1 ? margin_x : (halign === 2 ? width / 2 : width - margin_x)),
                           y = entry.fY ? (1 - entry.fY)*height : (texty + (valign === 2 ? stepy / 2 : (valign === 3 ? stepy : 0))),
                           draw_g = text_g.append('svg:g');

                     promises.push(this.startTextDrawingAsync(this.textatt.font, this.textatt.getAltSize(entry.fTextSize, pp) || alt_text_size, draw_g)
                                       .then(() => this.drawText({ align, x, y, text: entry.fTitle, color,
                                                                   latex: (entry._typename === clTText) ? 0 : 1, draw_g, fast }))
                                       .then(() => this.finishTextDrawing(draw_g)));
                  } else {
                     const arg = { x: 0, y: texty, draw_g: text_g,
                                   latex: (entry._typename === clTText) ? 0 : 1,
                                   text: entry.fTitle, color, fast };

                     if (this.$postitle) {
                        // remember box produced by title text
                        arg.post_process = function(painter) {
                           painter.$titlebox = this.box;
                        };
                     } else {
                        arg.align = align;
                        arg.x = (halign === 1) ? margin_x : 0;
                        arg.width = (halign === 2) ? width : width - margin_x;
                        arg.y = texty + 0.05 * stepy;
                        arg.height = 0.9*stepy;
                        // prevent expand of normal title on full width
                        // if (this.isTitle() && (halign === 2) && (arg.width > 0.1*pad_width) && (arg.width < 0.7*pad_width)) {
                        //   arg.width -= 0.02*pad_width;
                        //   arg.x = 0.01*pad_width;
                        // }
                     }

                     this.drawText(arg);
                  }
                  break;
               }

               case clTLine: {
                  const lx1 = entry.fX1 ? Math.round(entry.fX1*width) : 0,
                        lx2 = entry.fX2 ? Math.round(entry.fX2*width) : width,
                        ly1 = entry.fY1 ? Math.round((1 - entry.fY1)*height) : Math.round(texty + stepy*0.5),
                        ly2 = entry.fY2 ? Math.round((1 - entry.fY2)*height) : Math.round(texty + stepy*0.5),
                        lineatt = this.createAttLine(entry);
                  text_g.append('svg:path')
                        .attr('d', `M${lx1},${ly1}L${lx2},${ly2}`)
                        .call(lineatt.func);
                  break;
               }
               case clTBox: {
                  const bx1 = entry.fX1 ? Math.round(entry.fX1*width) : 0,
                        bx2 = entry.fX2 ? Math.round(entry.fX2*width) : width,
                        by1 = entry.fY1 ? Math.round((1 - entry.fY1)*height) : Math.round(texty),
                        by2 = entry.fY2 ? Math.round((1 - entry.fY2)*height) : Math.round(texty + stepy),
                        fillatt = this.createAttFill(entry);
                  text_g.append('svg:path')
                        .attr('d', `M${bx1},${by1}H${bx2}V${by2}H${bx1}Z`)
                        .call(fillatt.func);
                  break;
               }
            }
         }

         if (num_txt > num_custom)
            promises.push(this.finishTextDrawing(text_g, num_txt > num_custom + 1));

         if (this.isTitle())
            this.draw_g.style('display', !num_txt ? 'none' : null);


         return Promise.all(promises).then(() => this);
      }).then(() => {
         if (!draw_header)
            return;

         const w = Math.round(width*0.5),
               h = Math.round(pad_height*0.04),
               lbl_g = text_g.append('svg:g');

         makeTranslate(lbl_g, Math.round(width*0.25), Math.round(-pad_height*0.02));

         this.drawBorder(lbl_g, w, h);

         lbl_g.append('svg:path')
               .attr('d', `M${0},${0}h${w}v${h}h${-w}z`)
               .call(this.fillatt.func)
               .call(this.lineatt.func);

         return this.startTextDrawingAsync(this.textatt.font, 0.9*h, lbl_g)
                    .then(() => this.drawText({ align: 22, x: 0, y: 0, width: w, height: h, text: pt.fLabel, color: this.textatt.color, draw_g: lbl_g }))
                    .then(() => promises.push(this.finishTextDrawing(lbl_g)));
      }).then(() => { return this; });
   }

   /** @summary Method used to convert value to string according specified format
     * @desc format can be like 5.4g or 4.2e or 6.4f or 'stat' or 'fit' or 'entries' */
   format(value, fmt) {
      if (!fmt) fmt = 'stat';

      const pave = this.getObject();

      switch (fmt) {
         case 'stat' : fmt = pave.fStatFormat || gStyle.fStatFormat; break;
         case 'fit': fmt = pave.fFitFormat || gStyle.fFitFormat; break;
         case 'entries':
            if ((Math.abs(value) < 1e9) && (Math.round(value) === value))
               return value.toFixed(0);
            fmt = '14.7g';
            break;
      }

      return floatToString(value, fmt || '6.4g');
   }

   /** @summary Draw TLegend object */
   drawLegend(w, h) {
      const legend = this.getObject(),
            nlines = legend.fPrimitives.arr.length,
            ncols = Math.max(1, legend.fNColumns);
      let nrows = Math.round(nlines / ncols),
          any_text = false,
          custom_textg = false; // each text entry has own attributes

      if (nrows * ncols < nlines)
         nrows++;

      const isEmpty = entry => !entry.fObject && !entry.fOption && (!entry.fLabel || !entry.fLabel.trim());

      for (let ii = 0; ii < nlines; ++ii) {
         const entry = legend.fPrimitives.arr[ii];
         if (isEmpty(entry)) {
            if (ncols === 1)
               nrows--;
         } else if (entry.fLabel) {
            any_text = true;
            if ((entry.fTextFont && (entry.fTextFont !== legend.fTextFont)) ||
                (entry.fTextSize && (entry.fTextSize !== legend.fTextSize)))
                   custom_textg = true;
         }
      }

      if (nrows < 1)
         nrows = 1;

      const padding_x = Math.round(0.03*w/ncols),
            padding_y = Math.round(0.03*h),
            row_height = (h - 2*padding_y) / (nrows + (nrows - 1) * legend.fEntrySeparation),
            gap_y = row_height * legend.fEntrySeparation;

      let gap_x = padding_x,
          column_width0 = (w - 2*padding_x - (ncols - 1)* gap_x) / ncols;

      if (legend.fColumnSeparation) {
         column_width0 = (w - 2*padding_x) / (ncols + (ncols - 1) * legend.fColumnSeparation);
         gap_x = column_width0 * legend.fColumnSeparation;
      }

      // calculate positions of columns by weight - means more letters, more weight
      const column_pos = new Array(ncols + 1).fill(padding_x),
            column_boxwidth = column_width0 * legend.fMargin;
      if (ncols > 1) {
         const column_weight = new Array(ncols).fill(1),
               space_for_text = w - 2 * padding_x - (ncols - 1) * gap_x - ncols * column_boxwidth;

         for (let ii = 0; ii < nlines; ++ii) {
            const entry = legend.fPrimitives.arr[ii];
            if (isEmpty(entry)) continue; // let discard empty entry
            const icol = ii % ncols;
            column_weight[icol] = Math.max(column_weight[icol], entry.fLabel.length);
         }

         let sum_weight = 0;
         for (let icol = 0; icol < ncols; ++icol)
            sum_weight += column_weight[icol];

         for (let icol = 0; icol < ncols-1; ++icol)
            column_pos[icol+1] = column_pos[icol] + column_boxwidth + column_weight[icol] / sum_weight * space_for_text + gap_x;
      }
      column_pos[ncols] = w - padding_x;

      let font_size = row_height,
          max_font_size = 0, // not limited in the beginning
          any_opt = false;

      this.createAttText({ attr: legend, can_rotate: false });

      const pp = this.getPadPainter(),
            tsz = this.textatt.getSize(pp);
      if (tsz && (tsz < font_size))
         font_size = max_font_size = tsz;

      const text_promises = [],
            pr = any_text && !custom_textg ? this.startTextDrawingAsync(this.textatt.font, font_size, this.draw_g, max_font_size) : Promise.resolve();

      return pr.then(() => {
         for (let ii = 0, i = -1; ii < nlines; ++ii) {
            const entry = legend.fPrimitives.arr[ii];
            if (isEmpty(entry))
               continue; // let discard empty entry

            if (ncols === 1)
               ++i;
            else
               i = ii;

            const lopt = entry.fOption.toLowerCase(),
                  icol = i % ncols, irow = (i - icol) / ncols,
                  x0 = Math.round(column_pos[icol]),
                  y0 = Math.round(padding_y + irow * (row_height + gap_y)),
                  tpos_x = Math.round(x0 + column_boxwidth),
                  mid_x = Math.round(x0 + (column_boxwidth - padding_x)/2),
                  box_y = Math.round(y0 + row_height * 0.1),
                  box_height = Math.round(row_height * 0.8),
                  mid_y = Math.round(y0 + row_height * 0.5), // center line
                  mo = entry.fObject,
                  draw_fill = lopt.indexOf('f') !== -1,
                  draw_line = lopt.indexOf('l') !== -1,
                  draw_error = lopt.indexOf('e') !== -1,
                  draw_marker = lopt.indexOf('p') !== -1;

            let o_fill = entry, o_marker = entry, o_line = entry,
                painter = null, isany = false;

            if (isObject(mo)) {
               if ('fLineColor' in mo) o_line = mo;
               if ('fFillColor' in mo) o_fill = mo;
               if ('fMarkerColor' in mo) o_marker = mo;
               painter = pp.findPainterFor(mo);
            }

            // Draw fill pattern (in a box)
            if (draw_fill) {
               const fillatt = painter?.fillatt?.used ? painter.fillatt : this.createAttFill(o_fill);
               let lineatt;
               if (!draw_line && !draw_error && !draw_marker) {
                  lineatt = painter?.lineatt?.used ? painter.lineatt : this.createAttLine(o_line);
                  if (lineatt.empty()) lineatt = null;
               }

               if (!fillatt.empty() || lineatt) {
                  isany = true;
                  // define x,y as the center of the symbol for this entry
                  this.draw_g.append('svg:path')
                           .attr('d', `M${x0},${box_y}v${box_height}h${tpos_x-padding_x-x0}v${-box_height}z`)
                           .call(fillatt.func)
                           .call(lineatt ? lineatt.func : () => {});
               }
            }

            // Draw line and/or error (when specified)
            if (draw_line || draw_error) {
               const lineatt = painter?.lineatt?.used ? painter.lineatt : this.createAttLine(o_line);
               if (!lineatt.empty()) {
                  isany = true;
                  if (draw_line) {
                     this.draw_g.append('svg:path')
                        .attr('d', `M${x0},${mid_y}h${tpos_x-padding_x-x0}`)
                        .call(lineatt.func);
                  }
                  if (draw_error) {
                     let endcaps = 0, edx = row_height*0.05;
                     if (isFunc(painter?.getHisto) && painter.options?.ErrorKind === 1)
                        endcaps = 1; // draw bars for e1 option in histogram
                     else if (isFunc(painter?.getGraph) && mo?.fLineWidth !== undefined && mo?.fMarkerSize !== undefined) {
                        endcaps = painter.options?.Ends ?? 1; // default is 1
                        edx = mo.fLineWidth + gStyle.fEndErrorSize;
                        if (endcaps > 1) edx = Math.max(edx, mo.fMarkerSize*8*0.66);
                     }

                     const eoff = (endcaps === 3) ? 0.2 : 0,
                           ey1 = Math.round(y0 + row_height*eoff),
                           ey2 = Math.round(y0 + row_height*(1 - eoff)),
                           edy = Math.round(edx * 0.66);
                     edx = Math.round(edx);
                     let path = `M${mid_x},${ey1}V${ey2}`;
                     switch (endcaps) {
                        case 1: path += `M${mid_x-edx},${ey1}h${2*edx}M${mid_x-edx},${ey2}h${2*edx}`; break; // bars
                        case 2: path += `M${mid_x-edx},${ey1+edy}v${-edy}h${2*edx}v${edy}M${mid_x-edx},${ey2-edy}v${edy}h${2*edx}v${-edy}`; break; // ]
                        case 3: path += `M${mid_x-edx},${ey1}h${2*edx}l${-edx},${-edy}zM${mid_x-edx},${ey2}h${2*edx}l${-edx},${edy}z`; break; // triangle
                        case 4: path += `M${mid_x-edx},${ey1+edy}l${edx},${-edy}l${edx},${edy}M${mid_x-edx},${ey2-edy}l${edx},${edy}l${edx},${-edy}`; break; // arrow
                     }
                     this.draw_g.append('svg:path')
                        .attr('d', path)
                        .call(lineatt.func)
                        .style('fill', endcaps > 1 ? 'none' : null);
                  }
               }
            }

            // Draw Poly marker
            if (draw_marker) {
               const marker = painter?.markeratt?.used ? painter.markeratt : this.createAttMarker(o_marker);
               if (!marker.empty()) {
                  isany = true;
                  this.draw_g
                     .append('svg:path')
                     .attr('d', marker.create(mid_x, mid_y))
                     .call(marker.func);
               }
            }

            // special case - nothing draw, try to show rect with line attributes
            if (!isany && painter?.lineatt && !painter.lineatt.empty()) {
               this.draw_g.append('svg:path')
                        .attr('d', `M${x0},${box_y}v${box_height}h${tpos_x-padding_x-x0}v${-box_height}z`)
                        .style('fill', 'none')
                        .call(painter.lineatt.func);
            }

            let pos_x = tpos_x;
            if (isStr(lopt) && (lopt.toLowerCase() !== 'h'))
               any_opt = true;
            else if (!any_opt)
               pos_x = x0;

            if (entry.fLabel) {
               const textatt = this.createAttText({ attr: entry, std: false, attr_alt: legend }),
                     arg = { draw_g: this.draw_g, align: textatt.align,
                             x: pos_x, width: Math.round(column_pos[icol + 1] - pos_x),
                             y: y0, height: Math.round(row_height),
                             scale: (custom_textg && !entry.fTextSize) || !legend.fTextSize,
                             text: entry.fLabel, color: textatt.color };
               if (custom_textg) {
                  arg.draw_g = this.draw_g.append('svg:g');
                  text_promises.push(this.startTextDrawingAsync(textatt.font, textatt.getSize(pp), arg.draw_g, max_font_size)
                                       .then(() => this.drawText(arg))
                                       .then(() => this.finishTextDrawing(arg.draw_g)));
               } else
                  this.drawText(arg);
            }
         }

         if (any_text && !custom_textg)
            text_promises.push(this.finishTextDrawing());

         // rescale after all entries are shown
         return Promise.all(text_promises);
      });
   }

   /** @summary draw color palette with axis */
   drawPaletteAxis(s_width, s_height, arg) {
      const palette = this.getObject(),
            axis = palette.fAxis,
            can_move = isStr(arg) && (arg.indexOf('can_move') >= 0),
            postpone_draw = isStr(arg) && (arg.indexOf('postpone') >= 0),
            cjust = isStr(arg) && (arg.indexOf('cjust') >= 0),
            bring_stats_front = isStr(arg) && (arg.indexOf('bring_stats_front') >= 0),
            pp = this.getPadPainter(),
            width = pp.getPadWidth(),
            height = pp.getPadHeight(),
            pad = pp.getRootPad(true),
            main = palette.$main_painter || this.getMainPainter(),
            framep = this.getFramePainter(),
            contour = main.fContour,
            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._color_palette,
            zaxis = is_scatter ? main.getZaxis() : main.getObject()?.fZaxis,
            sizek = pad?.fTickz ? 0.35 : 0.7;

      let zmin = 0, zmax = 100, gzmin, gzmax, axis_transform, axis_second = 0;

      this._palette_vertical = (palette.fX2NDC - palette.fX1NDC) < (palette.fY2NDC - palette.fY1NDC);

      axis.fTickSize = 0.03; // adjust axis ticks size

      if ((typeof zaxis?.fLabelOffset !== 'undefined') && !is_th3) {
         axis.fBits = zaxis.fBits & ~EAxisBits.kTickMinus & ~EAxisBits.kTickPlus;
         axis.fTitle = zaxis.fTitle;
         axis.fTickSize = zaxis.fTickLength;
         axis.fTitleSize = zaxis.fTitleSize;
         axis.fTitleOffset = zaxis.fTitleOffset;
         axis.fTextColor = zaxis.fTitleColor;
         axis.fTextFont = zaxis.fTitleFont;
         axis.fLineColor = zaxis.fAxisColor;
         axis.fLabelSize = zaxis.fLabelSize;
         axis.fLabelColor = zaxis.fLabelColor;
         axis.fLabelFont = zaxis.fLabelFont;
         axis.fLabelOffset = zaxis.fLabelOffset;
         this.z_handle.setHistPainter(main, is_scatter ? 'hist#z' : 'z');
         this.z_handle.source_axis = zaxis;
      }

      if (contour && framep && !is_th3) {
         if ((framep.zmin !== undefined) && (framep.zmax !== undefined) && (framep.zmin !== framep.zmax)) {
            gzmin = framep.zmin;
            gzmax = framep.zmax;
            zmin = framep.zoom_zmin;
            zmax = framep.zoom_zmax;
            if (zmin === zmax) { zmin = gzmin; zmax = gzmax; }
         } else {
            zmin = levels.at(0);
            zmax = levels.at(-1);
         }
      } else if ((main.gmaxbin !== undefined) && (main.gminbin !== undefined)) {
         // this is case of TH2 (needs only for size adjustment)
         zmin = main.gminbin; zmax = main.gmaxbin;
      } else if ((main.hmin !== undefined) && (main.hmax !== undefined)) {
         // this is case of TH1
         zmin = main.hmin; zmax = main.hmax;
      }

      this.draw_g.selectAll('rect').style('fill', 'white');

      if ((gzmin === undefined) || (gzmax === undefined) || (gzmin === gzmax)) {
         gzmin = zmin; gzmax = zmax;
      }

      if (this._palette_vertical) {
         this._swap_side = palette.fX2NDC < 0.5;
         axis.fChopt = 'S+' + (this._swap_side ? 'R' : 'L'); // clearly configure text align
         this.z_handle.configureAxis('zaxis', gzmin, gzmax, zmin, zmax, true, [0, s_height], { log, fixed_ticks: cjust ? levels : null, maxTickSize: Math.round(s_width*sizek), swap_side: this._swap_side, minposbin: main.gminposbin });
         axis_transform = this._swap_side ? null : `translate(${s_width})`;
         if (pad?.fTickz) axis_second = this._swap_side ? s_width : -s_width;
      } else {
         this._swap_side = palette.fY1NDC > 0.5;
         axis.fChopt = 'S+';
         this.z_handle.configureAxis('zaxis', gzmin, gzmax, zmin, zmax, false, [0, s_width], { log, fixed_ticks: cjust ? levels : null, maxTickSize: Math.round(s_height*sizek), swap_side: this._swap_side, minposbin: main.gminposbin });
         axis_transform = this._swap_side ? null : `translate(0,${s_height})`;
         if (pad?.fTickz) axis_second = this._swap_side ? s_height : -s_height;
      }

      if (!contour || !draw_palette || postpone_draw) {
         // we need such rect to correctly calculate size
         this.draw_g.append('svg:path')
                    .attr('d', `M0,0H${s_width}V${s_height}H0Z`)
                    .style('fill', 'white');
      } else {
         for (let i = 0; i < levels.length - 1; ++i) {
            let z0 = Math.round(this.z_handle.gr(levels[i])),
                z1 = Math.round(this.z_handle.gr(levels[i+1])),
                lvl = (levels[i] + levels[i+1])*0.5, d;

            if (this._palette_vertical) {
               if ((z1 >= s_height) || (z0 < 0)) continue;
               z0 += 1; // ensure correct gap filling between colors

               if (z0 > s_height) {
                  z0 = s_height;
                  lvl = levels[i]*0.001 + levels[i+1]*0.999;
                  if (z1 < 0) z1 = 0;
               } else if (z1 < 0) {
                  z1 = 0;
                  lvl = levels[i]*0.999 + levels[i+1]*0.001;
               }
               d = `M0,${z1}H${s_width}V${z0}H0Z`;
            } else {
               if ((z0 >= s_width) || (z1 < 0)) continue;
               z1 += 1; // ensure correct gap filling between colors

               if (z1 > s_width) {
                  z1 = s_width;
                  lvl = levels[i]*0.999 + levels[i+1]*0.001;
                  if (z0 < 0) z0 = 0;
               } else if (z0 < 0) {
                  z0 = 0;
                  lvl = levels[i]*0.001 + levels[i+1]*0.999;
               }
               d = `M${z0},0V${s_height}H${z1}V0Z`;
            }

            const col = contour.getPaletteColor(draw_palette, lvl);
            if (!col) continue;

            const r = this.draw_g.append('svg:path')
                       .attr('d', d)
                       .style('fill', col)
                       .property('fill0', col)
                       .property('fill1', d3_rgb(col).darker(0.5).formatRgb());

            if (this.isBatchMode())
               continue;

            if (this.isTooltipAllowed()) {
               r.on('mouseover', function() {
                  d3_select(this).transition().duration(100).style('fill', d3_select(this).property('fill1'));
               }).on('mouseout', function() {
                  d3_select(this).transition().duration(100).style('fill', d3_select(this).property('fill0'));
               }).append('svg:title').text(this.z_handle.axisAsText(levels[i]) + ' - ' + this.z_handle.axisAsText(levels[i+1]));
            }

            if (settings.Zooming)
               r.on('dblclick', () => this.getFramePainter().unzoomSingle('z'));
         }
      }

      if (bring_stats_front)
         this.getPadPainter()?.findPainterFor(null, '', clTPaveStats)?.bringToFront();

      return this.z_handle.drawAxis(this.draw_g, s_width, s_height, axis_transform, axis_second).then(() => {
         let rect;
         if (can_move) {
            if (settings.ApproxTextSize || isNodeJs()) {
               // for batch testing provide approx estimation
               rect = { x: this._pave_x, y: this._pave_y, width: s_width, height: s_height };
               const fsz = this.z_handle.labelsFont?.size || 14;
               if (this._palette_vertical) {
                  const dx = (this.z_handle._maxlbllen || 3) * 0.6 * fsz;
                  rect.width += dx;
                  if (this._swap_side) rect.x -= dx;
               } else {
                  rect.height += fsz;
                  if (this._swap_side) rect.y -= fsz;
               }
            } else if ('getBoundingClientRect' in this.draw_g.node())
               rect = this.draw_g.node().getBoundingClientRect();
         }
         if (!rect)
            return this;

         if (this._palette_vertical) {
            const shift = (this._pave_x + parseInt(rect.width)) - Math.round(0.995*width) + 3;

            if (shift > 0) {
               this._pave_x -= shift;
               makeTranslate(this.draw_g, this._pave_x, this._pave_y);
               palette.fX1NDC -= shift/width;
               palette.fX2NDC -= shift/width;
            }
         } else {
            const shift = Math.round((1.05 - gStyle.fTitleY)*height) - rect.y;
            if (shift > 0) {
               this._pave_y += shift;
               makeTranslate(this.draw_g, this._pave_x, this._pave_y);
               palette.fY1NDC -= shift/height;
               palette.fY2NDC -= shift/height;
            }
         }

         return this;
      });
   }

   /** @summary Add interactive methods for palette drawing */
   interactivePaletteAxis(s_width, s_height) {
      let doing_zoom = false, sel1 = 0, sel2 = 0, zoom_rect = null;

      const moveRectSel = evnt => {
         if (!doing_zoom) return;
         evnt.preventDefault();

         const m = d3_pointer(evnt, this.draw_g.node());
         if (this._palette_vertical) {
            sel2 = Math.min(Math.max(m[1], 0), s_height);
            zoom_rect.attr('y', Math.min(sel1, sel2))
                     .attr('height', Math.abs(sel2-sel1));
         } else {
            sel2 = Math.min(Math.max(m[0], 0), s_width);
            zoom_rect.attr('x', Math.min(sel1, sel2))
                     .attr('width', Math.abs(sel2-sel1));
         }
      }, endRectSel = evnt => {
         if (!doing_zoom) return;

         evnt.preventDefault();
         d3_select(window).on('mousemove.colzoomRect', null)
                          .on('mouseup.colzoomRect', null);
         zoom_rect.remove();
         zoom_rect = null;
         doing_zoom = false;

         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);
      }, startRectSel = evnt => {
         // ignore when touch selection is activated
         if (doing_zoom) return;
         doing_zoom = true;

         evnt.preventDefault();
         evnt.stopPropagation();

         const origin = d3_pointer(evnt, this.draw_g.node());

         zoom_rect = this.draw_g.append('svg:rect').attr('id', 'colzoomRect').call(addHighlightStyle, true);

         if (this._palette_vertical) {
            sel1 = sel2 = origin[1];
            zoom_rect.attr('x', '0')
                     .attr('width', s_width)
                     .attr('y', sel1)
                     .attr('height', 1);
         } else {
            sel1 = sel2 = origin[0];
            zoom_rect.attr('x', sel1)
                     .attr('width', 1)
                     .attr('y', 0)
                     .attr('height', s_height);
         }

         d3_select(window).on('mousemove.colzoomRect', moveRectSel)
                          .on('mouseup.colzoomRect', endRectSel, true);
      };

      if (settings.Zooming) {
         this.draw_g.selectAll('.axis_zoom')
                    .on('mousedown', startRectSel)
                    .on('dblclick', () => this.getFramePainter().zoomSingle('z', 0, 0, true));
      }

      if (settings.ZoomWheel) {
         this.draw_g.on('wheel', evnt => {
            const pos = d3_pointer(evnt, this.draw_g.node()),
                  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);
         });
       }
   }

   /** @summary Fill context menu items for the TPave object */
   fillContextMenuItems(menu) {
      const pave = this.getObject(),
            set_opt = this.isStats() ? 'SetOption' : 'SetDrawOption';

      menu.sub('Shadow');
      menu.addSizeMenu('size', 0, 12, 1, pave.fBorderSize, arg => {
         pave.fBorderSize = arg;
         this.interactiveRedraw(true, `exec:SetBorderSize(${arg})`);
      });
      menu.addColorMenu('color', pave.fShadowColor, arg => {
         pave.fShadowColor = arg;
         this.interactiveRedraw(true, getColorExec(arg, 'SetShadowColor'));
      });
      const posarr = ['nb', 'tr', 'tl', 'br', 'bl'];
      let value = '', opt = this.getPaveDrawOption(), remain = opt;
      posarr.forEach(nn => {
         const p = remain.indexOf(nn);
         if ((p >= 0) && !value) {
            value = nn; remain = remain.slice(0, p) + remain.slice(p + nn.length);
         }
      });
      menu.addSelectMenu('positon', posarr, value || 'nb', arg => {
         arg += remain;
         this.setPaveDrawOption(arg);
         this.interactiveRedraw(true, `exec:${set_opt}("${arg}")`);
      }, 'Direction of pave shadow or nb - off');
      menu.endsub();

      menu.sub('Corner');
      const parc = opt.toLowerCase().indexOf('arc');
      menu.addchk(parc >= 0, 'arc', flag => {
         if (flag)
            opt += ' arc';
         else
            opt = opt.slice(0, parc) + opt.slice(parc + 3);
         this.setPaveDrawOption(opt);
         this.interactiveRedraw(true, `exec:${set_opt}("${opt}")`);
      }, 'Usage of ARC draw option');
      menu.addSizeMenu('radius', 0, 0.2, 0.02, pave.fCornerRadius, val => {
         pave.fCornerRadius = val;
         this.interactiveRedraw(true, `exec:SetCornerRadius(${val})`);
      }, 'Corner radius when ARC is enabled');
      menu.endsub();

      if (this.isStats() || this.isPaveText() || this.isPavesText()) {
         menu.add('Label', () => menu.input('Enter new label', pave.fLabel).then(lbl => {
            pave.fLabel = lbl;
            this.interactiveRedraw('pad', `exec:SetLabel("${lbl}")`);
         }));
         menu.addSizeMenu('Margin', 0, 0.2, 0.02, pave.fMargin, val => {
            pave.fMargin = val;
            this.interactiveRedraw(true, `exec:SetMargin(${val})`);
         });
      }

      if (this.isStats()) {
         menu.add('Default position', () => {
            pave.fX2NDC = gStyle.fStatX;
            pave.fX1NDC = pave.fX2NDC - gStyle.fStatW;
            pave.fY2NDC = gStyle.fStatY;
            pave.fY1NDC = pave.fY2NDC - gStyle.fStatH;
            pave.fInit = 1;
            this.interactiveRedraw(true, 'pave_moved');
         });

         menu.add('Save to gStyle', () => {
            gStyle.fStatX = pave.fX2NDC;
            gStyle.fStatW = pave.fX2NDC - pave.fX1NDC;
            gStyle.fStatY = pave.fY2NDC;
            gStyle.fStatH = pave.fY2NDC - pave.fY1NDC;
            this.fillatt?.saveToStyle('fStatColor', 'fStatStyle');
            gStyle.fStatTextColor = pave.fTextColor;
            gStyle.fStatFontSize = pave.fTextSize;
            gStyle.fStatFont = pave.fTextFont;
            gStyle.fFitFormat = pave.fFitFormat;
            gStyle.fStatFormat = pave.fStatFormat;
            gStyle.fOptStat = pave.fOptStat;
            gStyle.fOptFit = pave.fOptFit;
         }, 'Store stats attributes to gStyle');

         menu.separator();

         menu.add('SetStatFormat', () => {
            menu.input('Enter StatFormat', pave.fStatFormat).then(fmt => {
               if (!fmt) return;
               pave.fStatFormat = fmt;
               this.interactiveRedraw(true, `exec:SetStatFormat("${fmt}")`);
            });
         });
         menu.add('SetFitFormat', () => {
            menu.input('Enter FitFormat', pave.fFitFormat).then(fmt => {
               if (!fmt) return;
               pave.fFitFormat = fmt;
               this.interactiveRedraw(true, `exec:SetFitFormat("${fmt}")`);
            });
         });
         menu.sub('SetOptStat', () => {
            menu.input('Enter OptStat', pave.fOptStat, 'int').then(fmt => {
               pave.fOptStat = fmt;
               this.interactiveRedraw(true, `exec:SetOptStat(${fmt})`);
            });
         });
         const addStatOpt = (pos, name) => {
            let sopt = (pos < 10) ? pave.fOptStat : pave.fOptFit;
            sopt = parseInt(parseInt(sopt) / parseInt(Math.pow(10, pos % 10))) % 10;
            menu.addchk(sopt, name, sopt * 100 + pos, arg => {
               const oldopt = parseInt(arg / 100);
               let newopt = (arg % 100 < 10) ? pave.fOptStat : pave.fOptFit;
               newopt -= (oldopt > 0 ? oldopt : -1) * parseInt(Math.pow(10, arg % 10));
               if (arg % 100 < 10) {
                  pave.fOptStat = newopt;
                  this.interactiveRedraw(true, `exec:SetOptStat(${newopt})`);
               } else {
                  pave.fOptFit = newopt;
                  this.interactiveRedraw(true, `exec:SetOptFit(${newopt})`);
               }
            });
         };

         addStatOpt(0, 'Histogram name');
         addStatOpt(1, 'Entries');
         addStatOpt(2, 'Mean');
         addStatOpt(3, 'Std Dev');
         addStatOpt(4, 'Underflow');
         addStatOpt(5, 'Overflow');
         addStatOpt(6, 'Integral');
         addStatOpt(7, 'Skewness');
         addStatOpt(8, 'Kurtosis');
         menu.endsub();

         menu.sub('SetOptFit', () => {
            menu.input('Enter OptStat', pave.fOptFit, 'int').then(fmt => {
               pave.fOptFit = fmt;
               this.interactiveRedraw(true, `exec:SetOptFit(${fmt})`);
            });
         });
         addStatOpt(10, 'Fit parameters');
         addStatOpt(11, 'Par errors');
         addStatOpt(12, 'Chi square / NDF');
         addStatOpt(13, 'Probability');
         menu.endsub();

         menu.separator();
      } else if (this.isPaveText() || this.isPavesText()) {
         if (this.isPavesText()) {
            menu.addSizeMenu('Paves', 1, 10, 1, pave.fNpaves, val => {
               pave.fNpaves = val;
               this.interactiveRedraw(true, `exec:SetNpaves(${val})`);
            });
         }

         if (this.isTitle()) {
            menu.add('Default position', () => {
               pave.fX1NDC = gStyle.fTitleW > 0 ? gStyle.fTitleX - gStyle.fTitleW/2 : gStyle.fPadLeftMargin;
               pave.fY1NDC = gStyle.fTitleY - Math.min(gStyle.fTitleFontSize*1.1, 0.06);
               pave.fX2NDC = gStyle.fTitleW > 0 ? gStyle.fTitleX + gStyle.fTitleW/2 : 1 - gStyle.fPadRightMargin;
               pave.fY2NDC = gStyle.fTitleY;
               pave.fInit = 1;
               this.interactiveRedraw(true, 'pave_moved');
            });

            menu.add('Save to gStyle', () => {
               gStyle.fTitleX = (pave.fX2NDC + pave.fX1NDC)/2;
               gStyle.fTitleY = pave.fY2NDC;
               this.fillatt?.saveToStyle('fTitleColor', 'fTitleStyle');
               gStyle.fTitleTextColor = pave.fTextColor;
               gStyle.fTitleFontSize = pave.fTextSize;
               gStyle.fTitleFont = pave.fTextFont;
            }, 'Store title position and graphical attributes to gStyle');
         }
      } else if (pave._typename === clTLegend) {
         menu.sub('Legend');
         menu.add('Autoplace', () => {
            this.autoPlaceLegend(pave, this.getPadPainter()?.getRootPad(true), true).then(res => {
               if (res) this.interactiveRedraw(true, 'pave_moved');
            });
         });
         menu.addSizeMenu('Entry separation', 0, 1, 0.1, pave.fEntrySeparation, v => {
            pave.fEntrySeparation = v;
            this.interactiveRedraw(true, `exec:SetEntrySeparation(${v})`);
         }, 'Vertical entries separation, meaningful values between 0 and 1');
         menu.addSizeMenu('Columns separation', 0, 1, 0.1, pave.fColumnSeparation, v => {
            pave.fColumnSeparation = v;
            this.interactiveRedraw(true, `exec:SetColumnSeparation(${v})`);
         }, 'Horizontal columns separation, meaningful values between 0 and 1');
         menu.addSizeMenu('Num columns', 1, 7, 1, pave.fNColumns, v => {
            pave.fNColumns = v;
            this.interactiveRedraw(true, `exec:SetNColumns(${v})`);
         }, 'Number of columns in the legend');
         menu.endsub();
      }
   }

   /** @summary Show pave context menu */
   paveContextMenu(evnt) {
      if (this.z_handle) {
         const fp = this.getFramePainter();
         if (isFunc(fp?.showContextMenu))
             fp.showContextMenu('pal', evnt);
      } else
         showPainterMenu(evnt, this, this.isTitle() ? kTitle : undefined);
   }

   /** @summary Returns true when stat box is drawn */
   isStats() {
      return this.matchObjectType(clTPaveStats);
   }

   /** @summary Returns true when stat box is drawn */
   isPaveText() {
      return this.matchObjectType(clTPaveText);
   }

   /** @summary Returns true when stat box is drawn */
   isPavesText() {
      return this.matchObjectType(clTPavesText);
   }

   /** @summary Returns true when stat box is drawn */
   isPalette() {
      return this.matchObjectType(clTPaletteAxis);
   }

   /** @summary Returns true when title is drawn */
   isTitle() {
      return this.isPaveText() && (this.getObject()?.fName === kTitle);
   }

   /** @summary Clear text in the pave */
   clearPave() {
      this.getObject().Clear();
   }

   /** @summary Add text to pave */
   addText(txt) {
      this.getObject().AddText(txt);
   }

   /** @summary Remade version of THistPainter::GetBestFormat
     * @private */
   getBestFormat(tv, e) {
      const ie = tv.indexOf('e'), id = tv.indexOf('.');

      if (ie >= 0)
         return (tv.indexOf('+') < 0) || (e >= 1) ? `.${ie-id-1}e` : '.1f';
      if (id < 0)
         return '.1f';

      return `.${tv.length - id - 1}f`;
   }

   /** @summary Fill function parameters */
   fillFunctionStat(f1, dofit, ndim = 1) {
      this._has_fit = false;

      if (!dofit || !f1) return false;

      this._has_fit = true;
      this._fit_dim = ndim;
      this._fit_cnt = 0;

      const print_fval = (ndim === 1) ? dofit % 10 : 1,
            print_ferrors = (ndim === 1) ? Math.floor(dofit/10) % 10 : 1,
            print_fchi2 = (ndim === 1) ? Math.floor(dofit/100) % 10 : 1,
            print_fprob = (ndim === 1) ? Math.floor(dofit/1000) % 10 : 0;

      if (print_fchi2) {
         this.addText('#chi^{2} / ndf = ' + this.format(f1.fChisquare, 'fit') + ' / ' + f1.fNDF);
         this._fit_cnt++;
      }
      if (print_fprob) {
         this.addText('Prob = ' + this.format(Prob(f1.fChisquare, f1.fNDF)));
         this._fit_cnt++;
      }
      if (print_fval) {
         for (let n = 0; n < f1.GetNumPars(); ++n) {
            const parname = f1.GetParName(n);
            let parvalue = f1.GetParValue(n), parerr = f1.GetParError(n);
            if (parvalue === undefined) {
               parvalue = '<not avail>';
               parerr = null;
            } else {
               parvalue = this.format(Number(parvalue), 'fit');
               if (print_ferrors && (parerr !== undefined)) {
                  parerr = floatToString(parerr, this.getBestFormat(parvalue, parerr));
                  if ((Number(parerr) === 0) && (f1.GetParError(n) !== 0))
                     parerr = floatToString(f1.GetParError(n), '4.2g');
               }
            }

            if (print_ferrors && parerr)
               this.addText(`${parname} = ${parvalue} #pm ${parerr}`);
            else
               this.addText(`${parname} = ${parvalue}`);
            this._fit_cnt++;
         }
      }


      return true;
   }

   /** @summary Is dummy pos of the pave painter */
   isDummyPos(p) {
      if (!p) return true;

      return !p.fInit && !p.fX1 && !p.fX2 && !p.fY1 && !p.fY2 && !p.fX1NDC && !p.fX2NDC && !p.fY1NDC && !p.fY2NDC;
   }

   /** @summary Update TPave object  */
   updateObject(obj, opt) {
      if (!this.matchObjectType(obj))
         return false;

      const pave = this.getObject(),
            is_auto = opt === kAutoPlace;

      if (!pave.$modifiedNDC && !this.isDummyPos(obj)) {
         // if position was not modified interactively, update from source object

         if (this.stored && !obj.fInit && (this.stored.fX1 === obj.fX1) &&
             (this.stored.fX2 === obj.fX2) && (this.stored.fY1 === obj.fY1) && (this.stored.fY2 === obj.fY2)) {
            // case when source object not initialized and original coordinates are not changed
            // take over only modified NDC coordinate, used in tutorials/graphics/canvas.C
            if (this.stored.fX1NDC !== obj.fX1NDC) pave.fX1NDC = obj.fX1NDC;
            if (this.stored.fX2NDC !== obj.fX2NDC) pave.fX2NDC = obj.fX2NDC;
            if (this.stored.fY1NDC !== obj.fY1NDC) pave.fY1NDC = obj.fY1NDC;
            if (this.stored.fY2NDC !== obj.fY2NDC) pave.fY2NDC = obj.fY2NDC;
         } else {
            pave.fInit = obj.fInit;
            pave.fX1 = obj.fX1; pave.fX2 = obj.fX2;
            pave.fY1 = obj.fY1; pave.fY2 = obj.fY2;
            pave.fX1NDC = obj.fX1NDC; pave.fX2NDC = obj.fX2NDC;
            pave.fY1NDC = obj.fY1NDC; pave.fY2NDC = obj.fY2NDC;
         }

         this.stored = Object.assign({}, obj); // store latest coordinates
      }

      pave.fOption = obj.fOption;
      pave.fBorderSize = obj.fBorderSize;
      if (pave.fTextColor !== undefined && obj.fTextColor !== undefined) {
         pave.fTextAngle = obj.fTextAngle;
         pave.fTextSize = obj.fTextSize;
         pave.fTextAlign = obj.fTextAlign;
         pave.fTextColor = obj.fTextColor;
         pave.fTextFont = obj.fTextFont;
      }

      switch (obj._typename) {
         case clTDiamond:
         case clTPaveText:
            pave.fLines = clone(obj.fLines);
            break;
         case clTPavesText:
            pave.fLines = clone(obj.fLines);
            pave.fNpaves = obj.fNpaves;
            break;
         case clTPaveLabel:
         case clTPaveClass:
            pave.fLabel = obj.fLabel;
            break;
         case clTPaveStats:
            pave.fOptStat = obj.fOptStat;
            pave.fOptFit = obj.fOptFit;
            break;
         case clTLegend: {
            const oldprim = pave.fPrimitives;
            pave.fPrimitives = obj.fPrimitives;
            pave.fNColumns = obj.fNColumns;
            this.AutoPlace = is_auto;
            if (oldprim?.arr?.length && (oldprim?.arr?.length === pave.fPrimitives?.arr?.length)) {
               // try to sync object reference, new object does not displayed automatically
               // in ideal case one should use snapids in the entries
               for (let k = 0; k < oldprim.arr.length; ++k) {
                  const oldobj = oldprim.arr[k].fObject, newobj = pave.fPrimitives.arr[k].fObject;
                  if (oldobj && newobj && oldobj._typename === newobj._typename && oldobj.fName === newobj.fName)
                     pave.fPrimitives.arr[k].fObject = oldobj;
               }
            }
            return true;
         }
         case clTPaletteAxis:
            pave.fBorderSize = 1;
            pave.fShadowColor = 0;
            break;
         default:
            return false;
      }

      this.storeDrawOpt(is_auto ? kDefaultDrawOpt : opt);

      return true;
   }

   /** @summary redraw pave object */
   async redraw() {
      return this.drawPave();
   }

   /** @summary cleanup pave painter */
   cleanup() {
      this.z_handle?.cleanup();
      delete this.z_handle;
      const pp = this.getObject();
      if (pp)
         delete pp.$main_painter;
      super.cleanup();
   }

   /** @summary Set position of title
     * @private */
   setTitlePosition(pave, text_width, text_height) {
      const posx = gStyle.fTitleX, posy = gStyle.fTitleY,
            valign = gStyle.fTitleAlign % 10, halign = (gStyle.fTitleAlign - valign) / 10;
      let w = gStyle.fTitleW, h = gStyle.fTitleH, need_readjust = false;

      if (h <= 0) {
         if (text_height)
            h = 1.1 * text_height / this.getPadPainter().getPadHeight();
         else {
            h = 0.05;
            need_readjust = true;
         }
      }

      if (w <= 0) {
         if (text_width)
            w = Math.min(0.7, 0.02 + text_width / this.getPadPainter().getPadWidth());
         else {
            w = 0.5;
            need_readjust = true;
         }
      }

      pave.fX1NDC = halign < 2 ? posx : (halign > 2 ? posx - w : posx - w/2);
      pave.fY1NDC = valign < 2 ? posy : (valign > 2 ? posy - h : posy - h/2);
      pave.fX2NDC = pave.fX1NDC + w;
      pave.fY2NDC = pave.fY1NDC + h;
      pave.fInit = 1;

      return need_readjust;
   }

   /** @summary Returns true if object is supported */
   static canDraw(obj) {
      const typ = obj?._typename;
      return typ === clTPave || typ === clTPaveLabel || typ === clTPaveClass || typ === clTPaveStats || typ === clTPaveText ||
             typ === clTPavesText || typ === clTDiamond || typ === clTLegend || typ === clTPaletteAxis;
   }

   /** @summary Draw TPave */
   static async draw(dom, pave, opt) {
      const arg_opt = opt,
            pos_title = (opt === kPosTitle),
            is_auto = (opt === kAutoPlace);
      if (pos_title || is_auto || (isStr(opt) && (opt.indexOf(';') >= 0)))
         opt = ''; // use default - or stored in TPave itself

      const painter = new TPavePainter(dom, pave, opt);

      return ensureTCanvas(painter, false).then(() => {
         if (painter.isTitle()) {
            const prev_painter = painter.getPadPainter().findPainterFor(null, kTitle, clTPaveText);
            if (prev_painter && (prev_painter !== painter)) {
               prev_painter.removeFromPadPrimitives();
               prev_painter.cleanup();
            } else if (pos_title || painter.isDummyPos(pave)) {
               if (painter.setTitlePosition(pave))
                  painter.$postitle = true;
            }
         } else if (pave._typename === clTPaletteAxis) {
            pave.fBorderSize = 1;
            pave.fShadowColor = 0;

            // check some default values of TGaxis object, otherwise axis will not be drawn
            if (pave.fAxis) {
               if (!pave.fAxis.fChopt) pave.fAxis.fChopt = '+';
               if (!pave.fAxis.fNdiv) pave.fAxis.fNdiv = 12;
               if (!pave.fAxis.fLabelOffset) pave.fAxis.fLabelOffset = 0.005;
            }

            painter.z_handle = new TAxisPainter(painter.getPadPainter(), pave.fAxis, true);

            painter.UseContextMenu = true;
         }

         painter.NoFillStats = pave.fName !== 'stats';

         switch (pave._typename) {
            case clTPaveLabel:
            case clTPaveClass:
               painter.paveDrawFunc = painter.drawPaveLabel;
               break;
            case clTPaveStats:
               painter.paveDrawFunc = painter.drawPaveStats;
               break;
            case clTPaveText:
            case clTPavesText:
            case clTDiamond:
               painter.paveDrawFunc = painter.drawPaveText;
               break;
            case clTLegend:
               painter.AutoPlace = is_auto;
               painter.paveDrawFunc = painter.drawLegend;
               break;
            case clTPaletteAxis:
               painter.paveDrawFunc = painter.drawPaletteAxis;
               break;
         }

         return painter.drawPave(arg_opt).then(() => {
            const adjust_title = painter.$postitle && painter.$titlebox;

            if (adjust_title)
               painter.setTitlePosition(pave, painter.$titlebox.width, painter.$titlebox.height);

            delete painter.$postitle;
            delete painter.$titlebox;

            return adjust_title ? painter.drawPave(arg_opt) : painter;
         });
      });
   }

} // class TPavePainter


export { TPavePainter, kPosTitle };