Source: JSRoot.painter.js

/// @file JSRoot.painter.js
/// Baisc JavaScript ROOT painter classes

JSROOT.define(['d3'], (d3) => {

   "use strict";

   JSROOT.loadScript('$$$style/JSRoot.painter');

   if ((typeof d3 !== 'object') || !d3.version)
      console.error('Fail to detect d3.js');
   else if (d3.version[0] !== "6")
      console.error(`Unsupported d3.js version ${d3.version}, expected 6.1.1`);
   else if (d3.version !== '6.1.1')
      console.log(`Reuse existing d3.js version ${d3.version}, expected 6.1.1`);

   // ==========================================================================================

   /** @summary Draw options interpreter
     * @memberof JSROOT
     * @private */
   function DrawOptions(opt) {
      this.opt = opt && (typeof opt == "string") ? opt.toUpperCase().trim() : "";
      this.part = "";
   }

   /** @summary Returns true if remaining options are empty. */
   DrawOptions.prototype.empty = function() { return this.opt.length === 0; }

   /** @summary Returns remaining part of the draw options. */
   DrawOptions.prototype.remain = function() { return this.opt; }

   /** @summary Checks if given option exists */
   DrawOptions.prototype.check = function(name, postpart) {
      let pos = this.opt.indexOf(name);
      if (pos < 0) return false;
      this.opt = this.opt.substr(0, pos) + this.opt.substr(pos + name.length);
      this.part = "";
      if (!postpart) return true;

      let pos2 = pos;
      while ((pos2 < this.opt.length) && (this.opt[pos2] !== ' ') && (this.opt[pos2] !== ',') && (this.opt[pos2] !== ';')) pos2++;
      if (pos2 > pos) {
         this.part = this.opt.substr(pos, pos2 - pos);
         this.opt = this.opt.substr(0, pos) + this.opt.substr(pos2);
      }
      return true;
   }

   /** @summary Returns remaining part of found option as integer. */
   DrawOptions.prototype.partAsInt = function(offset, dflt) {
      let val = this.part.replace(/^\D+/g, '');
      val = val ? parseInt(val, 10) : Number.NaN;
      return isNaN(val) ? (dflt || 0) : val + (offset || 0);
   }

   /** @summary Returns remaining part of found option as float. */
   DrawOptions.prototype.partAsFloat = function(offset, dflt) {
      let val = this.part.replace(/^\D+/g, '');
      val = val ? parseFloat(val) : Number.NaN;
      return isNaN(val) ? (dflt || 0) : val + (offset || 0);
   }

   // ============================================================================================

   /** @namespace
     * @summary Collection of Painter-related methods and classes
     * @alias JSROOT.Painter */
   let jsrp = {
      Coord: {
         kCARTESIAN: 1,
         kPOLAR: 2,
         kCYLINDRICAL: 3,
         kSPHERICAL: 4,
         kRAPIDITY: 5
      },
      root_colors: [],
      root_line_styles: ["", "", "3,3", "1,2",
         "3,4,1,4", "5,3,1,3", "5,3,1,3,1,3,1,3", "5,5",
         "5,3,1,3,1,3", "20,5", "20,10,1,10", "1,3"],
      root_markers: [0, 100, 8, 7, 0,  //  0..4
         9, 100, 100, 100, 100,  //  5..9
         100, 100, 100, 100, 100,  // 10..14
         100, 100, 100, 100, 100,  // 15..19
         100, 103, 105, 104, 0,  // 20..24
         3, 4, 2, 1, 106,  // 25..29
         6, 7, 5, 102, 101], // 30..34
      root_fonts: ['Arial', 'iTimes New Roman',
         'bTimes New Roman', 'biTimes New Roman', 'Arial',
         'oArial', 'bArial', 'boArial', 'Courier New',
         'oCourier New', 'bCourier New', 'boCourier New',
         'Symbol', 'Times New Roman', 'Wingdings', 'iSymbol', 'Verdana'],
      // taken from https://www.math.utah.edu/~beebe/fonts/afm-widths.html
      root_fonts_aver_width: [0.537, 0.510,
         0.535, 0.520, 0.537,
         0.54, 0.556, 0.56, 0.6,
         0.6, 0.6, 0.6,
         0.587, 0.514, 0.896, 0.587, 0.55]
   };

   jsrp.createMenu = function(evnt, handler, menuname) {
      document.body.style.cursor = 'wait';
      let show_evnt;
      // copy event values, otherwise they will gone after scripts loading
      if (evnt && (typeof evnt == "object"))
         if ((evnt.clientX !== undefined) && (evnt.clientY !== undefined))
            show_evnt = { clientX: evnt.clientX, clientY: evnt.clientY };
      return JSROOT.require(['menu']).then(() => {
         document.body.style.cursor = 'auto';
         return jsrp.createMenu(show_evnt, handler, menuname);
      });
   }

   jsrp.closeMenu = function(menuname) {
      JSROOT.require(['menu']).then(() => {
         jsrp.closeMenu(menuname);
      });
   }

   /** @summary Read style and settings from URL
     * @private */
   jsrp.readStyleFromURL = function(url) {
      let d = JSROOT.decodeUrl(url), g = JSROOT.gStyle, s = JSROOT.settings;

      if (d.has("optimize")) {
         s.OptimizeDraw = 2;
         let optimize = d.get("optimize");
         if (optimize) {
            optimize = parseInt(optimize);
            if (!isNaN(optimize)) s.OptimizeDraw = optimize;
         }
      }

      let inter = d.get("interactive");
      if (inter === "nomenu")
         s.ContextMenu = false;
      else if (inter !== undefined) {
         if (!inter || (inter == "1")) inter = "111111"; else
            if (inter == "0") inter = "000000";
         if (inter.length === 6) {
            switch(inter[0]) {
               case "0": g.ToolBar = false; break;
               case "1": g.ToolBar = 'popup'; break;
               case "2": g.ToolBar = true; break;
            }
            inter = inter.substr(1);
         }
         if (inter.length == 5) {
            s.Tooltip = parseInt(inter[0]);
            s.ContextMenu = (inter[1] != '0');
            s.Zooming = (inter[2] != '0');
            s.MoveResize = (inter[3] != '0');
            s.DragAndDrop = (inter[4] != '0');
         }
      }

      let tt = d.get("tooltip");
      if ((tt == "off") || (tt == "false") || (tt == "0"))
         s.Tooltip = false;
      else if (d.has("tooltip"))
         s.Tooltip = true;

      let mathjax = d.get("mathjax", null), latex = d.get("latex", null);

      if ((mathjax !== null) && (mathjax != "0") && (latex === null)) latex = "math";
      if (latex !== null)
         s.Latex = JSROOT.constants.Latex.fromString(latex);

      if (d.has("nomenu")) s.ContextMenu = false;
      if (d.has("noprogress")) s.ProgressBox = false;
      if (d.has("notouch")) JSROOT.browser.touches = false;
      if (d.has("adjframe")) s.CanAdjustFrame = true;

      let optstat = d.get("optstat");
      if (optstat) g.fOptStat = parseInt(optstat);
      let optfit = d.get("optfit");
      if (optfit) g.fOptFit = parseInt(optfit);
      g.fStatFormat = d.get("statfmt", g.fStatFormat);
      g.fFitFormat = d.get("fitfmt", g.fFitFormat);

      if (d.has("toolbar")) {
         let toolbar = d.get("toolbar", ""), val = null;
         if (toolbar.indexOf('popup') >= 0) val = 'popup';
         if (toolbar.indexOf('left') >= 0) { s.ToolBarSide = 'left'; val = 'popup'; }
         if (toolbar.indexOf('right') >= 0) { s.ToolBarSide = 'right'; val = 'popup'; }
         if (toolbar.indexOf('vert') >= 0) { s.ToolBarVert = true; val = 'popup'; }
         if (toolbar.indexOf('show') >= 0) val = true;
         s.ToolBar = val || ((toolbar.indexOf("0") < 0) && (toolbar.indexOf("false") < 0) && (toolbar.indexOf("off") < 0));
      }

      if (d.has("palette")) {
         let palette = parseInt(d.get("palette"));
         if (!isNaN(palette) && (palette > 0) && (palette < 113)) s.Palette = palette;
      }

      let render3d = d.get("render3d");
      if (render3d)
         JSROOT.settings.Render3D = JSROOT.constants.Render3D.fromString(render3d);

      let embed3d = d.get("embed3d");
      if (embed3d)
         JSROOT.settings.Embed3D = JSROOT.constants.Embed3D.fromString(embed3d);

      let geosegm = d.get("geosegm");
      if (geosegm) s.GeoGradPerSegm = Math.max(2, parseInt(geosegm));
      let geocomp = d.get("geocomp");
      if (geocomp) s.GeoCompressComp = (geocomp !== '0') && (geocomp !== 'false') && (geocomp !== 'off');

      if (d.has("hlimit")) s.HierarchyLimit = parseInt(d.get("hlimit"));
   }

   /** @summary Generates all root colors, used also in jstests to reset colors
     * @private */
   jsrp.createRootColors = function() {
      let colorMap = ['white', 'black', 'red', 'green', 'blue', 'yellow', 'magenta', 'cyan', 'rgb(89,212,84)', 'rgb(89,84,217)', 'white'];
      colorMap[110] = 'white';

      let moreCol = [
         { n: 11, s: 'c1b7ad4d4d4d6666668080809a9a9ab3b3b3cdcdcde6e6e6f3f3f3cdc8accdc8acc3c0a9bbb6a4b3a697b8a49cae9a8d9c8f83886657b1cfc885c3a48aa9a1839f8daebdc87b8f9a768a926983976e7b857d9ad280809caca6c0d4cf88dfbb88bd9f83c89a7dc08378cf5f61ac8f94a6787b946971d45a549300ff7b00ff6300ff4b00ff3300ff1b00ff0300ff0014ff002cff0044ff005cff0074ff008cff00a4ff00bcff00d4ff00ecff00fffd00ffe500ffcd00ffb500ff9d00ff8500ff6d00ff5500ff3d00ff2600ff0e0aff0022ff003aff0052ff006aff0082ff009aff00b1ff00c9ff00e1ff00f9ff00ffef00ffd700ffbf00ffa700ff8f00ff7700ff6000ff4800ff3000ff1800ff0000' },
         { n: 201, s: '5c5c5c7b7b7bb8b8b8d7d7d78a0f0fb81414ec4848f176760f8a0f14b81448ec4876f1760f0f8a1414b84848ec7676f18a8a0fb8b814ecec48f1f1768a0f8ab814b8ec48ecf176f10f8a8a14b8b848ecec76f1f1' },
         { n: 390, s: 'ffffcdffff9acdcd9affff66cdcd669a9a66ffff33cdcd339a9a33666633ffff00cdcd009a9a00666600333300' },
         { n: 406, s: 'cdffcd9aff9a9acd9a66ff6666cd66669a6633ff3333cd33339a3333663300ff0000cd00009a00006600003300' },
         { n: 422, s: 'cdffff9affff9acdcd66ffff66cdcd669a9a33ffff33cdcd339a9a33666600ffff00cdcd009a9a006666003333' },
         { n: 590, s: 'cdcdff9a9aff9a9acd6666ff6666cd66669a3333ff3333cd33339a3333660000ff0000cd00009a000066000033' },
         { n: 606, s: 'ffcdffff9affcd9acdff66ffcd66cd9a669aff33ffcd33cd9a339a663366ff00ffcd00cd9a009a660066330033' },
         { n: 622, s: 'ffcdcdff9a9acd9a9aff6666cd66669a6666ff3333cd33339a3333663333ff0000cd00009a0000660000330000' },
         { n: 791, s: 'ffcd9acd9a669a66339a6600cd9a33ffcd66ff9a00ffcd33cd9a00ffcd00ff9a33cd66006633009a3300cd6633ff9a66ff6600ff6633cd3300ff33009aff3366cd00336600339a0066cd339aff6666ff0066ff3333cd0033ff00cdff9a9acd66669a33669a009acd33cdff669aff00cdff339acd00cdff009affcd66cd9a339a66009a6633cd9a66ffcd00ff6633ffcd00cd9a00ffcd33ff9a00cd66006633009a3333cd6666ff9a00ff9a33ff6600cd3300ff339acdff669acd33669a00339a3366cd669aff0066ff3366ff0033cd0033ff339aff0066cd00336600669a339acd66cdff009aff33cdff009acd00cdffcd9aff9a66cd66339a66009a9a33cdcd66ff9a00ffcd33ff9a00cdcd00ff9a33ff6600cd33006633009a6633cd9a66ff6600ff6633ff3300cd3300ffff339acd00666600339a0033cd3366ff669aff0066ff3366cd0033ff0033ff9acdcd669a9a33669a0066cd339aff66cdff009acd009aff33cdff009a' },
         { n: 920, s: 'cdcdcd9a9a9a666666333333' }];

      moreCol.forEach(entry => {
         let s = entry.s;
         for (let n = 0; n < s.length; n += 6) {
            let num = entry.n + n / 6;
            colorMap[num] = 'rgb(' + parseInt("0x" + s.substr(n, 2)) + "," + parseInt("0x" + s.substr(n + 2, 2)) + "," + parseInt("0x" + s.substr(n + 4, 2)) + ")";
         }
      });

      jsrp.root_colors = colorMap;
   }

   /** @summary Produces rgb code for TColor object
     * @private */
   jsrp.getRGBfromTColor = function(col) {
      if (!col || (col._typename != 'TColor')) return null;
      let rgb = Math.round(col.fRed * 255) + "," + Math.round(col.fGreen * 255) + "," + Math.round(col.fBlue * 255);
      if ((col.fAlpha === undefined) || (col.fAlpha == 1.))
         rgb = "rgb(" + rgb + ")";
      else
         rgb = "rgba(" + rgb + "," + col.fAlpha.toFixed(3) + ")";

      switch (rgb) {
         case 'rgb(255,255,255)': return 'white';
         case 'rgb(0,0,0)': return 'black';
         case 'rgb(255,0,0)': return 'red';
         case 'rgb(0,255,0)': return 'green';
         case 'rgb(0,0,255)': return 'blue';
         case 'rgb(255,255,0)': return 'yellow';
         case 'rgb(255,0,255)': return 'magenta';
         case 'rgb(0,255,255)': return 'cyan';
      }
      return rgb;
   }

   /** @summary Add new colors from object array
     * @private */
   jsrp.extendRootColors = function(jsarr, objarr) {
      if (!jsarr) {
         jsarr = [];
         for (let n = 0; n < jsrp.root_colors.length; ++n)
            jsarr[n] = jsrp.root_colors[n];
      }

      if (!objarr) return jsarr;

      let rgb_array = objarr;
      if (objarr._typename && objarr.arr) {
         rgb_array = [];
         for (let n = 0; n < objarr.arr.length; ++n) {
            let col = objarr.arr[n];
            if (!col || (col._typename != 'TColor')) continue;

            if ((col.fNumber >= 0) && (col.fNumber <= 10000))
               rgb_array[col.fNumber] = jsrp.getRGBfromTColor(col);
         }
      }

      for (let n = 0; n < rgb_array.length; ++n)
         if (rgb_array[n] && (jsarr[n] != rgb_array[n]))
            jsarr[n] = rgb_array[n];

      return jsarr;
   }

   /** @ummary Set global list of colors.
     * @desc Either TObjArray of TColor instances or just plain array with rgb() code.
     * List of colors typically stored together with TCanvas primitives
     * @private */
   jsrp.adoptRootColors = function(objarr) {
      jsrp.extendRootColors(jsrp.root_colors, objarr);
   }

   /** @summary Return ROOT color by index
     * @desc Color numbering corresponds typical ROOT colors
     * @returns {String} with RGB color code or existing color name like 'cyan' */
   jsrp.getColor = function(indx) {
      return jsrp.root_colors[indx];
   }

   /** @summary Add new color
     * @param {string} rgb - color name or just string with rgb value
     * @returns {number} index of new color */
   jsrp.addColor = function(rgb) {
      let indx = jsrp.root_colors.indexOf(rgb);
      if (indx >= 0) return indx;
      jsrp.root_colors.push(rgb);
      return jsrp.root_colors.length-1;
   }

   // =====================================================================

   /**
    * @summary Color palette handle
    *
    * @class
    * @memberof JSROOT
    * @private
    */

   function ColorPalette(arr) {
      this.palette = arr;
   }

   /** @summary Returns color index which correspond to contour index of provided length */
   ColorPalette.prototype.calcColorIndex = function(i, len) {
      let plen = this.palette.length, theColor = Math.floor((i + 0.99) * plen / (len - 1));
      return (theColor > plen - 1) ? plen - 1 : theColor;
    }

   /** @summary Returns color with provided index */
   ColorPalette.prototype.getColor = function(indx) { return this.palette[indx]; }

   /** @summary Returns number of colors in the palette */
   ColorPalette.prototype.getLength = function() { return this.palette.length; }

   /** @summary Calculate color for given i and len */
   ColorPalette.prototype.calcColor = function(i, len) { return this.getColor(this.calcColorIndex(i, len)); }

   // =============================================================================

   /**
     * @summary Handle for marker attributes
     *
     * @class
     * @memberof JSROOT
     * @param {object} args - different attributes, see {@link JSROOT.TAttMarkerHandler.setArgs} for details
     * @private
     */

   function TAttMarkerHandler(args) {
      this.x0 = this.y0 = 0;
      this.color = 'black';
      this.style = 1;
      this.size = 8;
      this.scale = 1;
      this.stroke = true;
      this.fill = true;
      this.marker = "";
      this.ndig = 0;
      this.used = true;
      this.changed = false;

      this.func = this.apply.bind(this);

      this.setArgs(args);

      this.changed = false;
   }

   /** @summary Set marker attributes.
     * @param {object} args - arguments can be
     * @param {object} args.attr - instance of TAttrMarker (or derived class) or
     * @param {string} args.color - color in HTML form like grb(1,4,5) or 'green'
     * @param {number} args.style - marker style
     * @param {number} args.size - marker size */
   TAttMarkerHandler.prototype.setArgs = function(args) {
      if ((typeof args == 'object') && (typeof args.fMarkerStyle == 'number')) args = { attr: args };

      if (args.attr) {
         if (args.color === undefined)
            args.color = args.painter ? args.painter.getColor(args.attr.fMarkerColor) : jsrp.getColor(args.attr.fMarkerColor);
         if (!args.style || (args.style < 0)) args.style = args.attr.fMarkerStyle;
         if (!args.size) args.size = args.attr.fMarkerSize;
      }

      this.change(args.color, args.style, args.size);
   }

   /** @summary Reset position, used for optimization of drawing of multiple markers
    * @private */
   TAttMarkerHandler.prototype.resetPos = function() { this.lastx = this.lasty = null; }

   /** @summary Create marker path for given position.
     * @desc When drawing many elementary points, created path may depend from previously produced markers.
     * @param {number} x - first coordinate
     * @param {number} y - second coordinate
     * @returns {string} path string */
   TAttMarkerHandler.prototype.create = function(x, y) {
      if (!this.optimized)
         return "M" + (x + this.x0).toFixed(this.ndig) + "," + (y + this.y0).toFixed(this.ndig) + this.marker;

      // use optimized handling with relative position
      let xx = Math.round(x), yy = Math.round(y), m1 = "M" + xx + "," + yy + "h1",
         m2 = (this.lastx === null) ? m1 : ("m" + (xx - this.lastx) + "," + (yy - this.lasty) + "h1");
      this.lastx = xx + 1; this.lasty = yy;
      return (m2.length < m1.length) ? m2 : m1;
   }

   /** @summary Returns full size of marker */
   TAttMarkerHandler.prototype.getFullSize = function() { return this.scale * this.size; }

   /** @summary Returns approximate length of produced marker string */
   TAttMarkerHandler.prototype.getMarkerLength = function() { return this.marker ? this.marker.length : 10; }

   /** @summary Change marker attributes.
    *  @param {string} color - marker color
    *  @param {number} style - marker style
    *  @param {number} size - marker size */
   TAttMarkerHandler.prototype.change = function(color, style, size) {
      this.changed = true;

      if (color !== undefined) this.color = color;
      if ((style !== undefined) && (style >= 0)) this.style = style;
      if (size !== undefined) this.size = size; else size = this.size;

      this.x0 = this.y0 = 0;

      if ((this.style === 1) || (this.style === 777)) {
         this.fill = false;
         this.marker = "h1";
         this.size = 1;
         this.optimized = true;
         this.resetPos();
         return true;
      }

      this.optimized = false;

      let marker_kind = jsrp.root_markers[this.style];
      if (marker_kind === undefined) marker_kind = 100;
      let shape = marker_kind % 100;

      this.fill = (marker_kind >= 100);

      switch (this.style) {
         case 1: this.size = 1; this.scale = 1; break;
         case 6: this.size = 2; this.scale = 1; break;
         case 7: this.size = 3; this.scale = 1; break;
         default: this.size = size; this.scale = 8;
      }

      size = this.getFullSize();

      this.ndig = (size > 7) ? 0 : ((size > 2) ? 1 : 2);
      if (shape == 6) this.ndig++;
      let half = (size / 2).toFixed(this.ndig), full = size.toFixed(this.ndig);

      switch (shape) {
         case 0: // circle
            this.x0 = -parseFloat(half);
            full = (parseFloat(half) * 2).toFixed(this.ndig);
            this.marker = "a" + half + "," + half + ",0,1,0," + full + ",0a" + half + "," + half + ",0,1,0,-" + full + ",0z";
            break;
         case 1: // cross
            let d = (size / 3).toFixed(this.ndig);
            this.x0 = this.y0 = size / 6;
            this.marker = "h" + d + "v-" + d + "h-" + d + "v-" + d + "h-" + d + "v" + d + "h-" + d + "v" + d + "h" + d + "v" + d + "h" + d + "z";
            break;
         case 2: // diamond
            this.x0 = -size / 2;
            this.marker = "l" + half + ",-" + half + "l" + half + "," + half + "l-" + half + "," + half + "z";
            break;
         case 3: // square
            this.x0 = this.y0 = -size / 2;
            this.marker = "v" + full + "h" + full + "v-" + full + "z";
            break;
         case 4: // triangle-up
            this.y0 = size / 2;
            this.marker = "l-" + half + ",-" + full + "h" + full + "z";
            break;
         case 5: // triangle-down
            this.y0 = -size / 2;
            this.marker = "l-" + half + "," + full + "h" + full + "z";
            break;
         case 6: // star
            this.y0 = -size / 2;
            this.marker = "l" + (size / 3).toFixed(this.ndig) + "," + full +
               "l-" + (5 / 6 * size).toFixed(this.ndig) + ",-" + (5 / 8 * size).toFixed(this.ndig) +
               "h" + full +
               "l-" + (5 / 6 * size).toFixed(this.ndig) + "," + (5 / 8 * size).toFixed(this.ndig) + "z";
            break;
         case 7: // asterisk
            this.x0 = this.y0 = -size / 2;
            this.marker = "l" + full + "," + full +
               "m0,-" + full + "l-" + full + "," + full +
               "m0,-" + half + "h" + full + "m-" + half + ",-" + half + "v" + full;
            break;
         case 8: // plus
            this.y0 = -size / 2;
            this.marker = "v" + full + "m-" + half + ",-" + half + "h" + full;
            break;
         case 9: // mult
            this.x0 = this.y0 = -size / 2;
            this.marker = "l" + full + "," + full + "m0,-" + full + "l-" + full + "," + full;
            break;
         default: // diamand
            this.x0 = -size / 2;
            this.marker = "l" + half + ",-" + half + "l" + half + "," + half + "l-" + half + "," + half + "z";
            break;
      }

      return true;
   }

   /** @summary get stroke color */
   TAttMarkerHandler.prototype.getStrokeColor = function() { return this.stroke ? this.color : "none"; }

   /** @summary get fill color */
   TAttMarkerHandler.prototype.getFillColor = function() { return this.fill ? this.color : "none"; }

   /** @summary Apply marker styles to created element */
   TAttMarkerHandler.prototype.apply = function(selection) {
      selection.style('stroke', this.stroke ? this.color : "none");
      selection.style('fill', this.fill ? this.color : "none");
   }

   /** @summary Method used when color or pattern were changed with OpenUi5 widgets.
    * @private */
   TAttMarkerHandler.prototype.verifyDirectChange = function(/* painter */) {
      this.change(this.color, parseInt(this.style), parseFloat(this.size));
   }

   /** @summary Create sample with marker in given SVG element
     * @param {selection} svg - SVG element
     * @param {number} width - width of sample SVG
     * @param {number} height - height of sample SVG
     * @private */
   TAttMarkerHandler.prototype.createSample = function(svg, width, height) {
      this.resetPos();

      svg.append("path")
         .attr("d", this.create(width / 2, height / 2))
         .call(this.func);
   }

   // =======================================================================

   /**
     * @summary Handle for line attributes
     *
     * @class
     * @memberof JSROOT
     * @param {object} attr - TAttLine object
     * @private
     */

   function TAttLineHandler(args) {
      this.func = this.apply.bind(this);
      this.used = true;
      if (args._typename && (args.fLineStyle !== undefined)) args = { attr: args };

      this.setArgs(args);
   }

   /** @summary Set line attributes.
     * @param {object} args - specify attributes by different ways
     * @param {object} args.attr - TAttLine object with appropriate data members or
     * @param {string} args.color - color in html like rgb(10,0,0) or "red"
     * @param {number} args.style - line style number
     * @param {number} args.width - line width */
   TAttLineHandler.prototype.setArgs = function(args) {
      if (args.attr) {
         args.color = args.color0 || (args.painter ? args.painter.getColor(args.attr.fLineColor) : jsrp.getColor(args.attr.fLineColor));
         if (args.width === undefined) args.width = args.attr.fLineWidth;
         args.style = args.attr.fLineStyle;
      } else if (typeof args.color == 'string') {
         if ((args.color !== 'none') && !args.width) args.width = 1;
      } else if (typeof args.color == 'number') {
         args.color = args.painter ? args.painter.getColor(args.color) : jsrp.getColor(args.color);
      }

      if (args.width === undefined)
         args.width = (args.color && args.color != 'none') ? 1 : 0;

      this.color = (args.width === 0) ? 'none' : args.color;
      this.width = args.width;
      this.style = args.style;

      if (args.can_excl) {
         this.excl_side = this.excl_width = 0;
         if (Math.abs(this.width) > 99) {
            // exclusion graph
            this.excl_side = (this.width < 0) ? -1 : 1;
            this.excl_width = Math.floor(this.width / 100) * 5;
            this.width = Math.abs(this.width % 100); // line width
         }
      }

      // if custom color number used, use lightgrey color to show lines
      if (!this.color && (this.width > 0))
         this.color = 'lightgrey';
   }

   /** @summary Change exclusion attributes */
   TAttLineHandler.prototype.changeExcl = function(side, width) {
      if (width !== undefined)
         this.excl_width = width;
      if (side !== undefined) {
         this.excl_side = side;
         if ((this.excl_width === 0) && (this.excl_side !== 0)) this.excl_width = 20;
      }
      this.changed = true;
   }

   /** @summary returns true if line attribute is empty and will not be applied. */
   TAttLineHandler.prototype.empty = function() { return this.color == 'none'; }

   /** @summary Applies line attribute to selection.
     * @param {object} selection - d3.js selection */
   TAttLineHandler.prototype.apply = function(selection) {
      this.used = true;
      if (this.empty())
         selection.style('stroke', null)
            .style('stroke-width', null)
            .style('stroke-dasharray', null);
      else
         selection.style('stroke', this.color)
            .style('stroke-width', this.width)
            .style('stroke-dasharray', jsrp.root_line_styles[this.style] || null);
   }

   /** @summary Change line attributes */
   TAttLineHandler.prototype.change = function(color, width, style) {
      if (color !== undefined) this.color = color;
      if (width !== undefined) this.width = width;
      if (style !== undefined) this.style = style;
      this.changed = true;
   }

   /** @summary Create sample element inside primitive SVG - used in context menu */
   TAttLineHandler.prototype.createSample = function(svg, width, height) {
      svg.append("path")
         .attr("d", "M0," + height / 2 + "h" + width)
         .call(this.func);
   }

   // =======================================================================

   /**
     * @summary Handle for fill attributes
     *
     * @class
     * @memberof JSROOT
     * @param {object} args - different arguments to set fill attributes, see {@link JSROOT.TAttFillHandler.setArgs} for more info
     * @param {number} [args.kind = 2] - 1 means object drawing where combination fillcolor==0 and fillstyle==1001 means no filling,  2 means all other objects where such combination is white-color filling
     * @private
     */

   function TAttFillHandler(args) {
      this.color = "none";
      this.colorindx = 0;
      this.pattern = 0;
      this.used = true;
      this.kind = args.kind || 2;
      this.changed = false;
      this.func = this.apply.bind(this);
      this.setArgs(args);
      this.changed = false; // unset change property that
   }

   /** @summary Set fill style as arguments
     * @param {object} args - different arguments to set fill attributes
     * @param {object} [args.attr] - TAttFill object
     * @param {number} [args.color] - color id
     * @param {number} [args.pattern] - filll pattern id
     * @param {object} [args.svg] - SVG element to store newly created patterns
     * @param {string} [args.color_as_svg] - color in SVG format */
   TAttFillHandler.prototype.setArgs = function(args) {
      if (args.attr && (typeof args.attr == 'object')) {
         if ((args.pattern === undefined) && (args.attr.fFillStyle !== undefined)) args.pattern = args.attr.fFillStyle;
         if ((args.color === undefined) && (args.attr.fFillColor !== undefined)) args.color = args.attr.fFillColor;
      }
      this.change(args.color, args.pattern, args.svg, args.color_as_svg, args.painter);
   }

   /** @summary Apply fill style to selection */
   TAttFillHandler.prototype.apply = function(selection) {
      this.used = true;

      selection.style('fill', this.getFillColor());

      if ('opacity' in this)
         selection.style('opacity', this.opacity);

      if ('antialias' in this)
         selection.style('antialias', this.antialias);
   }

   /** @summary Returns fill color (or pattern url) */
   TAttFillHandler.prototype.getFillColor = function() { return this.pattern_url || this.color; }

   /** @summary Returns fill color without pattern url.
    * @desc If empty, alternative color will be provided
    * @param {string} [altern] - alternative color which returned when fill color not exists
    * @private */
   TAttFillHandler.prototype.getFillColorAlt = function(altern) { return this.color && (this.color != "none") ? this.color : altern; }

   /** @summary Returns true if color not specified or fill style not specified */
   TAttFillHandler.prototype.empty = function() {
      let fill = this.getFillColor();
      return !fill || (fill == 'none');
   }

   /** @summary Set solid fill color as fill pattern
     * @param {string} col - solid color */
   TAttFillHandler.prototype.setSolidColor = function(col) {
      delete this.pattern_url;
      this.color = col;
      this.pattern = 1001;
   }

   /** @summary Check if solid fill is used, also color can be checked
     * @param {string} [solid_color] - when specified, checks if fill color matches */
   TAttFillHandler.prototype.isSolid = function(solid_color) {
      if (this.pattern !== 1001) return false;
      return !solid_color || solid_color == this.color;
   }

   /** @summary Method used when color or pattern were changed with OpenUi5 widgets
     * @private */
   TAttFillHandler.prototype.verifyDirectChange = function(painter) {
      if (typeof this.pattern == 'string') this.pattern = parseInt(this.pattern);
      if (isNaN(this.pattern)) this.pattern = 0;

      this.change(this.color, this.pattern, painter ? painter.getCanvSvg() : null, true, painter);
   }

   /** @summary Method to change fill attributes.
     * @param {number} color - color index
     * @param {number} pattern - pattern index
     * @param {selection} svg - top canvas element for pattern storages
     * @param {string} [color_as_svg] - when color is string, interpret as normal SVG color
     * @param {object} [painter] - when specified, used to extract color by index */
   TAttFillHandler.prototype.change = function(color, pattern, svg, color_as_svg, painter) {
      delete this.pattern_url;
      this.changed = true;

      if ((color !== undefined) && !isNaN(color) && !color_as_svg)
         this.colorindx = parseInt(color);

      if ((pattern !== undefined) && !isNaN(pattern)) {
         this.pattern = parseInt(pattern);
         delete this.opacity;
         delete this.antialias;
      }

      if ((this.pattern == 1000) && (this.colorindx === 0)) {
         this.pattern_url = 'white';
         return true;
      }

      if (this.pattern == 1000) this.pattern = 1001;

      if (this.pattern < 1001) {
         this.pattern_url = 'none';
         return true;
      }

      if (this.isSolid() && (this.colorindx === 0) && (this.kind === 1) && !color_as_svg) {
         this.pattern_url = 'none';
         return true;
      }

      let indx = this.colorindx;

      if (color_as_svg) {
         this.color = color;
         indx = 10000 + JSROOT._.id_counter++; // use fictional unique index far away from existing color indexes
      } else {
         this.color = painter ? painter.getColor(indx) : jsrp.getColor(indx);
      }

      if (typeof this.color != 'string') this.color = "none";

      if (this.isSolid()) return true;

      if ((this.pattern >= 4000) && (this.pattern <= 4100)) {
         // special transparent colors (use for subpads)
         this.opacity = (this.pattern - 4000) / 100;
         return true;
      }

      if (!svg || svg.empty() || (this.pattern < 3000)) return false;

      let id = "pat_" + this.pattern + "_" + indx,
         defs = svg.select('.canvas_defs');

      if (defs.empty())
         defs = svg.insert("svg:defs", ":first-child").attr("class", "canvas_defs");

      this.pattern_url = "url(#" + id + ")";
      this.antialias = false;

      if (!defs.select("." + id).empty()) {
         if (color_as_svg) console.log('find id in def', id);
         return true;
      }

      let lines = "", lfill = null, fills = "", fills2 = "", w = 2, h = 2;

      switch (this.pattern) {
         case 3001: w = h = 2; fills = "M0,0h1v1h-1zM1,1h1v1h-1z"; break;
         case 3002: w = 4; h = 2; fills = "M1,0h1v1h-1zM3,1h1v1h-1z"; break;
         case 3003: w = h = 4; fills = "M2,1h1v1h-1zM0,3h1v1h-1z"; break;
         case 3004: w = h = 8; lines = "M8,0L0,8"; break;
         case 3005: w = h = 8; lines = "M0,0L8,8"; break;
         case 3006: w = h = 4; lines = "M1,0v4"; break;
         case 3007: w = h = 4; lines = "M0,1h4"; break;
         case 3008:
            w = h = 10;
            fills = "M0,3v-3h3ZM7,0h3v3ZM0,7v3h3ZM7,10h3v-3ZM5,2l3,3l-3,3l-3,-3Z";
            lines = "M0,3l5,5M3,10l5,-5M10,7l-5,-5M7,0l-5,5";
            break;
         case 3009: w = 12; h = 12; lines = "M0,0A6,6,0,0,0,12,0M6,6A6,6,0,0,0,12,12M6,6A6,6,0,0,1,0,12"; lfill = "none"; break;
         case 3010: w = h = 10; lines = "M0,2h10M0,7h10M2,0v2M7,2v5M2,7v3"; break; // bricks
         case 3011: w = 9; h = 18; lines = "M5,0v8M2,1l6,6M8,1l-6,6M9,9v8M6,10l3,3l-3,3M0,9v8M3,10l-3,3l3,3"; lfill = "none"; break;
         case 3012: w = 10; h = 20; lines = "M5,1A4,4,0,0,0,5,9A4,4,0,0,0,5,1M0,11A4,4,0,0,1,0,19M10,11A4,4,0,0,0,10,19"; lfill = "none"; break;
         case 3013: w = h = 7; lines = "M0,0L7,7M7,0L0,7"; lfill = "none"; break;
         case 3014: w = h = 16; lines = "M0,0h16v16h-16v-16M0,12h16M12,0v16M4,0v8M4,4h8M0,8h8M8,4v8"; lfill = "none"; break;
         case 3015: w = 6; h = 12; lines = "M2,1A2,2,0,0,0,2,5A2,2,0,0,0,2,1M0,7A2,2,0,0,1,0,11M6,7A2,2,0,0,0,6,11"; lfill = "none"; break;
         case 3016: w = 12; h = 7; lines = "M0,1A3,2,0,0,1,3,3A3,2,0,0,0,9,3A3,2,0,0,1,12,1"; lfill = "none"; break;
         case 3017: w = h = 4; lines = "M3,1l-2,2"; break;
         case 3018: w = h = 4; lines = "M1,1l2,2"; break;
         case 3019:
            w = h = 12;
            lines = "M1,6A5,5,0,0,0,11,6A5,5,0,0,0,1,6h-1h1A5,5,0,0,1,6,11v1v-1" +
               "A5,5,0,0,1,11,6h1h-1A5,5,0,0,1,6,1v-1v1A5,5,0,0,1,1,6";
            lfill = "none";
            break;
         case 3020: w = 7; h = 12; lines = "M1,0A2,3,0,0,0,3,3A2,3,0,0,1,3,9A2,3,0,0,0,1,12"; lfill = "none"; break;
         case 3021: w = h = 8; lines = "M8,2h-2v4h-4v2M2,0v2h-2"; lfill = "none"; break; // left stairs
         case 3022: w = h = 8; lines = "M0,2h2v4h4v2M6,0v2h2"; lfill = "none"; break; // right stairs
         case 3023: w = h = 8; fills = "M4,0h4v4zM8,4v4h-4z"; fills2 = "M4,0L0,4L4,8L8,4Z"; break;
         case 3024: w = h = 16; fills = "M0,8v8h2v-8zM8,0v8h2v-8M4,14v2h12v-2z"; fills2 = "M0,2h8v6h4v-6h4v12h-12v-6h-4z"; break;
         case 3025: w = h = 18; fills = "M5,13v-8h8ZM18,0v18h-18l5,-5h8v-8Z"; break;
         default:
            if ((this.pattern > 3025) && (this.pattern < 3100)) {
               // same as 3002, see TGX11.cxx, line 2234
               w = 4; h = 2; fills = "M1,0h1v1h-1zM3,1h1v1h-1z"; break;
            }

            let code = this.pattern % 1000,
               k = code % 10, j = ((code - k) % 100) / 10, i = (code - j * 10 - k) / 100;
            if (!i) break;

            let sz = i * 12;  // axis distance between lines

            w = h = 6 * sz; // we use at least 6 steps

            function produce(dy, swap) {
               let pos = [], step = sz, y1 = 0, y2, max = h;

               // reduce step for smaller angles to keep normal distance approx same
               if (Math.abs(dy) < 3) step = Math.round(sz / 12 * 9);
               if (dy == 0) { step = Math.round(sz / 12 * 8); y1 = step / 2; }
               else if (dy > 0) max -= step; else y1 = step;

               while (y1 <= max) {
                  y2 = y1 + dy * step;
                  if (y2 < 0) {
                     let x2 = Math.round(y1 / (y1 - y2) * w);
                     pos.push(0, y1, x2, 0);
                     pos.push(w, h - y1, w - x2, h);
                  } else if (y2 > h) {
                     let x2 = Math.round((h - y1) / (y2 - y1) * w);
                     pos.push(0, y1, x2, h);
                     pos.push(w, h - y1, w - x2, 0);
                  } else {
                     pos.push(0, y1, w, y2);
                  }
                  y1 += step;
               }
               for (let k = 0; k < pos.length; k += 4)
                  if (swap) lines += "M" + pos[k + 1] + "," + pos[k] + "L" + pos[k + 3] + "," + pos[k + 2];
                  else lines += "M" + pos[k] + "," + pos[k + 1] + "L" + pos[k + 2] + "," + pos[k + 3];
            }

            switch (j) {
               case 0: produce(0); break;
               case 1: produce(1); break;
               case 2: produce(2); break;
               case 3: produce(3); break;
               case 4: produce(6); break;
               case 6: produce(3, true); break;
               case 7: produce(2, true); break;
               case 8: produce(1, true); break;
               case 9: produce(0, true); break;
            }

            switch (k) {
               case 0: if (j) produce(0); break;
               case 1: produce(-1); break;
               case 2: produce(-2); break;
               case 3: produce(-3); break;
               case 4: produce(-6); break;
               case 6: produce(-3, true); break;
               case 7: produce(-2, true); break;
               case 8: produce(-1, true); break;
               case 9: if (j != 9) produce(0, true); break;
            }

            break;
      }

      if (!fills && !lines) return false;

      let patt = defs.append('svg:pattern').attr("id", id).attr("class", id).attr("patternUnits", "userSpaceOnUse")
         .attr("width", w).attr("height", h);

      if (fills2) {
         let col = d3.rgb(this.color);
         col.r = Math.round((col.r + 255) / 2); col.g = Math.round((col.g + 255) / 2); col.b = Math.round((col.b + 255) / 2);
         patt.append("svg:path").attr("d", fills2).style("fill", col);
      }
      if (fills) patt.append("svg:path").attr("d", fills).style("fill", this.color);
      if (lines) patt.append("svg:path").attr("d", lines).style('stroke', this.color).style("stroke-width", 1).style("fill", lfill);

      return true;
   }

   /** @summary Create sample of fill pattern inside SVG
    * @private */
   TAttFillHandler.prototype.createSample = function(sample_svg, width, height) {

      // we need to create extra handle to change
      let sample = new TAttFillHandler({ svg: sample_svg, pattern: this.pattern, color: this.color, color_as_svg: true });

      sample_svg.append("path")
         .attr("d", "M0,0h" + width + "v" + height + "h-" + width + "z")
         .call(sample.func);
   }

   // ===========================================================================

   /**
    * @summary Helper class for font handling
    *
    * @class
    * @memberof JSROOT
    * @private
    */

   function FontHandler(fontIndex, size, scale, name, style, weight) {

      this.name = "Arial";
      this.style = null;
      this.weight = null;

      if (scale && (size < 1)) {
         size *= scale;
         this.scaled = true;
      }

      this.size = Math.round(size || 11);
      this.scale = scale;

      if (fontIndex !== null) {

         let indx = Math.floor(fontIndex / 10),
             fontName = jsrp.root_fonts[indx] || "";

         while (fontName.length > 0) {
            if (fontName[0] === 'b')
               this.weight = "bold";
            else if (fontName[0] === 'i')
               this.style = "italic";
            else if (fontName[0] === 'o')
               this.style = "oblique";
            else
               break;
            fontName = fontName.substr(1);
         }

         if (fontName == 'Symbol')
            this.weight = this.style = null;

         this.name = fontName;
         this.aver_width = jsrp.root_fonts_aver_width[indx] || 0.55;
      } else {
         this.name = name;
         this.style = style || null;
         this.weight = weight || null;
         this.aver_width = 0.55;
      }

      this.func = this.setFont.bind(this);
   }

   /** @summary Assigns font-related attributes */
   FontHandler.prototype.setFont = function(selection, arg) {
      selection.attr("font-family", this.name);
      if (arg != 'without-size')
         selection.attr("font-size", this.size)
                  .attr("xml:space", "preserve");
      if (this.weight)
         selection.attr("font-weight", this.weight);
      if (this.style)
         selection.attr("font-style", this.style);
   }

   /** @summary Set font size (optional) */
   FontHandler.prototype.setSize = function(size) {
      this.size = Math.round(size);
   }

   /** @summary Set text color (optional) */
   FontHandler.prototype.setColor = function(color) {
      this.color = color;
   }

   /** @summary Set text align (optional) */
   FontHandler.prototype.setAlign = function(align) {
      this.align = align;
   }

   /** @summary Set text angle (optional) */
   FontHandler.prototype.setAngle = function(angle) {
      this.angle = angle;
   }

   /** @summary Allign angle to step raster, add optional offset */
   FontHandler.prototype.roundAngle = function(step, offset) {
      this.angle = parseInt(this.angle || 0);
      if (isNaN(this.angle)) this.angle = 0;
      this.angle = Math.round(this.angle/step) * step + (offset || 0);
      if (this.angle < 0)
         this.angle += 360;
      else if (this.angle >= 360)
         this.angle -= 360;
   }

   /** @summary Clears all font-related attributes */
   FontHandler.prototype.clearFont = function(selection) {
      selection.attr("font-family", null)
               .attr("font-size", null)
               .attr("xml:space", null)
               .attr("font-weight", null)
               .attr("font-style", null);
   }

   /** @summary required for reasonable scaling of text in node.js
     * @returns approximate width of given label */
   FontHandler.prototype.approxTextWidth = function(label) { return label.length * this.size * this.aver_width; }

  // ===========================================================================

   /** @summary Tries to choose time format for provided time interval
     * @private */
   jsrp.chooseTimeFormat = function(awidth, ticks) {
      if (awidth < .5) return ticks ? "%S.%L" : "%H:%M:%S.%L";
      if (awidth < 30) return ticks ? "%Mm%S" : "%H:%M:%S";
      awidth /= 60; if (awidth < 30) return ticks ? "%Hh%M" : "%d/%m %H:%M";
      awidth /= 60; if (awidth < 12) return ticks ? "%d-%Hh" : "%d/%m/%y %Hh";
      awidth /= 24; if (awidth < 15.218425) return ticks ? "%d/%m" : "%d/%m/%y";
      awidth /= 30.43685; if (awidth < 6) return "%d/%m/%y";
      awidth /= 12; if (awidth < 2) return ticks ? "%m/%y" : "%d/%m/%y";
      return "%Y";
   }

   /** @summary Returns time format
     * @param {TAxis} axis - TAxis object
     * @private */
   jsrp.getTimeFormat = function(axis) {
      let idF = axis.fTimeFormat.indexOf('%F');
      return (idF >= 0) ? axis.fTimeFormat.substr(0, idF) : axis.fTimeFormat;
   }

   /** @summary Return time offset value for given TAxis object
     * @private */
   jsrp.getTimeOffset = function(axis) {
      let dflt_time_offset = 788918400000;
      if (!axis) return dflt_time_offset;
      let idF = axis.fTimeFormat.indexOf('%F');
      if (idF < 0) return JSROOT.gStyle.fTimeOffset * 1000;
      let sof = axis.fTimeFormat.substr(idF + 2);
      // default string in axis offset
      if (sof.indexOf('1995-01-01 00:00:00s0') == 0) return dflt_time_offset;
      // special case, used from DABC painters
      if ((sof == "0") || (sof == "")) return 0;

      // decode time from ROOT string
      function next(separ, min, max) {
         let pos = sof.indexOf(separ);
         if (pos < 0) { pos = ""; return min; }
         let val = parseInt(sof.substr(0, pos));
         sof = sof.substr(pos + 1);
         if (isNaN(val) || (val < min) || (val > max)) { pos = ""; return min; }
         return val;
      }

      let year = next("-", 1970, 2300),
         month = next("-", 1, 12) - 1,
         day = next(" ", 1, 31),
         hour = next(":", 0, 23),
         min = next(":", 0, 59),
         sec = next("s", 0, 59),
         msec = next(" ", 0, 999);

      let dt = new Date(Date.UTC(year, month, day, hour, min, sec, msec));

      let offset = dt.getTime();

      // now also handle suffix like GMT or GMT -0600
      sof = sof.toUpperCase();

      if (sof.indexOf('GMT') == 0) {
         offset += dt.getTimezoneOffset() * 60000;
         sof = sof.substr(4).trim();
         if (sof.length > 3) {
            let p = 0, sign = 1000;
            if (sof[0] == '-') { p = 1; sign = -1000; }
            offset -= sign * (parseInt(sof.substr(p, 2)) * 3600 + parseInt(sof.substr(p + 2, 2)) * 60);
         }
      }

      return offset;
   }

   /** @summary Function used to provide svg:path for the smoothed curves.
     * @desc reuse code from d3.js. Used in TH1, TF1 and TGraph painters
     * @param {string} kind  should contain "bezier" or "line".
     * If first symbol "L", then it used to continue drawing
     * @private */
   jsrp.buildSvgPath = function(kind, bins, height, ndig) {

      let smooth = kind.indexOf("bezier") >= 0;

      if (ndig === undefined) ndig = smooth ? 2 : 0;
      if (height === undefined) height = 0;

      function jsroot_d3_svg_lineSlope(p0, p1) {
         return (p1.gry - p0.gry) / (p1.grx - p0.grx);
      }
      function jsroot_d3_svg_lineFiniteDifferences(points) {
         let i = 0, j = points.length - 1, m = [], p0 = points[0], p1 = points[1], d = m[0] = jsroot_d3_svg_lineSlope(p0, p1);
         while (++i < j) {
            m[i] = (d + (d = jsroot_d3_svg_lineSlope(p0 = p1, p1 = points[i + 1]))) / 2;
         }
         m[i] = d;
         return m;
      }
      function jsroot_d3_svg_lineMonotoneTangents(points) {
         let d, a, b, s, m = jsroot_d3_svg_lineFiniteDifferences(points), i = -1, j = points.length - 1;
         while (++i < j) {
            d = jsroot_d3_svg_lineSlope(points[i], points[i + 1]);
            if (Math.abs(d) < 1e-6) {
               m[i] = m[i + 1] = 0;
            } else {
               a = m[i] / d;
               b = m[i + 1] / d;
               s = a * a + b * b;
               if (s > 9) {
                  s = d * 3 / Math.sqrt(s);
                  m[i] = s * a;
                  m[i + 1] = s * b;
               }
            }
         }
         i = -1;
         while (++i <= j) {
            s = (points[Math.min(j, i + 1)].grx - points[Math.max(0, i - 1)].grx) / (6 * (1 + m[i] * m[i]));
            points[i].dgrx = s || 0;
            points[i].dgry = m[i] * s || 0;
         }
      }

      let res = { path: "", close: "" }, bin = bins[0], maxy = Math.max(bin.gry, height + 5),
         currx = Math.round(bin.grx), curry = Math.round(bin.gry), dx, dy, npnts = bins.length;

      function conv(val) {
         let vvv = Math.round(val);
         if ((ndig == 0) || (vvv === val)) return vvv.toString();
         let str = val.toFixed(ndig);
         while ((str[str.length - 1] == '0') && (str.lastIndexOf(".") < str.length - 1))
            str = str.substr(0, str.length - 1);
         if (str[str.length - 1] == '.')
            str = str.substr(0, str.length - 1);
         if (str == "-0") str = "0";
         return str;
      }

      res.path = ((kind[0] == "L") ? "L" : "M") + conv(bin.grx) + "," + conv(bin.gry);

      // just calculate all deltas, can be used to build exclusion
      if (smooth || kind.indexOf('calc') >= 0)
         jsroot_d3_svg_lineMonotoneTangents(bins);

      if (smooth) {
         // build smoothed curve
         res.path += "c" + conv(bin.dgrx) + "," + conv(bin.dgry) + ",";
         for (let n = 1; n < npnts; ++n) {
            let prev = bin;
            bin = bins[n];
            if (n > 1) res.path += "s";
            res.path += conv(bin.grx - bin.dgrx - prev.grx) + "," + conv(bin.gry - bin.dgry - prev.gry) + "," + conv(bin.grx - prev.grx) + "," + conv(bin.gry - prev.gry);
            maxy = Math.max(maxy, prev.gry);
         }
      } else if (npnts < 10000) {
         // build simple curve
         for (let n = 1; n < npnts; ++n) {
            bin = bins[n];
            dx = Math.round(bin.grx) - currx;
            dy = Math.round(bin.gry) - curry;
            if (dx && dy) res.path += "l" + dx + "," + dy;
            else if (!dx && dy) res.path += "v" + dy;
            else if (dx && !dy) res.path += "h" + dx;
            currx += dx; curry += dy;
            maxy = Math.max(maxy, curry);
         }
      } else {
         // build line with trying optimize many vertical moves
         let lastx, lasty, cminy = curry, cmaxy = curry, prevy = curry;
         for (let n = 1; n < npnts; ++n) {
            bin = bins[n];
            lastx = Math.round(bin.grx);
            lasty = Math.round(bin.gry);
            maxy = Math.max(maxy, lasty);
            dx = lastx - currx;
            if (dx === 0) {
               // if X not change, just remember amplitude and
               cminy = Math.min(cminy, lasty);
               cmaxy = Math.max(cmaxy, lasty);
               prevy = lasty;
               continue;
            }

            if (cminy !== cmaxy) {
               if (cminy != curry) res.path += "v" + (cminy - curry);
               res.path += "v" + (cmaxy - cminy);
               if (cmaxy != prevy) res.path += "v" + (prevy - cmaxy);
               curry = prevy;
            }
            dy = lasty - curry;
            if (dy) res.path += "l" + dx + "," + dy;
            else res.path += "h" + dx;
            currx = lastx; curry = lasty;
            prevy = cminy = cmaxy = lasty;
         }

         if (cminy != cmaxy) {
            if (cminy != curry) res.path += "v" + (cminy - curry);
            res.path += "v" + (cmaxy - cminy);
            if (cmaxy != prevy) res.path += "v" + (prevy - cmaxy);
            curry = prevy;
         }

      }

      if (height > 0)
         res.close = "L" + conv(bin.grx) + "," + conv(maxy) +
            "h" + conv(bins[0].grx - bin.grx) + "Z";

      return res;
   }

   /** @summary Returns visible rect of element
     * @param {object} elem - d3.select object with element
     * @param {string} [kind] - which size method is used
     * @desc kind = 'bbox' use getBBox, works only with SVG
     * kind = 'full' - full size of element, using getBoundingClientRect function
     * kind = 'nopadding' - excludes padding area
     * With node.js can use "width" and "height" attributes when provided in element
     * @private */
   jsrp.getElementRect = (elem, sizearg) => {
      if (JSROOT.nodejs && (sizearg != 'bbox'))
         return { x: 0, y: 0, width: parseInt(elem.attr("width")), height: parseInt(elem.attr("height")) };

      function styleValue(name) {
         let value = elem.style(name);
         if (!value || (typeof value !== 'string')) return 0;
         value = parseFloat(value.replace("px", ""));
         return isNaN(value) ? 0 : Math.round(value);
      }

      let rect = elem.node().getBoundingClientRect();
      if ((sizearg == 'bbox') && (parseFloat(rect.width) > 0))
         rect = elem.node().getBBox();

      let res = { x: 0, y: 0, width: parseInt(rect.width), height: parseInt(rect.height) };
      if (rect.left !== undefined) {
         res.x = parseInt(rect.left);
         res.y = parseInt(rect.top);
      } else if (rect.x !== undefined) {
         res.x = parseInt(rect.x);
         res.y = parseInt(rect.y);
      }

      if ((sizearg === undefined) || (sizearg == 'nopadding')) {
         // this is size exclude padding area
         res.width -= styleValue('padding-left') + styleValue('padding-right');
         res.height -= styleValue('padding-top') + styleValue('padding-bottom');
      }

      return res;
   }

   /** @summary Calculate absolute position of provided element in canvas
     * @private */
   jsrp.getAbsPosInCanvas = (sel, pos) => {
      while (!sel.empty() && !sel.classed('root_canvas') && pos) {
         let cl = sel.attr("class");
         if (cl && ((cl.indexOf("root_frame") >= 0) || (cl.indexOf("__root_pad_") >= 0))) {
            pos.x += sel.property("draw_x") || 0;
            pos.y += sel.property("draw_y") || 0;
         }
         sel = d3.select(sel.node().parentNode);
      }
      return pos;
   }

   // ========================================================================================

   /**
    * @summary Base painter class in JSROOT
    *
    * @class
    * @memberof JSROOT
    * @param {object|string} [dom] - dom element or id of dom element
    */

   function BasePainter(dom) {
      this.divid = null; // either id of DOM element or element itself
      if (dom) this.setDom(dom);
   }

   /** @summary Assign painter to specified DOM element
     * @param {string|object} elem - element ID or DOM Element
     * @desc Normally DOM element should be already assigned in constructor
     * @protected */
   BasePainter.prototype.setDom = function(elem) {
      if (elem !== undefined) {
         this.divid = elem;
         delete this._selected_main;
      }
   }

   /** @summary Returns assigned dom element */
   BasePainter.prototype.getDom = function() {
      return this.divid;
   }

   /** @summary Selects main HTML element assigned for drawing
     * @desc if main element was layouted, returns main element inside layout
     * @param {string} [is_direct] - if 'origin' specified, returns original element even if actual drawing moved to some other place
     * @returns {object} d3.select object for main element for drawing */
   BasePainter.prototype.selectDom = function(is_direct) {

      if (!this.divid) return d3.select(null);

      let res = this._selected_main;
      if (!res) {
         if (typeof this.divid == "string") {
            let id = this.divid;
            if (id[0] != '#') id = "#" + id;
            res = d3.select(id);
            if (!res.empty()) this.divid = res.node();
         } else {
            res = d3.select(this.divid);
         }
         this._selected_main = res;
      }

      if (!res || res.empty() || (is_direct === 'origin')) return res;

      let use_enlarge = res.property('use_enlarge'),
         layout = res.property('layout') || 'simple',
         layout_selector = (layout == 'simple') ? "" : res.property('layout_selector');

      if (layout_selector) res = res.select(layout_selector);

      // one could redirect here
      if (!is_direct && !res.empty() && use_enlarge) res = d3.select("#jsroot_enlarge_div");

      return res;
   }

   function _accessTopPainter(painter, on) {
      let main = painter.selectDom().node(),
          chld = main ? main.firstChild : null;
      if (!chld) return null;
      if (on === true) {
         chld.painter = painter;
      } else if (on === false)
         delete chld.painter;
      return chld.painter;
   }

   /** @summary Set painter, stored in first child element
     * @desc Only make sense after first drawing is completed and any child element add to configured DOM
     * @protected */
   BasePainter.prototype.setTopPainter = function() {
      _accessTopPainter(this, true);
   }

   /** @summary Return top painter set for the selected dom element
     * @protected */
   BasePainter.prototype.getTopPainter = function() {
      return _accessTopPainter(this);
   }

   /** @summary Clear reference on top painter
     * @protected */
   BasePainter.prototype.clearTopPainter = function() {
      _accessTopPainter(this, false);
   }

   /** @summary Generic method to cleanup painter
     * @desc Removes all visible elements and all internal data */
   BasePainter.prototype.cleanup = function(keep_origin) {
      this.clearTopPainter();
      let origin = this.selectDom('origin');
      if (!origin.empty() && !keep_origin) origin.html("");
      this.divid = null;
      delete this._selected_main;

      if (this._hpainter && typeof this._hpainter.removePainter === 'function')
         this._hpainter.removePainter(this);

      delete this._hitemname;
      delete this._hdrawopt;
      delete this._hpainter;
   }

   /** @summary Checks if draw elements were resized and drawing should be updated
     * @returns {boolean} true if resize was detected
     * @protected
     * @abstract */
   BasePainter.prototype.checkResize = function(/* arg */) {}

   /** @summary Function checks if geometry of main div was changed.
     * @desc take into account enlarge state, used only in PadPainter class
     * @returns size of area when main div is drawn
     * @private */
   BasePainter.prototype.testMainResize = function(check_level, new_size, height_factor) {

      let enlarge = this.enlargeMain('state'),
         main_origin = this.selectDom('origin'),
         main = this.selectDom(),
         lmt = 5; // minimal size

      if (enlarge !== 'on') {
         if (new_size && new_size.width && new_size.height)
            main_origin.style('width', new_size.width + "px")
               .style('height', new_size.height + "px");
      }

      let rect_origin = jsrp.getElementRect(main_origin, true),
         can_resize = main_origin.attr('can_resize'),
         do_resize = false;

      if (can_resize == "height")
         if (height_factor && Math.abs(rect_origin.width * height_factor - rect_origin.height) > 0.1 * rect_origin.width) do_resize = true;

      if (((rect_origin.height <= lmt) || (rect_origin.width <= lmt)) &&
         can_resize && can_resize !== 'false') do_resize = true;

      if (do_resize && (enlarge !== 'on')) {
         // if zero size and can_resize attribute set, change container size

         if (rect_origin.width > lmt) {
            height_factor = height_factor || 0.66;
            main_origin.style('height', Math.round(rect_origin.width * height_factor) + 'px');
         } else if (can_resize !== 'height') {
            main_origin.style('width', '200px').style('height', '100px');
         }
      }

      let rect = jsrp.getElementRect(main),
         old_h = main.property('draw_height'),
         old_w = main.property('draw_width');

      rect.changed = false;

      if (old_h && old_w && (old_h > 0) && (old_w > 0)) {
         if ((old_h !== rect.height) || (old_w !== rect.width))
            if ((check_level > 1) || (rect.width / old_w < 0.66) || (rect.width / old_w > 1.5) ||
               (rect.height / old_h < 0.66) && (rect.height / old_h > 1.5)) rect.changed = true;
      } else {
         rect.changed = true;
      }

      return rect;
   }

   /** @summary Try enlarge main drawing element to full HTML page.
     * @param {string|boolean} action  - defines that should be done
     * @desc Possible values for action parameter:
     *    - true - try to enlarge
     *    - false - revert enlarge state
     *    - 'toggle' - toggle enlarge state
     *    - 'state' - only returns current enlarge state
     *    - 'verify' - check if element can be enlarged
     * if action not specified, just return possibility to enlarge main div
     * @protected */
   BasePainter.prototype.enlargeMain = function(action, skip_warning) {

      let main = this.selectDom(true),
         origin = this.selectDom('origin');

      if (main.empty() || !JSROOT.settings.CanEnlarge || (origin.property('can_enlarge') === false)) return false;

      if (action === undefined) return true;

      if (action === 'verify') return true;

      let state = origin.property('use_enlarge') ? "on" : "off";

      if (action === 'state') return state;

      if (action === 'toggle') action = (state === "off");

      let enlarge = d3.select("#jsroot_enlarge_div");

      if ((action === true) && (state !== "on")) {
         if (!enlarge.empty()) return false;

         enlarge = d3.select(document.body)
            .append("div")
            .attr("id", "jsroot_enlarge_div");

         let rect1 = jsrp.getElementRect(main),
             rect2 = jsrp.getElementRect(enlarge);

         // if new enlarge area not big enough, do not do it
         if ((rect2.width <= rect1.width) || (rect2.height <= rect1.height))
            if (rect2.width * rect2.height < rect1.width * rect1.height) {
               if (!skip_warning)
                  console.log('Enlarged area ' + rect2.width + "x" + rect2.height + ' smaller then original drawing ' + rect1.width + "x" + rect1.height);
               enlarge.remove();
               return false;
            }

         while (main.node().childNodes.length > 0)
            enlarge.node().appendChild(main.node().firstChild);

         origin.property('use_enlarge', true);

         return true;
      }
      if ((action === false) && (state !== "off")) {

         while (enlarge.node() && enlarge.node().childNodes.length > 0)
            main.node().appendChild(enlarge.node().firstChild);

         enlarge.remove();
         origin.property('use_enlarge', false);
         return true;
      }

      return false;
   }

   /** @summary Set item name, associated with the painter
     * @desc Used by {@link JSROOT.HierarchyPainter}
     * @private */
   BasePainter.prototype.setItemName = function(name, opt, hpainter) {
      if (typeof name === 'string')
         this._hitemname = name;
      else
         delete this._hitemname;
      // only upate draw option, never delete. null specified when update drawing
      if (typeof opt === 'string') this._hdrawopt = opt;

      this._hpainter = hpainter;
   }

   /** @summary Returns assigned item name
     * @desc Used with {@link JSROOT.HierarchyPainter} to identify drawn item name */
   BasePainter.prototype.getItemName = function() { return ('_hitemname' in this) ? this._hitemname : null; }

   /** @summary Returns assigned item draw option
     * @desc Used with {@link JSROOT.HierarchyPainter} to identify drawn item option */
   BasePainter.prototype.getItemDrawOpt = function() { return this._hdrawopt || ""; }

   // ==============================================================================


   /**
    * @summary Painter class for ROOT objects
    *
    * @class
    * @memberof JSROOT
    * @extends JSROOT.BasePainter
    * @param {object|string} dom - dom element or identifier
    * @param {object} obj - object to draw
    * @param {string} [opt] - object draw options
    */

   function ObjectPainter(dom, obj, opt) {
      BasePainter.call(this, dom);
      // this.draw_g = undefined; // container for all drawn objects
      // this._main_painter = undefined;  // main painter in the correspondent pad
      if (obj !== undefined) {
         this.pad_name = dom ? this.selectCurrentPad() : ""; // name of pad where object is drawn
         this.assignObject(obj);
         if (typeof opt == "string") this.options = { original: opt };
      }
   }

   ObjectPainter.prototype = Object.create(BasePainter.prototype);

   /** @summary Assign object to the painter
     * @protected */
   ObjectPainter.prototype.assignObject = function(obj) {
      if (obj && (typeof obj == 'object'))
         this.draw_object = obj;
      else
         delete this.draw_object;
   }

   /** @summary Assigns pad name where element will be drawn
     * @desc Should happend before first draw of element is performed, only for special use case
     * @param {string} [pad_name] - on which subpad element should be draw, if not specified - use current
     * @protected */
   ObjectPainter.prototype.setPadName = function(pad_name) {
      this.pad_name = (typeof pad_name == 'string') ? pad_name : this.selectCurrentPad();
   }

   /** @summary Returns pad name where object is drawn */
   ObjectPainter.prototype.getPadName = function() {
      return this.pad_name || "";
   }

   /** @summary Assign snapid to the painter
    * @desc Identifier used to communicate with server side and identifies object on the server
    * @private */
   ObjectPainter.prototype.assignSnapId = function(id) { this.snapid = id; }

   /** @summary Generic method to cleanup painter.
     * @desc Remove object drawing and (in case of main painter) also main HTML components
     * @protected */
   ObjectPainter.prototype.cleanup = function() {

      this.removeG();

      let keep_origin = true;

      if (this.isMainPainter()) {
         let pp = this.getPadPainter();
         if (!pp || pp.normal_canvas === false) keep_origin = false;
      }

      // cleanup all existing references
      delete this.pad_name;
      delete this._main_painter;
      this.draw_object = null;
      delete this.snapid;

      // remove attributes objects (if any)
      delete this.fillatt;
      delete this.lineatt;
      delete this.markeratt;
      delete this.bins;
      delete this.root_colors;
      delete this.options;
      delete this.options_store;

      // remove extra fields from v7 painters
      delete this.rstyle;
      delete this.csstype;

      BasePainter.prototype.cleanup.call(this, keep_origin);
   }

   /** @summary Returns drawn object */
   ObjectPainter.prototype.getObject = function() { return this.draw_object; }

   /** @summary Returns drawn object class name */
   ObjectPainter.prototype.getClassName = function() {
      let obj = this.getObject(),
          clname = obj ? obj._typename : "";
      return clname || "";
   }

   /** @summary Checks if drawn object matches with provided typename
     * @param {string|object} arg - typename (or object with _typename member)
     * @protected */
   ObjectPainter.prototype.matchObjectType = function(arg) {
      if (!arg || !this.draw_object) return false;
      if (typeof arg === 'string') return (this.draw_object._typename === arg);
      if (arg._typename) return (this.draw_object._typename === arg._typename);
      return this.draw_object._typename.match(arg);
   }

   /** @summary Change item name
     * @desc When available, used for svg:title proprty
     * @private */
   ObjectPainter.prototype.setItemName = function(name, opt, hpainter) {
      BasePainter.prototype.setItemName.call(this,name, opt, hpainter);
      if (this.no_default_title || (name == "")) return;
      let can = this.getCanvSvg();
      if (!can.empty()) can.select("title").text(name);
                   else this.selectDom().attr("title", name);
   }

   /** @summary Store actual this.options together with original string
     * @private */
   ObjectPainter.prototype.storeDrawOpt = function(original) {
      if (!this.options) return;
      if (!original) original = "";
      let pp = original.indexOf(";;");
      if (pp >= 0) original = original.substr(0, pp);
      this.options.original = original;
      this.options_store = JSROOT.extend({}, this.options);
   }

   /** @summary Return actual draw options as string
     * @desc if options are not modified - returns original string which was specified for object draw */
   ObjectPainter.prototype.getDrawOpt = function() {
      if (!this.options) return "";
      let changed = false;
      if (!this.options_store) {
         changed  = true;
      } else {
         for (let k in this.options)
            if (this.options[k] !== this.options_store[k])
               changed = true;
      }

      if (changed && typeof this.options.asString == "function")
         return this.options.asString();

      return this.options.original || ""; // nothing better, return original draw option
   }

   /** @summary Central place to update objects drawing
     * @param {object} obj - new version of object, values will be updated in original object
     * @param {string} [opt] - when specified, new draw options
     * @returns {boolean|Promise} for object redraw
     * @desc Two actions typically done by redraw - update object content via {@link JSROOT.ObjectPainter.updateObject} and
      * then redraw correspondent pad via {@link JSROOT.ObjectPainter.redrawPad}. If possible one should redefine
      * only updateObject function and keep this function unchanged. But for some special painters this function is the
      * only way to control how object can be update while requested from the server
      * @protected */
   ObjectPainter.prototype.redrawObject = function(obj, opt) {
      if (!this.updateObject(obj,opt)) return false;
      let current = document.body.style.cursor;
      document.body.style.cursor = 'wait';
      let res = this.redrawPad();
      document.body.style.cursor = current;
      return res || true;
   }

   /** @summary Generic method to update object content.
     * @desc Default implementation just copies first-level members to current object
     * @param {object} obj - object with new data
     * @param {string} [opt] - option which will be used for redrawing
     * @protected */
   ObjectPainter.prototype.updateObject = function(obj /*, opt */) {
      if (!this.matchObjectType(obj)) return false;
      JSROOT.extend(this.getObject(), obj);
      return true;
   }

   /** @summary Returns string with object hint
     * @desc It is either item name or object name or class name.
     * Such string typically used as object tooltip.
     * If result string larger than 20 symbols, it will be cutted. */
   ObjectPainter.prototype.getObjectHint = function() {
      let res = this.getItemName(), obj = this.getObject();
      if (!res) res = obj && obj.fName ? obj.fName : "";
      if (!res) res = this.getClassName();
      if (res.lenght > 20) res = res.substr(0, 17) + "...";
      return res;
   }

   /** @summary returns color from current list of colors
     * @desc First checks canvas painter and then just access global list of colors
     * @param {number} indx - color index
     * @returns {string} with SVG color name or rgb()
     * @protected */
   ObjectPainter.prototype.getColor = function(indx) {
      let jsarr = this.root_colors;

      if (!jsarr) {
         let pp = this.getCanvPainter();
         jsarr = this.root_colors = (pp && pp.root_colors) ? pp.root_colors : jsrp.root_colors;
      }

      return jsarr[indx];
   }

   /** @summary Add color to list of colors
     * @desc Returned color index can be used as color number in all other draw functions
     * @returns {number} new color index
     * @protected */
   ObjectPainter.prototype.addColor = function(color) {
      let jsarr = this.root_colors;
      if (!jsarr) {
         let pp = this.getCanvPainter();
         jsarr = this.root_colors = (pp && pp.root_colors) ? pp.root_colors : jsrp.root_colors;
      }
      let indx = jsarr.indexOf(color);
      if (indx >= 0) return indx;
      jsarr.push(color);
      return jsarr.length - 1;
   }

   /** @summary returns tooltip allowed flag
     * @desc If available, checks in canvas painter
     * @private */
   ObjectPainter.prototype.isTooltipAllowed = function() {
      let src = this.getCanvPainter() || this;
      return src.tooltip_allowed ? true : false;
   }

   /** @summary change tooltip allowed flag
     * @param {boolean|string} [on = true] set tooltip allowed state or 'toggle'
     * @private */
   ObjectPainter.prototype.setTooltipAllowed = function(on) {
      if (on === undefined) on = true;
      let src = this.getCanvPainter() || this;
      src.tooltip_allowed = (on == "toggle") ? !src.tooltip_allowed : on;
   }

   /** @summary Checks if draw elements were resized and drawing should be updated.
     * @desc Redirects to {@link JSROOT.TPadPainter.checkCanvasResize}
     * @private */
   ObjectPainter.prototype.checkResize = function(arg) {
      let p = this.getCanvPainter();
      if (!p) return false;

      // only canvas should be checked
      p.checkCanvasResize(arg);
      return true;
   }

   /** @summary removes <g> element with object drawing
     * @desc generic method to delete all graphical elements, associated with the painter
     * @protected */
   ObjectPainter.prototype.removeG = function() {
      if (this.draw_g) {
         this.draw_g.remove();
         delete this.draw_g;
      }
   }

   /** @summary Returns created <g> element used for object drawing
     * @desc Element should be created by {@link JSROOT.ObjectPainter.createG}
     * @protected */
   ObjectPainter.prototype.getG = function() { return this.draw_g; }

   /** @summary (re)creates svg:g element for object drawings
     * @desc either one attach svg:g to pad list of primitives (default)
     * or svg:g element created in specified frame layer (default main_layer)
     * @param {boolean} [frame_layer] - when specified, <g> element will be created inside frame, otherwise in the pad
     * @protected */
   ObjectPainter.prototype.createG = function(frame_layer) {
      if (this.draw_g) {
         // one should keep svg:g element on its place
         // d3.selectAll(this.draw_g.node().childNodes).remove();
         this.draw_g.selectAll('*').remove();
      } else if (frame_layer) {
         let frame = this.getFrameSvg();
         if (frame.empty()) return frame;
         if (typeof frame_layer != 'string') frame_layer = "main_layer";
         let layer = frame.select("." + frame_layer);
         if (layer.empty()) layer = frame.select(".main_layer");
         this.draw_g = layer.append("svg:g");
      } else {
         let layer = this.getLayerSvg("primitives_layer");
         this.draw_g = layer.append("svg:g");

         // layer.selectAll(".most_upper_primitives").raise();
         let up = [], chlds = layer.node().childNodes;
         for (let n = 0; n < chlds.length; ++n)
            if (d3.select(chlds[n]).classed("most_upper_primitives")) up.push(chlds[n]);

         up.forEach(top => { d3.select(top).raise(); });
      }

      // set attributes for debugging
      if (this.draw_object) {
         this.draw_g.attr('objname', encodeURI(this.draw_object.fName || "name"));
         this.draw_g.attr('objtype', encodeURI(this.draw_object._typename || "type"));
      }

      this.draw_g.property('in_frame', !!frame_layer); // indicates coordinate system

      return this.draw_g;
   }

   /** @summary Canvas main svg element
     * @returns {object} d3 selection with canvas svg
     * @protected */
   ObjectPainter.prototype.getCanvSvg = function() { return this.selectDom().select(".root_canvas"); }

   /** @summary Pad svg element
     * @param {string} [pad_name] - pad name to select, if not specified - pad where object is drawn
     * @returns {object} d3 selection with pad svg
     * @protected */
   ObjectPainter.prototype.getPadSvg = function(pad_name) {
      if (pad_name === undefined)
         pad_name = this.pad_name;

      let c = this.getCanvSvg();
      if (!pad_name || c.empty()) return c;

      let cp = c.property('pad_painter');
      if (cp && cp.pads_cache && cp.pads_cache[pad_name])
         return d3.select(cp.pads_cache[pad_name]);

      c = c.select(".primitives_layer .__root_pad_" + pad_name);
      if (cp) {
         if (!cp.pads_cache) cp.pads_cache = {};
         cp.pads_cache[pad_name] = c.node();
      }
      return c;
   }

   /** @summary Method selects immediate layer under canvas/pad main element
     * @param {string} name - layer name, exits "primitives_layer", "btns_layer", "info_layer"
     * @param {string} [pad_name] - pad name; current pad name  used by default
     * @protected */
   ObjectPainter.prototype.getLayerSvg = function(name, pad_name) {
      let svg = this.getPadSvg(pad_name);
      if (svg.empty()) return svg;

      if (name.indexOf("prim#") == 0) {
         svg = svg.select(".primitives_layer");
         name = name.substr(5);
      }

      let node = svg.node().firstChild;
      while (node) {
         let elem = d3.select(node);
         if (elem.classed(name)) return elem;
         node = node.nextSibling;
      }

      return d3.select(null);
   }

   /** @summary Method selects current pad name
     * @param {string} [new_name] - when specified, new current pad name will be configured
     * @returns {string} previous selected pad or actual pad when new_name not specified
     * @private */
   ObjectPainter.prototype.selectCurrentPad = function(new_name) {
      let svg = this.getCanvSvg();
      if (svg.empty()) return "";
      let curr = svg.property('current_pad');
      if (new_name !== undefined) svg.property('current_pad', new_name);
      return curr;
   }

   /** @summary returns pad painter
     * @param {string} [pad_name] pad name or use current pad by default
     * @protected */
   ObjectPainter.prototype.getPadPainter = function(pad_name) {
      let elem = this.getPadSvg(typeof pad_name == "string" ? pad_name : undefined);
      return elem.empty() ? null : elem.property('pad_painter');
   }

   /** @summary returns canvas painter
     * @protected */
   ObjectPainter.prototype.getCanvPainter = function() {
      let elem = this.getCanvSvg();
      return elem.empty() ? null : elem.property('pad_painter');
   }

   /** @summary Return functor, which can convert x and y coordinates into pixels, used for drawing in the pad
     * @desc X and Y coordinates can be converted by calling func.x(x) and func.y(y)
     * Only can be used for painting in the pad, means CreateG() should be called without arguments
     * @param {boolean} isndc - if NDC coordinates will be used
     * @param {boolean} [noround] - if set, return coordinates will not be rounded
     * @protected */
   ObjectPainter.prototype.getAxisToSvgFunc = function(isndc, nornd) {
      let func = { isndc: isndc, nornd: nornd },
          use_frame = this.draw_g && this.draw_g.property('in_frame');
      if (use_frame) func.main = this.getFramePainter();
      if (func.main && func.main.grx && func.main.gry) {
         if (nornd) {
            func.x = function(x) { return this.main.grx(x); }
            func.y = function(y) { return this.main.gry(y); }
         } else {
            func.x = function(x) { return Math.round(this.main.grx(x)); }
            func.y = function(y) { return Math.round(this.main.gry(y)); }
         }
      } else if (!use_frame) {
         let pp = this.getPadPainter();
         if (!isndc && pp) func.pad = pp.getRootPad(true); // need for NDC conversion
         func.padw = pp ? pp.getPadWidth() : 10;
         func.x = function(value) {
            if (this.pad) {
               if (this.pad.fLogx)
                  value = (value > 0) ? Math.log10(value) : this.pad.fUxmin;
               value = (value - this.pad.fX1) / (this.pad.fX2 - this.pad.fX1);
            }
            value *= this.padw;
            return this.nornd ? value : Math.round(value);
         }
         func.padh = pp ? pp.getPadHeight() : 10;
         func.y = function(value) {
            if (this.pad) {
               if (this.pad.fLogy)
                  value = (value > 0) ? Math.log10(value) : this.pad.fUymin;
               value = (value - this.pad.fY1) / (this.pad.fY2 - this.pad.fY1);
            }
            value = (1 - value) * this.padh;
            return this.nornd ? value : Math.round(value);
         }
      } else {
         console.error('Problem to create functor for', this.getClassName());
         func.x = () => 0;
         func.y = () => 0;

      }
      return func;
   }

   /** @summary Converts x or y coordinate into pad SVG coordinates.
     * @desc Only can be used for painting in the pad, means CreateG() should be called without arguments
     * @param {string} axis - name like "x" or "y"
     * @param {number} value - axis value to convert.
     * @param {boolean} ndc - is value in NDC coordinates
     * @param {boolean} [noround] - skip rounding
     * @returns {number} value of requested coordiantes
     * @protected */
   ObjectPainter.prototype.axisToSvg = function(axis, value, ndc, noround) {
      let func = this.getAxisToSvgFunc(ndc, noround);
      return func[axis](value);
   }

   /** @summary Converts pad SVG x or y coordinates into axis values.
     * @desc Reverse transformation for {@link JSROOT.ObjectPainter.axisToSvg}
     * @param {string} axis - name like "x" or "y"
     * @param {number} coord - graphics coordiante.
     * @param {boolean} ndc - kind of return value
     * @returns {number} value of requested coordiantes
     * @protected */
   ObjectPainter.prototype.svgToAxis = function(axis, coord, ndc) {
      let use_frame = this.draw_g && this.draw_g.property('in_frame');

      if (use_frame) {
         let main = this.getFramePainter();
         return main ? main.revertAxis(axis, coord) : 0;
      }

      let pp = this.getPadPainter(),
          value = (axis == "y") ? (1 - coord / pp.getPadHeight()) : coord / pp.getPadWidth(),
          pad = (ndc || !pp) ? null : pp.getRootPad(true);

      if (pad) {
         if (axis == "y") {
            value = pad.fY1 + value * (pad.fY2 - pad.fY1);
            if (pad.fLogy) value = Math.pow(10, value);
         } else {
            value = pad.fX1 + value * (pad.fX2 - pad.fX1);
            if (pad.fLogx) value = Math.pow(10, value);
         }
      }

      return value;
   }

   /** @summary Returns svg element for the frame in current pad
     * @protected */
   ObjectPainter.prototype.getFrameSvg = function() { return this.getLayerSvg("primitives_layer").select(".root_frame"); }

   /** @summary Returns frame painter for current pad
     * @desc Pad has direct reference on frame if any
     * @protected */
   ObjectPainter.prototype.getFramePainter = function() {
      let pp = this.getPadPainter();
      return pp ? pp.getFramePainter() : null;
   }

   /** @summary Returns painter for main object on the pad.
     * @desc Typically it is first histogram drawn on the pad and which draws frame axes
     * But it also can be special usecase as TASImage or TGraphPolargram
     * @param {boolean} [not_store] - if true, prevent temporary storage of main painter reference
     * @protected */
   ObjectPainter.prototype.getMainPainter = function(not_store) {
      let res = this._main_painter;
      if (!res) {
         let pp = this.getPadPainter();
         if (!pp)
            res = this.getTopPainter();
         else
            res = pp.getMainPainter();
         if (!res) res = null;
         if (!not_store) this._main_painter = res;
      }
      return res;
   }

   /** @summary Returns true if this is main painter
     * @protected */
   ObjectPainter.prototype.isMainPainter = function() { return this === this.getMainPainter(); }

   /** @summary Assign this as main painter on the pad
     * @desc Main painter typically responsible for axes drawing
     * Should not be used by pad/canvas painters, but rather by objects which are drawing axis
     * @protected */
   ObjectPainter.prototype.setAsMainPainter = function(force) {
      let pp = this.getPadPainter();
      if (!pp)
         this.setTopPainter(); //fallback on BasePainter method
       else
         pp.setMainPainter(this, force);
   }

   /** @summary Add painter to pad list of painters
     * @param {string} [pad_name] - optional pad name where painter should be add
     * @desc Normally one should use {@link JSROOT.Painter.ensureTCanvas} to add painter to pad list of primitives
     * @protected */
   ObjectPainter.prototype.addToPadPrimitives = function(pad_name) {
      if (pad_name !== undefined) this.setPadName(pad_name);
      let pp = this.getPadPainter(pad_name); // important - pad_name must be here, otherwise PadPainter class confuses itself

      if (!pp || (pp === this)) return false;

      if (pp.painters.indexOf(this) < 0)
         pp.painters.push(this);

      if (!this.rstyle && pp.next_rstyle)
         this.rstyle = pp.next_rstyle;

      return true;
   }

   /** @summary Remove painter from pad list of painters
     * @protected */
   ObjectPainter.prototype.removeFromPadPrimitives = function() {
      let pp = this.getPadPainter();

      if (!pp || (pp === this)) return false;

      let k = pp.painters.indexOf(this);
      if (k >= 0) pp.painters.splice(k, 1);
      return true;
   }


   /** @summary Creates marker attributes object
     * @desc Can be used to produce markers in painter.
     * See {@link JSROOT.TAttMarkerHandler} for more info.
     * Instance assigned as this.markeratt data member, recognized by GED editor
     * @param {object} args - either TAttMarker or see arguments of {@link JSROOT.TAttMarkerHandler}
     * @returns {object} created handler
     * @protected */
   ObjectPainter.prototype.createAttMarker = function(args) {
      if (!args || (typeof args !== 'object')) args = { std: true }; else
         if (args.fMarkerColor !== undefined && args.fMarkerStyle !== undefined && args.fMarkerSize !== undefined) args = { attr: args, std: false };

      if (args.std === undefined) args.std = true;
      if (args.painter === undefined) args.painter = this;

      let handler = args.std ? this.markeratt : null;

      if (!handler)
         handler = new TAttMarkerHandler(args);
      else if (!handler.changed || args.force)
         handler.setArgs(args);

      if (args.std) this.markeratt = handler;

      // handler.used = false; // mark that line handler is not yet used
      return handler;
   }

   /** @summary Creates line attributes object.
     * @desc Can be used to produce lines in painter.
     * See {@link JSROOT.TAttLineHandler} for more info.
     * Instance assigned as this.lineatt data member, recognized by GED editor
     * @param {object} args - either TAttLine or see constructor arguments of {@link JSROOT.TAttLineHandler}
     * @protected */
   ObjectPainter.prototype.createAttLine = function(args) {
      if (!args || (typeof args !== 'object'))
         args = { std: true };
      else if (args.fLineColor !== undefined && args.fLineStyle !== undefined && args.fLineWidth !== undefined)
         args = { attr: args, std: false };

      if (args.std === undefined) args.std = true;
      if (args.painter === undefined) args.painter = this;

      let handler = args.std ? this.lineatt : null;

      if (!handler)
         handler = new TAttLineHandler(args);
      else if (!handler.changed || args.force)
         handler.setArgs(args);

      if (args.std) this.lineatt = handler;

      // handler.used = false; // mark that line handler is not yet used
      return handler;
   }

   /** @summary Creates fill attributes object.
     * @desc Method dedicated to create fill attributes, bound to canvas SVG
     * otherwise newly created patters will not be usable in the canvas
     * See {@link JSROOT.TAttFillHandler} for more info.
     * Instance assigned as this.fillatt data member, recognized by GED editors
     * @param {object} args - for special cases one can specify TAttFill as args or number of parameters
     * @param {boolean} [args.std = true] - this is standard fill attribute for object and should be used as this.fillatt
     * @param {object} [args.attr = null] - object, derived from TAttFill
     * @param {number} [args.pattern = undefined] - integer index of fill pattern
     * @param {number} [args.color = undefined] - integer index of fill color
     * @param {string} [args.color_as_svg = undefined] - color will be specified as SVG string, not as index from color palette
     * @param {number} [args.kind = undefined] - some special kind which is handled differently from normal patterns
     * @returns created handle
     * @protected */
   ObjectPainter.prototype.createAttFill = function(args) {
      if (!args || (typeof args !== 'object')) args = { std: true }; else
         if (args._typename && args.fFillColor !== undefined && args.fFillStyle !== undefined) args = { attr: args, std: false };

      if (args.std === undefined) args.std = true;

      let handler = args.std ? this.fillatt : null;

      if (!args.svg) args.svg = this.getCanvSvg();
      if (args.painter === undefined) args.painter = this;

      if (!handler)
         handler = new TAttFillHandler(args);
      else if (!handler.changed || args.force)
         handler.setArgs(args);

      if (args.std) this.fillatt = handler;

      // handler.used = false; // mark that fill handler is not yet used

      return handler;
   }

   /** @summary call function for each painter in the pad
     * @desc Iterate over all known painters
     * @private */
   ObjectPainter.prototype.forEachPainter = function(userfunc, kind) {
      // iterate over all painters from pad list
      let pp = this.getPadPainter();
      if (pp) {
         pp.forEachPainterInPad(userfunc, kind);
      } else {
         let painter = this.getTopPainter();
         if (painter && (kind !== "pads")) userfunc(painter);
      }
   }

   /** @summary indicate that redraw was invoked via interactive action (like context menu or zooming)
     * @desc Use to catch such action by GED and by server-side
     * @private */
   ObjectPainter.prototype.interactiveRedraw = function(arg, info, subelem) {

      let reason;
      if ((typeof info == "string") && (info.indexOf("exec:") != 0)) reason = info;
      if (arg == "pad")
         this.redrawPad(reason);
      else if (arg !== false)
         this.redraw(reason);

      // inform GED that something changes
      let canp = this.getCanvPainter();

      if (canp && (typeof canp.producePadEvent == 'function'))
         canp.producePadEvent("redraw", this.getPadPainter(), this, null, subelem);

      // inform server that drawopt changes
      if (canp && (typeof canp.processChanges == 'function'))
         canp.processChanges(info, this, subelem);
   }

   /** @summary Redraw all objects in the current pad
     * @param {string} [reason] - like 'resize' or 'zoom'
     * @protected */
   ObjectPainter.prototype.redrawPad = function(reason) {
      let pp = this.getPadPainter();
      if (pp) pp.redraw(reason);
   }

   /** @summary execute selected menu command, either locally or remotely
     * @private */
   ObjectPainter.prototype.executeMenuCommand = function(method) {

      if (method.fName == "Inspect") {
         // primitve inspector, keep it here
         this.showInspector();
         return true;
      }

      return false;
   }

   /** @summary Invoke method for object via WebCanvas functionality
     * @desc Requires that painter marked with object identifier (this.snapid) or identifier provided as second argument
     * Canvas painter should exists and in non-readonly mode
     * Execution string can look like "Print()".
     * Many methods call can be chained with "Print();;Update();;Clear()"
     * @private */
   ObjectPainter.prototype.submitCanvExec = function(exec, snapid) {
      if (!exec || (typeof exec != 'string')) return;

      let canp = this.getCanvPainter();
      if (canp && (typeof canp.submitExec == "function"))
         canp.submitExec(this, exec, snapid);
   }

   /** @summary remove all created draw attributes
     * @protected */
   ObjectPainter.prototype.deleteAttr = function() {
      delete this.lineatt;
      delete this.fillatt;
      delete this.markeratt;
   }

   /** @summary Show object in inspector for provided object
     * @protected */
   ObjectPainter.prototype.showInspector = function(obj) {
      let main = this.selectDom(),
         rect = jsrp.getElementRect(main),
         w = Math.round(rect.width * 0.05) + "px",
         h = Math.round(rect.height * 0.05) + "px",
         id = "root_inspector_" + JSROOT._.id_counter++;

      main.append("div")
         .attr("id", id)
         .attr("class", "jsroot_inspector")
         .style('position', 'absolute')
         .style('top', h)
         .style('bottom', h)
         .style('left', w)
         .style('right', w);

      if (!obj || (typeof obj !== 'object') || !obj._typename)
         obj = this.getObject();

      JSROOT.draw(id, obj, 'inspect');
   }

   /** @summary Fill context menu for the object
     * @private */
   ObjectPainter.prototype.fillContextMenu = function(menu) {
      let title = this.getObjectHint();
      if (this.getObject() && ('_typename' in this.getObject()))
         title = this.getObject()._typename + "::" + title;

      menu.add("header:" + title);

      menu.addAttributesMenu(this);

      if (menu.size() > 0)
         menu.add('Inspect', this.showInspector);

      return menu.size() > 0;
   }

   /** @summary shows objects status
     * @desc Either used canvas painter method or globaly assigned
     * When no parameters are specified, just basic object properties are shown
     * @private */
   ObjectPainter.prototype.showObjectStatus = function(name, title, info, info2) {
      let cp = this.getCanvPainter();

      if (cp && (typeof cp.showCanvasStatus !== 'function')) cp = null;

      if (!cp && (typeof jsrp.showStatus !== 'function')) return false;

      if (this.enlargeMain('state') === 'on') return false;

      if ((name === undefined) && (title === undefined)) {
         let obj = this.getObject();
         if (!obj) return;
         name = this.getItemName() || obj.fName;
         title = obj.fTitle || obj._typename;
         info = obj._typename;
      }

      if (cp)
         cp.showCanvasStatus(name, title, info, info2);
      else
         jsrp.showStatus(name, title, info, info2);
   }

   /** @summary Redraw object
     * @desc Basic method, should be reimplemented in all derived objects
     * for the case when drawing should be repeated
     * @abstract
     * @protected */
   ObjectPainter.prototype.redraw = function(/* reason */) {}

   /** @summary Start text drawing
     * @desc required before any text can be drawn
     * @param {number} font_face - font id as used in ROOT font attributes
     * @param {number} font_size - font size as used in ROOT font attributes
     * @param {object} [draw_g] - element where text drawm, by default using main object <g> element
     * @param {number} [max_font_size] - maximal font size, used when text can be scaled
     * @protected */
   ObjectPainter.prototype.startTextDrawing = function(font_face, font_size, draw_g, max_font_size) {

      if (!draw_g) draw_g = this.draw_g;
      if (!draw_g || draw_g.empty()) return;

      let font = (font_size === 'font') ? font_face : new FontHandler(font_face, font_size),
          pp = this.getPadPainter();

      draw_g.call(font.func);

      draw_g.property('draw_text_completed', false) // indicate that draw operations submitted
            .property('all_args',[]) // array of all submitted args, makes easier to analyze them
            .property('text_font', font)
            .property('text_factor', 0.)
            .property('max_text_width', 0) // keep maximal text width, use it later
            .property('max_font_size', max_font_size)
            .property("_fast_drawing", pp ? pp._fast_drawing : false);

      if (draw_g.property("_fast_drawing"))
         draw_g.property("_font_too_small", (max_font_size && (max_font_size < 5)) || (font.size < 4));
   }

   /** @summary Apply scaling factor to all drawn text in the <g> element
     * @desc Can be applied at any time before finishTextDrawing is called - even in the postprocess callbacks of text draw
     * @param {number} factor - scaling factor
     * @param {object} [draw_g] - drawing element for the text
     * @protected */
   ObjectPainter.prototype.scaleTextDrawing = function(factor, draw_g) {
      if (!draw_g) draw_g = this.draw_g;
      if (!draw_g || draw_g.empty()) return;
      if (factor && (factor > draw_g.property('text_factor')))
         draw_g.property('text_factor', factor);
   }

   /** @summary Analyze if all text draw operations are completed
     * @private */
   function _checkAllTextDrawing(painter, draw_g, resolveFunc) {

      let all_args = draw_g.property('all_args'), missing = 0;
      if (!all_args) {
         console.log('Text drawing is finished - why?????');
         all_args = [];
      }

      all_args.forEach(arg => { if (!arg.ready) missing++; });

      if (missing > 0) {
         if (typeof resolveFunc == 'function')
            draw_g.node().textResolveFunc = resolveFunc;
         return;
      }

      draw_g.property('all_args', null); // clear all_args property

      // adjust font size (if there are normal text)
      let f = draw_g.property('text_factor'),
          font = draw_g.property('text_font'),
          max_sz = draw_g.property('max_font_size'),
          font_size = font.size, any_text = false;

      if ((f > 0) && ((f < 0.9) || (f > 1)))
         font.size = Math.floor(font.size / f);

      if (max_sz && (font.size > max_sz))
         font.size = max_sz;

      if (font.size != font_size) {
         draw_g.call(font.func);
         font_size = font.size;
      }

      all_args.forEach(arg => {
         if (arg.mj_node && arg.applyAttributesToMathJax) {
            let svg = arg.mj_node.select("svg"); // MathJax svg
            arg.applyAttributesToMathJax(painter, arg.mj_node, svg, arg, font_size, f);
            delete arg.mj_node; // remove reference
         }
      });

      // now hidden text after rescaling can be shown
      all_args.forEach(arg => {
         if (!arg.txt_node) return; // only normal text is processed
         any_text = true;
         let txt = arg.txt_node;
         delete arg.txt_node;
         txt.attr('visibility', null);

         if (JSROOT.nodejs) {
            if (arg.scale && (f > 0)) { arg.box.width = arg.box.width / f; arg.box.height = arg.box.height / f; }
         } else if (!arg.plain && !arg.fast) {
            // exact box dimension only required when complex text was build
            arg.box = jsrp.getElementRect(txt, 'bbox');
         }

         // if (arg.text.length>20) console.log(arg.box, arg.align, arg.x, arg.y, 'plain', arg.plain, 'inside', arg.width, arg.height);

         if (arg.width) {
            // adjust x position when scale into specified rectangle
            if (arg.align[0] == "middle") arg.x += arg.width / 2; else
               if (arg.align[0] == "end") arg.x += arg.width;
         }

         arg.dx = arg.dy = 0;

         if (arg.plain) {
            txt.attr("text-anchor", arg.align[0]);
         } else {
            txt.attr("text-anchor", "start");
            arg.dx = ((arg.align[0] == "middle") ? -0.5 : ((arg.align[0] == "end") ? -1 : 0)) * arg.box.width;
         }

         if (arg.height) {
            if (arg.align[1].indexOf('bottom') === 0) arg.y += arg.height; else
               if (arg.align[1] == 'middle') arg.y += arg.height / 2;
         }

         if (arg.plain) {
            if (arg.align[1] == 'top') txt.attr("dy", ".8em"); else
               if (arg.align[1] == 'middle') {
                  if (JSROOT.nodejs) txt.attr("dy", ".4em"); else txt.attr("dominant-baseline", "middle");
               }
         } else {
            arg.dy = ((arg.align[1] == 'top') ? (arg.top_shift || 1) : (arg.align[1] == 'middle') ? (arg.mid_shift || 0.5) : 0) * arg.box.height;
         }

         if (!arg.rotate) { arg.x += arg.dx; arg.y += arg.dy; arg.dx = arg.dy = 0; }

         // use translate and then rotate to avoid complex sign calculations
         let trans = (arg.x || arg.y) ? "translate(" + Math.round(arg.x) + "," + Math.round(arg.y) + ")" : "";
         if (arg.rotate) trans += " rotate(" + Math.round(arg.rotate) + ")";
         if (arg.dx || arg.dy) trans += " translate(" + Math.round(arg.dx) + "," + Math.round(arg.dy) + ")";
         if (trans) txt.attr("transform", trans);
      });

      // when no any normal text drawn - remove font attributes
      if (!any_text)
         font.clearFont(draw_g);

      if (!resolveFunc) resolveFunc = draw_g.node().textResolveFunc;
      draw_g.node().textResolveFunc = null;

      // if specified, call ready function
      if (resolveFunc) resolveFunc(painter); // IMPORTANT - return painter, may use in draw methods
   }

   /** @summary After normal SVG text is generated, check and recalculate some properties
     * @private */
   function _postprocessText(painter, txt_node, arg) {
      // complete rectangle with very rougth size estimations
      arg.box = !JSROOT.nodejs && !JSROOT.settings.ApproxTextSize && !arg.fast ? jsrp.getElementRect(txt_node, 'bbox') :
               (arg.text_rect || { height: arg.font_size * 1.2, width: arg.font.approxTextWidth(arg.text) });

      txt_node.attr('visibility', 'hidden'); // hide elements until text drawing is finished

      if (arg.box.width > arg.draw_g.property('max_text_width'))
         arg.draw_g.property('max_text_width', arg.box.width);
      if (arg.scale)
         painter.scaleTextDrawing(Math.max(1.05 * arg.box.width / arg.width, 1. * arg.box.height / arg.height), arg.draw_g);

      arg.result_width = arg.box.width;
      arg.result_height = arg.box.height;

      if (typeof arg.post_process == 'function')
         arg.post_process(painter);

      return arg.box.width;
   }

   /** @summary Draw text
     * @desc The only legal way to draw text, support plain, latex and math text output
     * @param {object} arg - different text draw options
     * @param {string} arg.text - text to draw
     * @param {number} [arg.align = 12] - int value like 12 or 31
     * @param {string} [arg.align = undefined] - end;bottom
     * @param {number} [arg.x = 0] - x position
     * @param {number} [arg.y = 0] - y position
     * @param {number} [arg.width] - when specified, adjust font size in the specified box
     * @param {number} [arg.height] - when specified, adjust font size in the specified box
     * @param {number} [arg.latex] - 0 - plain text, 1 - normal TLatex, 2 - math
     * @param {string} [arg.color=black] - text color
     * @param {number} [arg.rotate] - rotaion angle
     * @param {number} [arg.font_size] - fixed font size
     * @param {object} [arg.draw_g] - element where to place text, if not specified central draw_g container is used
     * @param {function} [arg.post_process] - optional function called when specified text is drawn
     * @protected */
   ObjectPainter.prototype.drawText = function(arg) {

      if (!arg.text) arg.text = "";

      arg.draw_g = arg.draw_g || this.draw_g;
      if (!arg.draw_g || arg.draw_g.empty()) return;

      let font = arg.draw_g.property('text_font');
      arg.font = font; // use in latex conversion

      if (font) {
         if (font.color && !arg.color) arg.color = font.color;
         if (font.align && !arg.align) arg.align = font.align;
         if (font.angle && !arg.rotate) arg.rotate = font.angle;
      }

      let align = ['start', 'middle'];

      if (typeof arg.align == 'string') {
         align = arg.align.split(";");
         if (align.length == 1) align.push('middle');
      } else if (typeof arg.align == 'number') {
         if ((arg.align / 10) >= 3)
            align[0] = 'end';
         else if ((arg.align / 10) >= 2)
            align[0] = 'middle';
         if ((arg.align % 10) == 0)
            align[1] = 'bottom';
         else if ((arg.align % 10) == 1)
            align[1] = 'bottom-base';
         else if ((arg.align % 10) == 3)
            align[1] = 'top';
      } else if (arg.align && (typeof arg.align == 'object') && arg.align.length == 2) {
         align = arg.align;
      }

      if (arg.latex === undefined) arg.latex = 1; //  latex 0-text, 1-latex, 2-math
      arg.align = align;
      arg.x = arg.x || 0;
      arg.y = arg.y || 0;
      arg.scale = arg.width && arg.height && !arg.font_size;
      arg.width = arg.width || 0;
      arg.height = arg.height || 0;

      if (arg.draw_g.property("_fast_drawing")) {
         if (arg.scale) {
            // area too small - ignore such drawing
            if (arg.height < 4) return 0;
         } else if (arg.font_size) {
            // font size too small
            if (arg.font_size < 4) return 0;
         } else if (arg.draw_g.property("_font_too_small")) {
            // configure font is too small - ignore drawing
            return 0;
         }
      }

      // include drawing into list of all args
      arg.draw_g.property('all_args').push(arg);
      arg.ready = false; // indicates if drawing is ready for post-processing

      let use_mathjax = (arg.latex == 2);

      if (arg.latex === 1)
         use_mathjax = (JSROOT.settings.Latex == JSROOT.constants.Latex.AlwaysMathJax) ||
                       ((JSROOT.settings.Latex == JSROOT.constants.Latex.MathJax) && arg.text.match(/[#{\\]/g));

      if (!use_mathjax || arg.nomathjax) {

         arg.txt_node = arg.draw_g.append("svg:text");

         if (arg.color) arg.txt_node.attr("fill", arg.color);

         if (arg.font_size) arg.txt_node.attr("font-size", arg.font_size);
                       else arg.font_size = font.size;

         arg.plain = !arg.latex || (JSROOT.settings.Latex == JSROOT.constants.Latex.Off) || (JSROOT.settings.Latex == JSROOT.constants.Latex.Symbols);

         arg.simple_latex = arg.latex && (JSROOT.settings.Latex == JSROOT.constants.Latex.Symbols);

         if (!arg.plain || arg.simple_latex) {
            JSROOT.require(['latex']).then(ltx => {
               if (arg.simple_latex)
                  ltx.producePlainText(this, arg.txt_node, arg);
               else
                  ltx.produceLatex(this, arg.txt_node, arg);
               arg.ready = true;
               _postprocessText(this, arg.txt_node, arg);

               if (arg.draw_g.property('draw_text_completed'))
                  _checkAllTextDrawing(this, arg.draw_g); // check if all other elements are completed
            });
            return 0;
         }

         arg.plain = true;
         arg.txt_node.text(arg.text);
         arg.ready = true;

         return _postprocessText(this, arg.txt_node, arg);
      }

      arg.mj_node = arg.draw_g.append("svg:g")
                           .attr('visibility', 'hidden'); // hide text until drawing is finished

      JSROOT.require(['latex'])
            .then(ltx => ltx.produceMathjax(this, arg.mj_node, arg))
            .then(() => {
               arg.ready = true;
               if (arg.draw_g.property('draw_text_completed'))
                  _checkAllTextDrawing(this, arg.draw_g);
            });

      return 0;
   }

   /** @summary Finish text drawing
     * @desc Should be called to complete all text drawing operations
     * @param {function} [draw_g] - <g> element for text drawing, this.draw_g used when not specified
     * @returns {Promise} when text drawing completed
     * @protected */
   ObjectPainter.prototype.finishTextDrawing = function(draw_g) {
      if (!draw_g) draw_g = this.draw_g;
      if (!draw_g || draw_g.empty())
         return Promise.resolve(false);

      draw_g.property('draw_text_completed', true); // mark that text drawing is completed

      return new Promise(resolveFunc => {
         _checkAllTextDrawing(this, draw_g, resolveFunc);
      });
   }

   /** @summary Configure user-defined context menu for the object
     * @desc fillmenu_func will be called when context menu is actiavted
     * Arguments fillmenu_func are (menu,kind)
     * First is JSROOT menu object, second is object subelement like axis "x" or "y"
     * Function should return promise with menu when items are filled
     * @param {function} fillmenu_func - function to fill custom context menu for oabject */
   ObjectPainter.prototype.configureUserContextMenu = function(fillmenu_func) {

      if (!fillmenu_func || (typeof fillmenu_func !== 'function'))
         delete this._userContextMenuFunc;
      else
         this._userContextMenuFunc = fillmenu_func;
   }

   /** @summary Fill object menu in web canvas
     * @private */
   ObjectPainter.prototype.fillObjectExecMenu = function(menu, kind) {

      if (this._userContextMenuFunc)
         return this._userContextMenuFunc(menu, kind);

      let canvp = this.getCanvPainter();

      if (!this.snapid || !canvp || canvp._readonly || !canvp._websocket)
         return Promise.resolve(menu);

      function DoExecMenu(arg) {
         let execp = this.exec_painter || this,
            cp = execp.getCanvPainter(),
            item = execp.args_menu_items[parseInt(arg)];

         if (!item || !item.fName) return;

         // this is special entry, produced by TWebMenuItem, which recognizes editor entries itself
         if (item.fExec == "Show:Editor") {
            if (cp && (typeof cp.activateGed == 'function'))
               cp.activateGed(execp);
            return;
         }

         if (cp && (typeof cp.executeObjectMethod == 'function'))
            if (cp.executeObjectMethod(execp, item, execp.args_menu_id)) return;

         if (execp.executeMenuCommand(item)) return;

         if (execp.args_menu_id)
            execp.submitCanvExec(item.fExec, execp.args_menu_id);
      }

      let DoFillMenu = (_menu, _reqid, _resolveFunc, reply) => {

         // avoid multiple call of the callback after timeout
         if (this._got_menu) return;
         this._got_menu = true;

         if (reply && (_reqid !== reply.fId))
            console.error('missmatch between request ' + _reqid + ' and reply ' + reply.fId + ' identifiers');

         let items = reply ? reply.fItems : null;

         if (items && items.length) {
            if (_menu.size() > 0)
               _menu.add("separator");

            this.args_menu_items = items;
            this.args_menu_id = reply.fId;

            let lastclname;

            for (let n = 0; n < items.length; ++n) {
               let item = items[n];

               if (item.fClassName && lastclname && (lastclname != item.fClassName)) {
                  _menu.add("endsub:");
                  lastclname = "";
               }
               if (lastclname != item.fClassName) {
                  lastclname = item.fClassName;
                  let p = lastclname.lastIndexOf("::"),
                      shortname = (p > 0) ? lastclname.substr(p+2) : lastclname;

                  _menu.add("sub:" + shortname.replace("<","_").replace(">","_"));
               }

               if ((item.fChecked === undefined) || (item.fChecked < 0))
                  _menu.add(item.fName, n, DoExecMenu);
               else
                  _menu.addchk(item.fChecked, item.fName, n, DoExecMenu);
            }

            if (lastclname) _menu.add("endsub:");
         }

         _resolveFunc(_menu);
      }

      let reqid = this.snapid;
      if (kind) reqid += "#" + kind; // use # to separate object id from member specifier like 'x' or 'z'

      this._got_menu = false;

      // if menu painter differs from this, remember it for further usage
      if (menu.painter)
         menu.painter.exec_painter = (menu.painter !== this) ? this : undefined;

      return new Promise(resolveFunc => {

         // set timeout to avoid menu hanging
         setTimeout(() => DoFillMenu(menu, reqid, resolveFunc), 2000);

         canvp.submitMenuRequest(this, kind, reqid).then(lst => DoFillMenu(menu, reqid, resolveFunc, lst));
      });
   }

   /** @summary Configure user-defined tooltip handler
     * @desc Hook for the users to get tooltip information when mouse cursor moves over frame area
     * Hanlder function will be called every time when new data is selected
     * when mouse leave frame area, handler(null) will be called
     * @param {function} handler - function called when tooltip is produced
     * @param {number} [tmout = 100] - delay in ms before tooltip delivered */
   ObjectPainter.prototype.configureUserTooltipHandler = function(handler, tmout) {
      if (!handler || (typeof handler !== 'function')) {
         delete this._user_tooltip_handler;
         delete this._user_tooltip_timeout;
      } else {
         this._user_tooltip_handler = handler;
         this._user_tooltip_timeout = tmout || 100;
      }
   }

    /** @summary Configure user-defined click handler
      * @desc Function will be called every time when frame click was perfromed
      * As argument, tooltip object with selected bins will be provided
      * If handler function returns true, default handling of click will be disabled
      * @param {function} handler - function called when mouse click is done */
   ObjectPainter.prototype.configureUserClickHandler = function(handler) {
      let fp = this.getFramePainter();
      if (fp && fp.configureUserClickHandler)
         fp.configureUserClickHandler(handler);
   }

   /** @summary Configure user-defined dblclick handler
     * @desc Function will be called every time when double click was called
     * As argument, tooltip object with selected bins will be provided
     * If handler function returns true, default handling of dblclick (unzoom) will be disabled
     * @param {function} handler - function called when mouse double click is done */
   ObjectPainter.prototype.configureUserDblclickHandler = function(handler) {
      let fp = this.getFramePainter();
      if (fp && fp.configureUserDblclickHandler)
         fp.configureUserDblclickHandler(handler);
   }


   /** @summary Check if user-defined tooltip function was configured
     * @returns {boolean} flag is user tooltip handler was configured */
   ObjectPainter.prototype.hasUserTooltip = function() {
      return typeof this._user_tooltip_handler == 'function';
   }

   /** @summary Provide tooltips data to user-defined function
     * @param {object} data - tooltip data
     * @private */
   ObjectPainter.prototype.provideUserTooltip = function(data) {

      if (!this.hasUserTooltip()) return;

      if (this._user_tooltip_timeout <= 0)
         return this._user_tooltip_handler(data);

      if (this._user_tooltip_handle) {
         clearTimeout(this._user_tooltip_handle);
         delete this._user_tooltip_handle;
      }

      if (!data)
         return this._user_tooltip_handler(data);

      let d = data;

      // only after timeout user function will be called
      this._user_tooltip_handle = setTimeout(() => {
         delete this._user_tooltip_handle;
         if (this._user_tooltip_handler) this._user_tooltip_handler(d);
      }, this._user_tooltip_timeout);
   }

   // ===========================================================


   /**
     * @summary Base painter for axis objects in v6/v7
     *
     * @class
     * @memberof JSROOT
     * @param {object|string} dom - DOM element for drawing or element id
     * @param {object} obj - axis object if any
     * @private
     */

   function AxisBasePainter(dom, obj) {
      ObjectPainter.call(this, dom, obj);

      this.name = "yaxis";
      this.kind = "normal";
      this.func = null;
      this.order = 0; // scaling order for axis labels

      this.full_min = 0;
      this.full_max = 1;
      this.scale_min = 0;
      this.scale_max = 1;
      this.ticks = []; // list of major ticks
   }

   AxisBasePainter.prototype = Object.create(ObjectPainter.prototype);

   AxisBasePainter.prototype.cleanup = function() {
      this.ticks = [];
      delete this.format;
      delete this.func;
      delete this.tfunc1;
      delete this.tfunc2;
      delete this.gr;

      // cleanup of v7 members
      delete this.axis;
      delete this.axis_g;

      ObjectPainter.prototype.cleanup.call(this);
   }

   /** @summary Assign often used members of frame painter
     * @private */
   AxisBasePainter.prototype.assignFrameMembers = function(fp, axis) {
      fp["gr"+axis] = this.gr;                    // fp.grx
      fp["log"+axis] = this.log;                  // fp.logx
      fp["scale_"+axis+"min"] = this.scale_min;   // fp.scale_xmin
      fp["scale_"+axis+"max"] = this.scale_max;   // fp.scale_xmax
   }

   /** @summary Convert axis value into the Date object */
   AxisBasePainter.prototype.convertDate = function(v) {
      return new Date(this.timeoffset + v*1000);
   }

   /** @summary Convert graphical point back into axis value */
   AxisBasePainter.prototype.revertPoint = function(pnt) {
      let value = this.func.invert(pnt);
      return (this.kind == "time") ?  (value - this.timeoffset) / 1000 : value;
   }

   /** @summary Provide label for time axis */
   AxisBasePainter.prototype.formatTime = function(d, asticks) {
      return asticks ? this.tfunc1(d) : this.tfunc2(d);
   }

   /** @summary Provide label for log axis */
   AxisBasePainter.prototype.formatLog = function(d, asticks, fmt) {
      let val = parseFloat(d), rnd = Math.round(val);
      if (!asticks)
         return ((rnd === val) && (Math.abs(rnd)<1e9)) ? rnd.toString() : jsrp.floatToString(val, fmt || JSROOT.gStyle.fStatFormat);
      if (val <= 0) return null;
      let vlog = Math.log10(val), base = this.logbase;
      if (base !== 10) vlog = vlog / Math.log10(base);
      if (this.moreloglabels || (Math.abs(vlog - Math.round(vlog))<0.001)) {
         if (!this.noexp && (asticks != 2))
            return this.formatExp(base, Math.floor(vlog+0.01), val);

         return (vlog<0) ? val.toFixed(Math.round(-vlog+0.5)) : val.toFixed(0);
      }
      return null;
   }

   /** @summary Provide label for normal axis */
   AxisBasePainter.prototype.formatNormal = function(d, asticks, fmt) {
      let val = parseFloat(d);
      if (asticks && this.order) val = val / Math.pow(10, this.order);

      if (val === Math.round(val))
         return (Math.abs(val)<1e9) ? val.toFixed(0) : val.toExponential(4);

      if (asticks) return (this.ndig>10) ? val.toExponential(this.ndig-11) : val.toFixed(this.ndig);

      return jsrp.floatToString(val, fmt || JSROOT.gStyle.fStatFormat);
   }

   /** @summary Provide label for exponential form */
   AxisBasePainter.prototype.formatExp = function(base, order, value) {
      let res = "";
      if (value) {
         value = Math.round(value/Math.pow(base,order));
         if ((value!=0) && (value!=1)) res = value.toString() + (JSROOT.settings.Latex ? "#times" : "x");
      }
      if (Math.abs(base-Math.exp(1)) < 0.001)
         res += "e";
      else
         res += base.toString();
      if (JSROOT.settings.Latex > JSROOT.constants.Latex.Symbols)
         return res + "^{" + order + "}";
      const superscript_symbols = {
            '0': '\u2070', '1': '\xB9', '2': '\xB2', '3': '\xB3', '4': '\u2074', '5': '\u2075',
            '6': '\u2076', '7': '\u2077', '8': '\u2078', '9': '\u2079', '-': '\u207B'
         };
      let str = order.toString();
      for (let n = 0; n < str.length; ++n)
         res += superscript_symbols[str[n]];
      return res;
   }

   /** @summary Convert "raw" axis value into text */
   AxisBasePainter.prototype.axisAsText = function(value, fmt) {
      if (this.kind == 'time')
         value = this.convertDate(value);
      if (this.format)
         return this.format(value, false, fmt);
      return value.toPrecision(4);
   }

   /** @summary Produce ticks for d3.scaleLog
     * @desc Fixing following problem, described [here]{@link https://stackoverflow.com/questions/64649793} */
   AxisBasePainter.prototype.poduceLogTicks = function(func, number) {
      function linearArray(arr) {
         let sum1 = 0, sum2 = 0;
         for (let k=1;k<arr.length;++k) {
            let diff = (arr[k] - arr[k-1]);
            sum1 += diff;
            sum2 += diff*diff;
         }
         let mean = sum1/(arr.length-1);
         let dev = sum2/(arr.length-1) - mean*mean;

         if (dev <= 0) return true;
         if (Math.abs(mean) < 1e-100) return false;
         return Math.sqrt(dev)/mean < 1e-6;
      }

      let arr = func.ticks(number);

      while ((number > 4) && linearArray(arr)) {
         number = Math.round(number*0.8);
         arr = func.ticks(number);
      }

      // if still linear array, try to sort out "bad" ticks
      if ((number < 5) && linearArray(arr) && this.logbase && (this.logbase != 10)) {
         let arr2 = [];
         arr.forEach(val => {
            let pow = Math.log10(val) / Math.log10(this.logbase);
            if (Math.abs(Math.round(pow) - pow) < 0.01) arr2.push(val);
         });
         if (arr2.length > 0) arr = arr2;
      }

      return arr;
   }

   /** @summary Produce axis ticks */
   AxisBasePainter.prototype.produceTicks = function(ndiv, ndiv2) {
      if (!this.noticksopt) {
         let total = ndiv * (ndiv2 || 1);
         return this.log ? this.poduceLogTicks(this.func, total) : this.func.ticks(total);
      }

      let dom = this.func.domain(), ticks = [];
      if (ndiv2) ndiv = (ndiv-1) * ndiv2;
      for (let n=0;n<=ndiv;++n)
         ticks.push((dom[0]*(ndiv-n) + dom[1]*n)/ndiv);
      return ticks;
   }

   /** @summary Method analyze mouse wheel event and returns item with suggested zooming range */
   AxisBasePainter.prototype.analyzeWheelEvent = function(evnt, dmin, item, test_ignore) {
      if (!item) item = {};

      let delta = 0, delta_left = 1, delta_right = 1;

      if ('dleft' in item) { delta_left = item.dleft; delta = 1; }
      if ('dright' in item) { delta_right = item.dright; delta = 1; }

      if (item.delta) {
         delta = item.delta;
      } else if (evnt) {
         delta = evnt.wheelDelta ? -evnt.wheelDelta : (evnt.deltaY || evnt.detail);
      }

      if (!delta || (test_ignore && item.ignore)) return;

      delta = (delta < 0) ? -0.2 : 0.2;
      delta_left *= delta
      delta_right *= delta;

      let lmin = item.min = this.scale_min,
          lmax = item.max = this.scale_max,
          gmin = this.full_min,
          gmax = this.full_max;

      if ((item.min === item.max) && (delta < 0)) {
         item.min = gmin;
         item.max = gmax;
      }

      if (item.min >= item.max) return;

      if (item.reverse) dmin = 1 - dmin;

      if ((dmin > 0) && (dmin < 1)) {
         if (this.log) {
            let factor = (item.min>0) ? Math.log10(item.max/item.min) : 2;
            if (factor>10) factor = 10; else if (factor<0.01) factor = 0.01;
            item.min = item.min / Math.pow(10, factor*delta_left*dmin);
            item.max = item.max * Math.pow(10, factor*delta_right*(1-dmin));
         } else {
            let rx_left = (item.max - item.min), rx_right = rx_left;
            if (delta_left>0) rx_left = 1.001 * rx_left / (1-delta_left);
            item.min += -delta_left*dmin*rx_left;
            if (delta_right>0) rx_right = 1.001 * rx_right / (1-delta_right);
            item.max -= -delta_right*(1-dmin)*rx_right;
         }
         if (item.min >= item.max) {
            item.min = item.max = undefined;
         } else if (delta_left !== delta_right) {
            // extra check case when moving left or right
            if (((item.min < gmin) && (lmin === gmin)) ||
                ((item.max > gmax) && (lmax === gmax)))
                   item.min = item.max = undefined;
         } else {
            if (item.min < gmin) item.min = gmin;
            if (item.max > gmax) item.max = gmax;
         }
      } else {
         item.min = item.max = undefined;
      }

      item.changed = ((item.min !== undefined) && (item.max !== undefined));

      return item;
   }

   // ===========================================================

   /** @summary Set active pad painter
     * @desc Normally be used to handle key press events, which are global in the web browser
     * @param {object} args - functions arguments
     * @param {object} args.pp - pad painter
     * @param {boolean} [args.active] - is pad activated or not
     * @private */
   jsrp.selectActivePad = function(args) {
      if (args.active) {
         let fp = this.$active_pp ? this.$active_pp.getFramePainter() : null;
         if (fp) fp.setFrameActive(false);

         this.$active_pp = args.pp;

         fp = this.$active_pp ? this.$active_pp.getFramePainter() : null;
         if (fp) fp.setFrameActive(true);
      } else if (this.$active_pp === args.pp) {
         delete this.$active_pp;
      }
   }

   /** @summary Returns current active pad
     * @desc Should be used only for keyboard handling
     * @private */
   jsrp.getActivePad = function() {
      return this.$active_pp;
   }

   // ================= painter of raw text ========================================

   /** @summary Generic text drawing
     * @private */
   jsrp.drawRawText = function(dom, txt /*, opt*/) {

      let painter = new BasePainter(dom);
      painter.txt = txt;

      painter.redrawObject = function(obj) {
         this.txt = obj;
         this.Draw();
         return true;
      }

      painter.Draw = function() {
         let txt = (this.txt._typename && (this.txt._typename == "TObjString")) ? this.txt.fString : this.txt.value;
         if (typeof txt != 'string') txt = "<undefined>";

         let mathjax = this.txt.mathjax || (JSROOT.settings.Latex == JSROOT.constants.Latex.AlwaysMathJax);

         if (!mathjax && !('as_is' in this.txt)) {
            let arr = txt.split("\n"); txt = "";
            for (let i = 0; i < arr.length; ++i)
               txt += "<pre style='margin:0'>" + arr[i] + "</pre>";
         }

         let frame = this.selectDom(),
            main = frame.select("div");
         if (main.empty())
            main = frame.append("div").style('max-width', '100%').style('max-height', '100%').style('overflow', 'auto');
         main.html(txt);

         // (re) set painter to first child element, base painter not requires canvas
         this.setTopPainter();

         if (mathjax)
            return JSROOT.require('latex').then(ltx => { ltx.typesetMathjax(frame.node()); return this; });

         return Promise.resolve(this);
      }

      return painter.Draw();
   }

   /** @summary Register handle to react on window resize
     * @desc function used to react on browser window resize event
     * While many resize events could come in short time,
     * resize will be handled with delay after last resize event
     * @param {object|string} handle can be function or object with checkResize function or dom where painting was done
     * @param {number} [delay] - one could specify delay after which resize event will be handled
     * @protected */
   jsrp.registerForResize = function(handle, delay) {

      if (!handle || JSROOT.batch_mode || (typeof window == 'undefined')) return;

      let myInterval = null, myDelay = delay ? delay : 300;

      if (myDelay < 20) myDelay = 20;

      function ResizeTimer() {
         myInterval = null;

         document.body.style.cursor = 'wait';
         if (typeof handle == 'function')
            handle();
         else if (handle && (typeof handle == 'object') && (typeof handle.checkResize == 'function')) {
            handle.checkResize();
         } else {
            let dummy = new BasePainter(handle);
            let node = dummy.selectDom();
            if (!node.empty()) {
               let mdi = node.property('mdi');
               if (mdi && typeof mdi.checkMDIResize == 'function') {
                  mdi.checkMDIResize();
               } else {
                  JSROOT.resize(node.node());
               }
            }
         }
         document.body.style.cursor = 'auto';
      }

      function ProcessResize() {
         if (myInterval !== null) clearTimeout(myInterval);
         myInterval = setTimeout(ResizeTimer, myDelay);
      }

      window.addEventListener('resize', ProcessResize);
   }

   // list of registered draw functions
   let drawFuncs = { lst: [
      { name: "TCanvas", icon: "img_canvas", prereq: "gpad", func: ".drawCanvas", opt: ";grid;gridx;gridy;tick;tickx;ticky;log;logx;logy;logz", expand_item: "fPrimitives" },
      { name: "TPad", icon: "img_canvas", prereq: "gpad", func: ".drawPad", opt: ";grid;gridx;gridy;tick;tickx;ticky;log;logx;logy;logz", expand_item: "fPrimitives" },
      { name: "TSlider", icon: "img_canvas", prereq: "gpad", func: ".drawPad" },
      { name: "TFrame", icon: "img_frame", prereq: "gpad", func: ".drawFrame" },
      { name: "TPave", icon: "img_pavetext", prereq: "hist", func: ".drawPave" },
      { name: "TPaveText", icon: "img_pavetext", prereq: "hist", func: ".drawPave" },
      { name: "TPavesText", icon: "img_pavetext", prereq: "hist", func: ".drawPave" },
      { name: "TPaveStats", icon: "img_pavetext", prereq: "hist", func: ".drawPave" },
      { name: "TPaveLabel", icon: "img_pavelabel", prereq: "hist", func: ".drawPave" },
      { name: "TDiamond", icon: "img_pavelabel", prereq: "hist", func: ".drawPave" },
      { name: "TLatex", icon: "img_text", prereq: "more", func: ".drawText", direct: true },
      { name: "TMathText", icon: "img_text", prereq: "more", func: ".drawText", direct: true },
      { name: "TText", icon: "img_text", prereq: "more", func: ".drawText", direct: true },
      { name: /^TH1/, icon: "img_histo1d", prereq: "hist", func: ".drawHistogram1D", opt: ";hist;P;P0;E;E1;E2;E3;E4;E1X0;L;LF2;B;B1;A;TEXT;LEGO;same", ctrl: "l" },
      { name: "TProfile", icon: "img_profile", prereq: "hist", func: ".drawHistogram1D", opt: ";E0;E1;E2;p;AH;hist" },
      { name: "TH2Poly", icon: "img_histo2d", prereq: "hist", func: ".drawHistogram2D", opt: ";COL;COL0;COLZ;LCOL;LCOL0;LCOLZ;LEGO;TEXT;same", expand_item: "fBins", theonly: true },
      { name: "TProfile2Poly", sameas: "TH2Poly" },
      { name: "TH2PolyBin", icon: "img_histo2d", draw_field: "fPoly" },
      { name: /^TH2/, icon: "img_histo2d", prereq: "hist", func: ".drawHistogram2D", opt: ";COL;COLZ;COL0;COL1;COL0Z;COL1Z;COLA;BOX;BOX1;PROJ;PROJX1;PROJX2;PROJX3;PROJY1;PROJY2;PROJY3;SCAT;TEXT;TEXTE;TEXTE0;CONT;CONT1;CONT2;CONT3;CONT4;ARR;SURF;SURF1;SURF2;SURF4;SURF6;E;A;LEGO;LEGO0;LEGO1;LEGO2;LEGO3;LEGO4;same", ctrl: "colz" },
      { name: "TProfile2D", sameas: "TH2" },
      { name: /^TH3/, icon: 'img_histo3d', prereq: "hist3d", func: ".drawHistogram3D", opt: ";SCAT;BOX;BOX2;BOX3;GLBOX1;GLBOX2;GLCOL" },
      { name: "THStack", icon: "img_histo1d", prereq: "hist", func: ".drawHStack", expand_item: "fHists", opt: "NOSTACK;HIST;E;PFC;PLC" },
      { name: "TPolyMarker3D", icon: 'img_histo3d', prereq: "hist3d", func: ".drawPolyMarker3D", direct: true, frame: "3d" },
      { name: "TPolyLine3D", icon: 'img_graph', prereq: "base3d", func: ".drawPolyLine3D", direct: true },
      { name: "TGraphStruct" },
      { name: "TGraphNode" },
      { name: "TGraphEdge" },
      { name: "TGraphTime", icon: "img_graph", prereq: "more", func: ".drawGraphTime", opt: "once;repeat;first", theonly: true },
      { name: "TGraph2D", icon: "img_graph", prereq: "hist3d", func: ".drawGraph2D", opt: ";P;PCOL", theonly: true },
      { name: "TGraph2DErrors", icon: "img_graph", prereq: "hist3d", func: ".drawGraph2D", opt: ";P;PCOL;ERR", theonly: true },
      { name: "TGraphPolargram", icon: "img_graph", prereq: "more", func: ".drawGraphPolargram", theonly: true },
      { name: "TGraphPolar", icon: "img_graph", prereq: "more", func: ".drawGraphPolar", opt: ";F;L;P;PE", theonly: true },
      { name: /^TGraph/, icon: "img_graph", prereq: "more", func: ".drawGraph", opt: ";L;P" },
      { name: "TEfficiency", icon: "img_graph", prereq: "more", func: ".drawEfficiency", opt: ";AP" },
      { name: "TCutG", sameas: "TGraph" },
      { name: /^RooHist/, sameas: "TGraph" },
      { name: /^RooCurve/, sameas: "TGraph" },
      { name: "RooPlot", icon: "img_canvas", prereq: "more", func: ".drawRooPlot" },
      { name: "TMultiGraph", icon: "img_mgraph", prereq: "more", func: ".drawMultiGraph", expand_item: "fGraphs" },
      { name: "TStreamerInfoList", icon: 'img_question', prereq: "hierarchy", func: ".drawStreamerInfo" },
      { name: "TPaletteAxis", icon: "img_colz", prereq: "hist", func: ".drawPave" },
      { name: "TWebPainting", icon: "img_graph", prereq: "more", func: ".drawWebPainting" },
      { name: "TCanvasWebSnapshot", icon: "img_canvas", prereq: "gpad", func: ".drawPadSnapshot" },
      { name: "TPadWebSnapshot", sameas: "TCanvasWebSnapshot" },
      { name: "kind:Text", icon: "img_text", func: jsrp.drawRawText },
      { name: "TObjString", icon: "img_text", func: jsrp.drawRawText },
      { name: "TF1", icon: "img_tf1", prereq: "math;more", func: ".drawFunction" },
      { name: "TF2", icon: "img_tf2", prereq: "math;hist", func: ".drawTF2" },
      { name: "TSpline3", icon: "img_tf1", prereq: "more", func: ".drawSpline" },
      { name: "TSpline5", icon: "img_tf1", prereq: "more", func: ".drawSpline" },
      { name: "TEllipse", icon: 'img_graph', prereq: "more", func: ".drawEllipse", direct: true },
      { name: "TArc", sameas: 'TEllipse' },
      { name: "TCrown", sameas: 'TEllipse' },
      { name: "TPie", icon: 'img_graph', prereq: "more", func: ".drawPie", direct: true },
      { name: "TPieSlice", icon: 'img_graph', dummy: true },
      { name: "TLine", icon: 'img_graph', prereq: "more", func: ".drawLine", direct: true },
      { name: "TArrow", icon: 'img_graph', prereq: "more", func: ".drawArrow", direct: true },
      { name: "TPolyLine", icon: 'img_graph', prereq: "more", func: ".drawPolyLine", direct: true },
      { name: "TCurlyLine", sameas: 'TPolyLine' },
      { name: "TCurlyArc", sameas: 'TPolyLine' },
      { name: "TGaxis", icon: "img_graph", prereq: "gpad", func: ".drawGaxis" },
      { name: "TLegend", icon: "img_pavelabel", prereq: "hist", func: ".drawPave" },
      { name: "TBox", icon: 'img_graph', prereq: "more", func: ".drawBox", direct: true },
      { name: "TWbox", icon: 'img_graph', prereq: "more", func: ".drawBox", direct: true },
      { name: "TSliderBox", icon: 'img_graph', prereq: "more", func: ".drawBox", direct: true },
      { name: "TAxis3D", prereq: "hist3d", func: ".drawAxis3D" },
      { name: "TMarker", icon: 'img_graph', prereq: "more", func: ".drawMarker", direct: true },
      { name: "TPolyMarker", icon: 'img_graph', prereq: "more", func: ".drawPolyMarker", direct: true },
      { name: "TASImage", icon: 'img_mgraph', prereq: "more", func: ".drawASImage", opt: ";z" },
      { name: "TJSImage", icon: 'img_mgraph', prereq: "more", func: ".drawJSImage", opt: ";scale;center" },
      { name: "TGeoVolume", icon: 'img_histo3d', prereq: "geom", func: ".drawGeoObject", expand: "JSROOT.GEO.expandObject", opt: ";more;all;count;projx;projz;wire;dflt", ctrl: "dflt" },
      { name: "TEveGeoShapeExtract", icon: 'img_histo3d', prereq: "geom", func: ".drawGeoObject", expand: "JSROOT.GEO.expandObject", opt: ";more;all;count;projx;projz;wire;dflt", ctrl: "dflt" },
      { name: "ROOT::Experimental::REveGeoShapeExtract", icon: 'img_histo3d', prereq: "geom", func: ".drawGeoObject", expand: "JSROOT.GEO.expandObject", opt: ";more;all;count;projx;projz;wire;dflt", ctrl: "dflt" },
      { name: "TGeoOverlap", icon: 'img_histo3d', prereq: "geom", expand: "JSROOT.GEO.expandObject", func: ".drawGeoObject", opt: ";more;all;count;projx;projz;wire;dflt", dflt: "dflt", ctrl: "expand" },
      { name: "TGeoManager", icon: 'img_histo3d', prereq: "geom", expand: "JSROOT.GEO.expandObject", func: ".drawGeoObject", opt: ";more;all;count;projx;projz;wire;tracks;dflt", dflt: "expand", ctrl: "dflt" },
      { name: /^TGeo/, icon: 'img_histo3d', prereq: "geom", func: ".drawGeoObject", opt: ";more;all;axis;compa;count;projx;projz;wire;dflt", ctrl: "dflt" },
      // these are not draw functions, but provide extra info about correspondent classes
      { name: "kind:Command", icon: "img_execute", execute: true },
      { name: "TFolder", icon: "img_folder", icon2: "img_folderopen", noinspect: true, prereq: "hierarchy", expand: ".folderHierarchy" },
      { name: "TTask", icon: "img_task", prereq: "hierarchy", expand: ".taskHierarchy", for_derived: true },
      { name: "TTree", icon: "img_tree", prereq: "tree", expand: 'JSROOT.treeHierarchy', func: 'JSROOT.drawTree', dflt: "expand", opt: "player;testio", shift: "inspect" },
      { name: "TNtuple", icon: "img_tree", prereq: "tree", expand: 'JSROOT.treeHierarchy', func: 'JSROOT.drawTree', dflt: "expand", opt: "player;testio", shift: "inspect" },
      { name: "TNtupleD", icon: "img_tree", prereq: "tree", expand: 'JSROOT.treeHierarchy', func: 'JSROOT.drawTree', dflt: "expand", opt: "player;testio", shift: "inspect" },
      { name: "TBranchFunc", icon: "img_leaf_method", prereq: "tree", func: 'JSROOT.drawTree', opt: ";dump", noinspect: true },
      { name: /^TBranch/, icon: "img_branch", prereq: "tree", func: 'JSROOT.drawTree', dflt: "expand", opt: ";dump", ctrl: "dump", shift: "inspect", ignore_online: true },
      { name: /^TLeaf/, icon: "img_leaf", prereq: "tree", noexpand: true, func: 'JSROOT.drawTree', opt: ";dump", ctrl: "dump", ignore_online: true },
      { name: "TList", icon: "img_list", prereq: "hierarchy", func: ".drawList", expand: ".listHierarchy", dflt: "expand" },
      { name: "THashList", sameas: "TList" },
      { name: "TObjArray", sameas: "TList" },
      { name: "TClonesArray", sameas: "TList" },
      { name: "TMap", sameas: "TList" },
      { name: "TColor", icon: "img_color" },
      { name: "TFile", icon: "img_file", noinspect: true },
      { name: "TMemFile", icon: "img_file", noinspect: true },
      { name: "TStyle", icon: "img_question", noexpand: true },
      { name: "Session", icon: "img_globe" },
      { name: "kind:TopFolder", icon: "img_base" },
      { name: "kind:Folder", icon: "img_folder", icon2: "img_folderopen", noinspect: true },
      { name: "ROOT::Experimental::RCanvas", icon: "img_canvas", prereq: "v7gpad", func: "JSROOT.v7.drawRCanvas", opt: "", expand_item: "fPrimitives" },
      { name: "ROOT::Experimental::RCanvasDisplayItem", icon: "img_canvas", prereq: "v7gpad", func: "JSROOT.v7.drawPadSnapshot", opt: "", expand_item: "fPrimitives" }
   ], cache: {} };


   /** @summary Register draw function for the class
     * @desc List of supported draw options could be provided, separated  with ';'
     * @param {object} args - arguments
     * @param {string|regexp} args.name - class name or regexp pattern
     * @param {string} [args.prereq] - prerequicities to load before search for the draw function
     * @param {string} args.func - draw function name or just a function
     * @param {boolean} [args.direct] - if true, function is just Redraw() method of ObjectPainter
     * @param {string} [args.opt] - list of supported draw options (separated with semicolon) like "col;scat;"
     * @param {string} [args.icon] - icon name shown for the class in hierarchy browser
     * @param {string} [args.draw_field] - draw only data member from object, like fHistogram
     * @protected */
   jsrp.addDrawFunc = function(args) {
      drawFuncs.lst.push(args);
      return args;
   }

   /** @summary return draw handle for specified item kind
     * @desc kind could be ROOT.TH1I for ROOT classes or just
     * kind string like "Command" or "Text"
     * selector can be used to search for draw handle with specified option (string)
     * or just sequence id
     * @memberof JSROOT.Painter
     * @private */
   function getDrawHandle(kind, selector) {

      if (typeof kind != 'string') return null;
      if (selector === "") selector = null;

      let first = null;

      if ((selector === null) && (kind in drawFuncs.cache))
         return drawFuncs.cache[kind];

      let search = (kind.indexOf("ROOT.") == 0) ? kind.substr(5) : "kind:" + kind, counter = 0;
      for (let i = 0; i < drawFuncs.lst.length; ++i) {
         let h = drawFuncs.lst[i];
         if (typeof h.name == "string") {
            if (h.name != search) continue;
         } else {
            if (!search.match(h.name)) continue;
         }

         if (h.sameas !== undefined)
            return getDrawHandle("ROOT." + h.sameas, selector);

         if ((selector === null) || (selector === undefined)) {
            // store found handle in cache, can reuse later
            if (!(kind in drawFuncs.cache)) drawFuncs.cache[kind] = h;
            return h;
         } else if (typeof selector == 'string') {
            if (!first) first = h;
            // if drawoption specified, check it present in the list

            if (selector == "::expand") {
               if (('expand' in h) || ('expand_item' in h)) return h;
            } else
               if ('opt' in h) {
                  let opts = h.opt.split(';');
                  for (let j = 0; j < opts.length; ++j) opts[j] = opts[j].toLowerCase();
                  if (opts.indexOf(selector.toLowerCase()) >= 0) return h;
               }
         } else if (selector === counter) {
            return h;
         }
         ++counter;
      }

      return first;
   }

   /** @summary Scan streamer infos for derived classes
     * @desc Assign draw functions for such derived classes
     * @private */
   jsrp.addStreamerInfos = function(lst) {
      if (!lst) return;

      function CheckBaseClasses(si, lvl) {
         if (!si.fElements) return null;
         if (lvl > 10) return null; // protect against recursion

         for (let j = 0; j < si.fElements.arr.length; ++j) {
            // extract streamer info for each class member
            let element = si.fElements.arr[j];
            if (element.fTypeName !== 'BASE') continue;

            let handle = getDrawHandle("ROOT." + element.fName);
            if (handle && !handle.for_derived) handle = null;

            // now try find that base class of base in the list
            if (handle === null)
               for (let k = 0; k < lst.arr.length; ++k)
                  if (lst.arr[k].fName === element.fName) {
                     handle = CheckBaseClasses(lst.arr[k], lvl + 1);
                     break;
                  }

            if (handle && handle.for_derived) return handle;
         }
         return null;
      }

      for (let n = 0; n < lst.arr.length; ++n) {
         let si = lst.arr[n];
         if (getDrawHandle("ROOT." + si.fName) !== null) continue;

         let handle = CheckBaseClasses(si, 0);

         if (!handle) continue;

         let newhandle = JSROOT.extend({}, handle);
         // delete newhandle.for_derived; // should we disable?
         newhandle.name = si.fName;
         drawFuncs.lst.push(newhandle);
      }
   }

   /** @summary Provide draw settings for specified class or kind
     * @memberof JSROOT.Painter
     * @private */
   function getDrawSettings(kind, selector) {
      let res = { opts: null, inspect: false, expand: false, draw: false, handle: null };
      if (typeof kind != 'string') return res;
      let isany = false, noinspect = false, canexpand = false;
      if (typeof selector !== 'string') selector = "";

      for (let cnt = 0; cnt < 1000; ++cnt) {
         let h = getDrawHandle(kind, cnt);
         if (!h) break;
         if (!res.handle) res.handle = h;
         if (h.noinspect) noinspect = true;
         if (h.expand || h.expand_item || h.can_expand) canexpand = true;
         if (!('func' in h)) break;
         isany = true;
         if (!('opt' in h)) continue;
         let opts = h.opt.split(';');
         for (let i = 0; i < opts.length; ++i) {
            opts[i] = opts[i].toLowerCase();
            if ((selector.indexOf('nosame') >= 0) && (opts[i].indexOf('same') == 0)) continue;

            if (res.opts === null) res.opts = [];
            if (res.opts.indexOf(opts[i]) < 0) res.opts.push(opts[i]);
         }
         if (h.theonly) break;
      }

      if (selector.indexOf('noinspect') >= 0) noinspect = true;

      if (isany && (res.opts === null)) res.opts = [""];

      // if no any handle found, let inspect ROOT-based objects
      if (!isany && (kind.indexOf("ROOT.") == 0) && !noinspect) res.opts = [];

      if (!noinspect && res.opts)
         res.opts.push("inspect");

      res.inspect = !noinspect;
      res.expand = canexpand;
      res.draw = res.opts && (res.opts.length > 0);

      return res;
   }

   /** @summary Returns true if provided object class can be drawn
     * @param {string} classname - name of class to be tested
     * @private */
   jsrp.canDraw = function(classname) {
      return getDrawSettings("ROOT." + classname).opts !== null;
   }

   /** @summary Implementation of JSROOT.draw
     * @private */
   function jsroot_draw(divid, obj, opt) {

      if (!obj || (typeof obj !== 'object'))
         return Promise.reject(Error('not an object in JSROOT.draw'));

      if (opt == 'inspect')
         return JSROOT.require("hierarchy").then(() => jsrp.drawInspector(divid, obj));

      let handle, type_info;
      if ('_typename' in obj) {
         type_info = "type " + obj._typename;
         handle = getDrawHandle("ROOT." + obj._typename, opt);
      } else if ('_kind' in obj) {
         type_info = "kind " + obj._kind;
         handle = getDrawHandle(obj._kind, opt);
      } else
         return JSROOT.require("hierarchy").then(() => jsrp.drawInspector(divid, obj));

      // this is case of unsupported class, close it normally
      if (!handle)
         return Promise.reject(Error(`Object of ${type_info} cannot be shown with JSROOT.draw`));

      if (handle.dummy)
         return Promise.resolve(null);

      if (handle.draw_field && obj[handle.draw_field])
         return JSROOT.draw(divid, obj[handle.draw_field], opt);

      if (!handle.func && !handle.direct) {
         if (opt && (opt.indexOf("same") >= 0)) {

            let main_painter = jsrp.getElementMainPainter(divid);

            if (main_painter && (typeof main_painter.performDrop === 'function'))
               return main_painter.performDrop(obj, "", null, opt);
         }

         return Promise.reject(Error(`Function not specified to draw object ${type_info}`));
      }

      function isPromise(obj) {
         return obj && (typeof obj == 'object') && (typeof obj.then == 'function');
      }

      function performDraw() {
         let promise;
         if (handle.direct == "v7") {
            let painter = new ObjectPainter(divid, obj, opt);
            painter.csstype = handle.csstype;
            promise = jsrp.ensureRCanvas(painter, handle.frame || false).then(() => {
               painter.redraw = handle.func;
               painter.redraw();
               return painter;
            })
         } else if (handle.direct) {
            let painter = new ObjectPainter(divid, obj, opt);
            promise = jsrp.ensureTCanvas(painter, handle.frame || false).then(() => {
               painter.redraw = handle.func;
               painter.redraw();
               return painter;
            });
         } else {
            promise = handle.func(divid, obj, opt);

            if (!isPromise(promise)) promise = Promise.resolve(promise);
         }

         return promise.then(p => {
            if (!p)
               return Promise.reject(Error(`Fail to draw object ${type_info}`));

            if ((typeof p == 'object') && !p.options)
               p.options = { original: opt || "" }; // keep original draw options

             return p;
         });
      }

      if (typeof handle.func == 'function')
         return performDraw();

      let funcname = handle.func;
      if (typeof funcname != "string")
         return Promise.reject(Error(`Draw function not specified to draw ${type_info}`));

      let prereq = handle.prereq || "";
      if (handle.direct == "v7")
         prereq += ";v7gpad";
      else if (handle.direct)
         prereq += ";gpad";
      let script = handle.script || "";
      if (script) script = script.split(";");

      if (!prereq && !script)
         return Promise.reject(Error(`Prerequicities to load ${funcname} are not specified`));

      return JSROOT.require(prereq).then(() => JSROOT.loadScript(script)).then(() => {
         let func = JSROOT.findFunction(funcname);
         if (!func)
            return Promise.reject(Error(`Fail to find function ${funcname} after loading ${prereq}`));

         handle.func = func; // remember function once it found

         return performDraw();
      });
   }

   /** @summary Draw object in specified HTML element with given draw options.
     * @param {string|object} dom - id of div element to draw or directly DOMElement
     * @param {object} obj - object to draw, object type should be registered before in JSROOT
     * @param {string} opt - draw options separated by space, comma or semicolon
     * @param {function} [callback] - deprecated, will be removed in 6.2.0, called with painter object
     * @returns {Promise} with painter object only if callback parameter is not specified
     * @requires painter
     * @desc An extensive list of support draw options can be found on [JSROOT examples page]{@link https://root.cern/js/latest/examples.htm}
     * Parameter ```callback``` kept only for backward compatibility and will be removed in JSROOT v6.2
     * @example
     * JSROOT.openFile("https://root.cern/js/files/hsimple.root")
     *       .then(file => file.readObject("hpxpy;1"))
     *       .then(obj => JSROOT.draw("drawing", obj, "colz;logx;gridx;gridy")); */
   JSROOT.draw = function(dom, obj, opt, callback) {
      let res = jsroot_draw(dom, obj, opt);
      if (!callback || (typeof callback != 'function')) return res;
      res.then(callback).catch(() => callback(null));
   }

   /** @summary Redraw object in specified HTML element with given draw options.
     * @param {string|object} dom - id of div element to draw or directly DOMElement
     * @param {object} obj - object to draw, object type should be registered before in JSROOT
     * @param {string} opt - draw options
     * @param {function} [callback] - deprecated, will be removed in 6.2.0, called with painter object
     * @returns {Promise} with painter used only when callback parameter is not specified
     * @requires painter
     * @desc If drawing was not done before, it will be performed with {@link JSROOT.draw}.
     * Otherwise drawing content will be updated
     * Parameter ```callback``` kept only for backward compatibility and will be removed in JSROOT v6.2 */
   JSROOT.redraw = function(dom, obj, opt, callback) {

      if (!obj || (typeof obj !== 'object'))
         return callback ? callback(null) : Promise.reject(Error('not an object in JSROOT.redraw'));

      let can_painter = jsrp.getElementCanvPainter(dom), handle, res_painter = null, redraw_res;
      if (obj._typename)
         handle = getDrawHandle("ROOT." + obj._typename);
      if (handle && handle.draw_field && obj[handle.draw_field])
         obj = obj[handle.draw_field];

      if (can_painter) {
         if (can_painter.matchObjectType(obj._typename)) {
            redraw_res = can_painter.redrawObject(obj, opt);
            if (redraw_res) res_painter = can_painter;
         } else {
            for (let i = 0; i < can_painter.painters.length; ++i) {
               let painter = can_painter.painters[i];
               if (painter.matchObjectType(obj._typename)) {
                  redraw_res = painter.redrawObject(obj, opt);
                  if (redraw_res) {
                     res_painter = painter;
                     break;
                  }
               }
            }
         }
      } else {
         let dummy = new ObjectPainter(dom),
             top = dummy.getTopPainter();
         // base painter do not have this method, if it there use it
         // it can be object painter here or can be specially introduce method to handling redraw!
         if (top && typeof top.redrawObject == 'function') {
            redraw_res = top.redrawObject(obj, opt);
            if (redraw_res) res_painter = top;
         }
      }

      if (res_painter) {
         if (!redraw_res || (typeof redraw_res != 'object') || !redraw_res.then)
            redraw_res = Promise.resolve(true);
         return redraw_res.then(() => { if (callback) callback(res_painter); return res_painter; });
      }

      JSROOT.cleanup(dom);

      return JSROOT.draw(dom, obj, opt, callback);
   }

   /** @summary Save object, drawn in specified element, as JSON.
     * @desc Normally it is TCanvas object with list of primitives
     * @param {string|object} dom - id of top div element or directly DOMElement
     * @returns {string} produced JSON string */
   JSROOT.drawingJSON = function(dom) {
      let canp = jsrp.getElementCanvPainter(dom);
      return canp ? canp.produceJSON() : "";
   }

   /** @summary Compress SVG code, produced from JSROOT drawing
     * @desc removes extra info or empty elements
     * @private */
   jsrp.compressSVG = function(svg) {

      svg = svg.replace(/url\(\&quot\;\#(\w+)\&quot\;\)/g, "url(#$1)")        // decode all URL
               .replace(/ class=\"\w*\"/g, "")                                // remove all classes
               .replace(/ pad=\"\w*\"/g, "")                                  // remove all pad ids
               .replace(/ title=\"\"/g, "")                                   // remove all empty titles
               .replace(/<g objname=\"\w*\" objtype=\"\w*\"/g, "<g")          // remove object ids
               .replace(/<g transform=\"translate\(\d+\,\d+\)\"><\/g>/g, "")  // remove all empty groups with transform
               .replace(/<g><\/g>/g, "");                                     // remove all empty groups

      // remove all empty frame svgs, typically appears in 3D drawings, maybe should be improved in frame painter itself
      svg = svg.replace(/<svg x=\"0\" y=\"0\" overflow=\"hidden\" width=\"\d+\" height=\"\d+\" viewBox=\"0 0 \d+ \d+\"><\/svg>/g, "")

      if (svg.indexOf("xlink:href") < 0)
         svg = svg.replace(/ xmlns:xlink=\"http:\/\/www.w3.org\/1999\/xlink\"/g, "");

      return svg;
   }

   /** @summary Create SVG image for provided object.
     * @desc Function especially useful in Node.js environment to generate images for
     * supported ROOT classes
     * @param {object} args - contains different settings
     * @param {object} args.object - object for the drawing
     * @param {string} [args.option] - draw options
     * @param {number} [args.width = 1200] - image width
     * @param {number} [args.height = 800] - image height
     * @returns {Promise} with svg code */
   JSROOT.makeSVG = function(args) {

      if (!args) args = {};
      if (!args.object) return Promise.reject(Error("No object specified to generate SVG"));
      if (!args.width) args.width = 1200;
      if (!args.height) args.height = 800;

      function build(main) {

         main.attr("width", args.width).attr("height", args.height);

         main.style("width", args.width + "px").style("height", args.height + "px");

         JSROOT._.svg_3ds = undefined;

         return JSROOT.draw(main.node(), args.object, args.option || "").then(() => {

            let has_workarounds = JSROOT._.svg_3ds && jsrp.processSvgWorkarounds;

            main.select('svg')
                .attr("xmlns", "http://www.w3.org/2000/svg")
                .attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
                .attr("width", args.width)
                .attr("height", args.height)
                .attr("style", null).attr("class", null).attr("x", null).attr("y", null);

            let svg = main.html();

            if (has_workarounds)
               svg = jsrp.processSvgWorkarounds(svg);

            svg = jsrp.compressSVG(svg);

            main.remove();

            return svg;
         });
      }

      if (!JSROOT.nodejs)
         return build(d3.select('body').append("div").style("visible", "hidden"));

      if (!JSROOT._.nodejs_document) {
         // use eval while old minifier is not able to parse newest Node.js syntax
         const { JSDOM } = require("jsdom");
         JSROOT._.nodejs_window = (new JSDOM("<!DOCTYPE html>hello")).window;
         JSROOT._.nodejs_document = JSROOT._.nodejs_window.document; // used with three.js
         JSROOT._.nodejs_window.d3 = d3.select(JSROOT._.nodejs_document); //get d3 into the dom
      }

      return build(JSROOT._.nodejs_window.d3.select('body').append('div'));
   }

   /** @summary Check resize of drawn element
     * @param {string|object} divid - id or DOM element
     * @param {boolean|object} arg - options on how to resize
     * @desc As first argument divid one should use same argument as for the drawing
     * As second argument, one could specify "true" value to force redrawing of
     * the element even after minimal resize
     * Or one just supply object with exact sizes like { width:300, height:200, force:true };
     * @example
     * JSROOT.resize("drawing", { width: 500, height: 200 } );
     * JSROOT.resize(document.querySelector("#drawing"), true); */
   JSROOT.resize = function(divid, arg) {
      if (arg === true) arg = { force: true }; else
         if (typeof arg !== 'object') arg = null;
      let done = false, dummy = new ObjectPainter(divid);
      dummy.forEachPainter(painter => {
         if (!done && (typeof painter.checkResize == 'function'))
            done = painter.checkResize(arg);
      });
      return done;
   }

   /** @summary Returns canvas painter (if any) for specified HTML element
     * @param {string|object} dom - id or DOM element
     * @private */
   jsrp.getElementCanvPainter = function(dom) {
      let dummy = new ObjectPainter(dom);
      return dummy.getCanvPainter();
   }

   /** @summary Returns main painter (if any) for specified HTML element - typically histogram painter
     * @param {string|object} dom - id or DOM element
     * @private */
   jsrp.getElementMainPainter = function(dom) {
      let dummy = new ObjectPainter(dom);
      return dummy.getMainPainter(true);
   }

   /** @summary Safely remove all JSROOT drawings from specified element
     * @param {string|object} dom - id or DOM element
     * @requires painter
     * @example
     * JSROOT.cleanup("drawing");
     * JSROOT.cleanup(document.querySelector("#drawing")); */
   JSROOT.cleanup = function(divid) {
      let dummy = new ObjectPainter(divid), lst = [];
      dummy.forEachPainter(p => { if (lst.indexOf(p) < 0) lst.push(p); });
      lst.forEach(p => p.cleanup());
      dummy.selectDom().html("");
      return lst;
   }

   /** @summary Display progress message in the left bottom corner.
     * @desc Previous message will be overwritten
     * if no argument specified, any shown messages will be removed
     * @param {string} msg - message to display
     * @param {number} tmout - optional timeout in milliseconds, after message will disappear
     * @private */
   jsrp.showProgress = function(msg, tmout) {
      if (JSROOT.batch_mode || (typeof document === 'undefined')) return;
      let id = "jsroot_progressbox",
          box = d3.select("#" + id);

      if (!JSROOT.settings.ProgressBox) return box.remove();

      if ((arguments.length == 0) || !msg) {
         if ((tmout !== -1) || (!box.empty() && box.property("with_timeout"))) box.remove();
         return;
      }

      if (box.empty()) {
         box = d3.select(document.body).append("div").attr("id", id);
         box.append("p");
      }

      box.property("with_timeout", false);

      if (typeof msg === "string") {
         box.select("p").html(msg);
      } else {
         box.html("");
         box.node().appendChild(msg);
      }

      if (!isNaN(tmout) && (tmout > 0)) {
         box.property("with_timeout", true);
         setTimeout(() => jsrp.showProgress('', -1), tmout);
      }
   }

   /** @summary Converts numeric value to string according to specified format.
     * @param {number} value - value to convert
     * @param {string} [fmt="6.4g"] - format can be like 5.4g or 4.2e or 6.4f
     * @param {boolean} [ret_fmt] - when true returns array with value and actual format like ["0.1","6.4f"]
     * @returns {string|Array} - converted value or array with value and actual format */
   jsrp.floatToString = function(value, fmt, ret_fmt) {
      if (!fmt) fmt = "6.4g";

      fmt = fmt.trim();
      let len = fmt.length;
      if (len<2)
         return ret_fmt ? [value.toFixed(4), "6.4f"] : value.toFixed(4);
      let last = fmt[len-1];
      fmt = fmt.slice(0,len-1);
      let isexp, prec = fmt.indexOf(".");
      prec = (prec<0) ? 4 : parseInt(fmt.slice(prec+1));
      if (isNaN(prec) || (prec <=0)) prec = 4;

      let significance = false;
      if ((last=='e') || (last=='E')) { isexp = true; } else
      if (last=='Q') { isexp = true; significance = true; } else
      if ((last=='f') || (last=='F')) { isexp = false; } else
      if (last=='W') { isexp = false; significance = true; } else
      if ((last=='g') || (last=='G')) {
         let se = jsrp.floatToString(value, fmt+'Q', true),
             sg = jsrp.floatToString(value, fmt+'W', true);

         if (se[0].length < sg[0].length) sg = se;
         return ret_fmt ? sg : sg[0];
      } else {
         isexp = false;
         prec = 4;
      }

      if (isexp) {
         // for exponential representation only one significant digit befor point
         if (significance) prec--;
         if (prec < 0) prec = 0;

         let se = value.toExponential(prec);

         return ret_fmt ? [se, '5.'+prec+'e'] : se;
      }

      let sg = value.toFixed(prec);

      if (significance) {

         // when using fixed representation, one could get 0.0
         if ((value!=0) && (Number(sg)==0.) && (prec>0)) {
            prec = 20; sg = value.toFixed(prec);
         }

         let l = 0;
         while ((l<sg.length) && (sg[l] == '0' || sg[l] == '-' || sg[l] == '.')) l++;

         let diff = sg.length - l - prec;
         if (sg.indexOf(".")>l) diff--;

         if (diff != 0) {
            prec-=diff;
            if (prec<0) prec = 0; else if (prec>20) prec = 20;
            sg = value.toFixed(prec);
         }
      }

      return ret_fmt ? [sg, '5.'+prec+'f'] : sg;
   }

   /** @summary Tries to close current browser tab
     * @desc Many browsers do not allow simple window.close() call,
     * therefore try several workarounds
     * @private */
   jsrp.closeCurrentWindow = function() {
      if (!window) return;
      window.close();
      window.open('', '_self').close();
   }

   jsrp.createRootColors();

   if (JSROOT.nodejs) jsrp.readStyleFromURL("?interactive=0&tooltip=0&nomenu&noprogress&notouch&toolbar=0&webgl=0");

   jsrp.getDrawHandle = getDrawHandle;
   jsrp.getDrawSettings = getDrawSettings;

   JSROOT.DrawOptions = DrawOptions;
   JSROOT.ColorPalette = ColorPalette;
   JSROOT.TAttLineHandler = TAttLineHandler;
   JSROOT.TAttFillHandler = TAttFillHandler;
   JSROOT.TAttMarkerHandler = TAttMarkerHandler;
   JSROOT.FontHandler = FontHandler;
   JSROOT.BasePainter = BasePainter;
   JSROOT.ObjectPainter = ObjectPainter;
   JSROOT.AxisBasePainter = AxisBasePainter;

   JSROOT.Painter = jsrp;
   if (JSROOT.nodejs) module.exports = jsrp;

   return jsrp;

});