hist2d/TGraphPainter.mjs

import { gStyle, BIT, settings, create, createHistogram, setHistogramTitle, isFunc, isStr,
         clTPaveStats, clTCutG, clTH1F, clTH2F, clTF1, clTF2, clTPad, kNoZoom, kNoStats } from '../core.mjs';
import { select as d3_select } from '../d3.mjs';
import { DrawOptions, buildSvgCurve, makeTranslate, addHighlightStyle } from '../base/BasePainter.mjs';
import { ObjectPainter, kAxisNormal } from '../base/ObjectPainter.mjs';
import { FunctionsHandler } from './THistPainter.mjs';
import { TH1Painter, PadDrawOptions } from './TH1Painter.mjs';
import { kBlack, kWhite } from '../base/colors.mjs';
import { addMoveHandler } from '../gui/utils.mjs';
import { assignContextMenu, kNoReorder } from '../gui/menu.mjs';


const kNotEditable = BIT(18),   // bit set if graph is non editable
      clTGraphErrors = 'TGraphErrors',
      clTGraphAsymmErrors = 'TGraphAsymmErrors',
      clTGraphBentErrors = 'TGraphBentErrors',
      clTGraphMultiErrors = 'TGraphMultiErrors';

/**
 * @summary Painter for TGraph object.
 *
 * @private
 */


class TGraphPainter extends ObjectPainter {

   constructor(dom, graph) {
      super(dom, graph);
      this.axes_draw = false; // indicate if graph histogram was drawn for axes
      this.bins = null;
      this.xmin = this.ymin = this.xmax = this.ymax = 0;
      this.wheel_zoomy = true;
      this.is_bent = (graph._typename === clTGraphBentErrors);
      this.has_errors = (graph._typename === clTGraphErrors) ||
                        (graph._typename === clTGraphMultiErrors) ||
                        (graph._typename === clTGraphAsymmErrors) ||
                         this.is_bent || graph._typename.match(/^RooHist/);
   }

   /** @summary Return drawn graph object */
   getGraph() { return this.getObject(); }

   /** @summary Return histogram object used for axis drawings */
   getHistogram() { return this.getObject()?.fHistogram; }

   /** @summary Set histogram object to graph */
   setHistogram(histo) {
      const obj = this.getObject();
      if (obj) obj.fHistogram = histo;
   }

   /** @summary Redraw graph
     * @desc may redraw histogram which was used to draw axes
     * @return {Promise} for ready */
   async redraw() {
      let promise = Promise.resolve(true);

      if (this.$redraw_hist) {
         delete this.$redraw_hist;
         const hist_painter = this.getMainPainter();
         if (hist_painter?.isSecondary(this) && this.axes_draw)
            promise = hist_painter.redraw();
      }

      return promise.then(() => this.drawGraph()).then(() => {
         const res = this._funcHandler?.drawNext(0) ?? this;
         delete this._funcHandler;
         return res;
      });
   }

   /** @summary Cleanup graph painter */
   cleanup() {
      delete this.interactive_bin; // break mouse handling
      delete this.bins;
      super.cleanup();
   }

   /** @summary Returns object if this drawing TGraphMultiErrors object */
   get_gme() {
      const graph = this.getGraph();
      return graph?._typename === clTGraphMultiErrors ? graph : null;
   }

   /** @summary Decode options */
   decodeOptions(opt, first_time) {
      if (isStr(opt) && (opt.indexOf('same ') === 0))
         opt = opt.slice(5);

      const graph = this.getGraph(),
          is_gme = Boolean(this.get_gme()),
          has_main = first_time ? Boolean(this.getMainPainter()) : !this.axes_draw;
      let blocks_gme = [];

      if (!this.options) this.options = {};

      // decode main draw options for the graph
      const decodeBlock = (d, res) => {
         Object.assign(res, { Line: 0, Curve: 0, Rect: 0, Mark: 0, Bar: 0, OutRange: 0, EF: 0, Fill: 0, MainError: 1, Ends: 1, ScaleErrX: 1 });

         if (is_gme && d.check('S=', true)) res.ScaleErrX = d.partAsFloat();

         if (d.check('L')) res.Line = 1;
         if (d.check('F')) res.Fill = 1;
         if (d.check('CC')) res.Curve = 2; // draw all points without reduction
         if (d.check('C')) res.Curve = 1;
         if (d.check('*')) res.Mark = 103;
         if (d.check('P0')) res.Mark = 104;
         if (d.check('P')) res.Mark = 1;
         if (d.check('B')) { res.Bar = 1; res.Errors = 0; }
         if (d.check('Z')) { res.Errors = 1; res.Ends = 0; }
         if (d.check('||')) { res.Errors = 1; res.MainError = 0; res.Ends = 1; }
         if (d.check('[]')) { res.Errors = 1; res.MainError = 0; res.Ends = 2; }
         if (d.check('|>')) { res.Errors = 1; res.Ends = 3; }
         if (d.check('>')) { res.Errors = 1; res.Ends = 4; }
         if (d.check('0')) { res.Mark = 1; res.Errors = 1; res.OutRange = 1; }
         if (d.check('1')) if (res.Bar === 1) res.Bar = 2;
         if (d.check('2')) { res.Rect = 1; res.Errors = 0; }
         if (d.check('3')) { res.EF = 1; res.Errors = 0; }
         if (d.check('4')) { res.EF = 2; res.Errors = 0; }
         if (d.check('5')) { res.Rect = 2; res.Errors = 0; }
         if (d.check('X')) res.Errors = 0;
      };

      Object.assign(this.options, { Axis: '', NoOpt: 0, PadStats: false, PadPalette: false, original: opt, second_x: false, second_y: false, individual_styles: false });

      if (is_gme && opt) {
         if (opt.indexOf(';') > 0) {
            blocks_gme = opt.split(';');
            opt = blocks_gme.shift();
         } else if (opt.indexOf('_') > 0) {
            blocks_gme = opt.split('_');
            opt = blocks_gme.shift();
         }
      }

      const res = this.options;
      let d = new DrawOptions(opt), hopt = '';

      PadDrawOptions.forEach(name => { if (d.check(name)) hopt += ';' + name; });
      if (d.check('XAXIS_', true)) hopt += ';XAXIS_' + d.part;
      if (d.check('YAXIS_', true)) hopt += ';YAXIS_' + d.part;

      if (d.empty()) {
         res.original = has_main ? 'lp' : 'alp';
         d = new DrawOptions(res.original);
      }

      if (d.check('NOOPT')) res.NoOpt = 1;

      if (d.check('POS3D_', true)) res.pos3d = d.partAsInt() - 0.5;

      if (d.check('PFC') && !res._pfc)
         res._pfc = 2;
      if (d.check('PLC') && !res._plc)
         res._plc = 2;
      if (d.check('PMC') && !res._pmc)
         res._pmc = 2;

      if (d.check('A')) res.Axis = d.check('I') ? 'A;' : ' '; // I means invisible axis
      if (d.check('X+')) { res.Axis += 'X+'; res.second_x = has_main; }
      if (d.check('Y+')) { res.Axis += 'Y+'; res.second_y = has_main; }
      if (d.check('RX')) res.Axis += 'RX';
      if (d.check('RY')) res.Axis += 'RY';

      if (is_gme) {
         res.blocks = [];
         res.skip_errors_x0 = res.skip_errors_y0 = false;
         if (d.check('X0')) res.skip_errors_x0 = true;
         if (d.check('Y0')) res.skip_errors_y0 = true;
      }

      decodeBlock(d, res);

      if (is_gme)
         if (d.check('S')) res.individual_styles = true;


      // if (d.check('E')) res.Errors = 1; // E option only defined for TGraphPolar

      if (res.Errors === undefined)
         res.Errors = this.has_errors && (!is_gme || !blocks_gme.length) ? 1 : 0;

      // special case - one could use svg:path to draw many pixels (
      if ((res.Mark === 1) && (graph.fMarkerStyle === 1)) res.Mark = 101;

      // if no drawing option is selected and if opt === '' nothing is done.
      if (res.Line + res.Fill + res.Curve + res.Mark + res.Bar + res.EF + res.Rect + res.Errors === 0)
         if (d.empty()) res.Line = 1;


      if (this.matchObjectType(clTGraphErrors)) {
         const len = graph.fEX.length;
         let m = 0;
         for (let k = 0; k < len; ++k)
            m = Math.max(m, graph.fEX[k], graph.fEY[k]);
         if (m < 1e-100)
            res.Errors = 0;
      }

      this._cutg = this.matchObjectType(clTCutG);
      this._cutg_lastsame = this._cutg && (graph.fNpoints > 3) &&
                            (graph.fX[0] === graph.fX[graph.fNpoints-1]) && (graph.fY[0] === graph.fY[graph.fNpoints-1]);

      if (!res.Axis) {
         // check if axis should be drawn
         // either graph drawn directly or
         // graph is first object in list of primitives
         const pad = this.getPadPainter()?.getRootPad(true);
         if (!pad || (pad?.fPrimitives?.arr[0] === this.getObject())) res.Axis = ' ';
      }

      res.Axis += hopt;

      for (let bl = 0; bl < blocks_gme.length; ++bl) {
         const subd = new DrawOptions(blocks_gme[bl]), subres = {};
         decodeBlock(subd, subres);
         subres.skip_errors_x0 = res.skip_errors_x0;
         subres.skip_errors_y0 = res.skip_errors_y0;
         res.blocks.push(subres);
      }
   }

   /** @summary Extract errors for TGraphMultiErrors */
   extractGmeErrors(nblock) {
      if (!this.bins) return;
      const gr = this.getGraph();
      this.bins.forEach(bin => {
         bin.eylow = gr.fEyL[nblock][bin.indx];
         bin.eyhigh = gr.fEyH[nblock][bin.indx];
      });
   }

   /** @summary Create bins for TF1 drawing */
   createBins() {
      const gr = this.getGraph();
      if (!gr) return;

      let kind = 0, npoints = gr.fNpoints;
      if (this._cutg && this._cutg_lastsame)
         npoints--;

      if (gr._typename === clTGraphErrors)
         kind = 1;
      else if (gr._typename === clTGraphMultiErrors)
         kind = 2;
      else if (gr._typename === clTGraphAsymmErrors || gr._typename === clTGraphBentErrors || gr._typename.match(/^RooHist/))
         kind = 3;

      this.bins = new Array(npoints);

      for (let p = 0; p < npoints; ++p) {
         const bin = this.bins[p] = { x: gr.fX[p], y: gr.fY[p], indx: p };
         switch (kind) {
            case 1:
               bin.exlow = bin.exhigh = gr.fEX[p];
               bin.eylow = bin.eyhigh = gr.fEY[p];
               break;
            case 2:
               bin.exlow = gr.fExL[p];
               bin.exhigh = gr.fExH[p];
               bin.eylow = gr.fEyL[0][p];
               bin.eyhigh = gr.fEyH[0][p];
               break;
            case 3:
               bin.exlow = gr.fEXlow[p];
               bin.exhigh = gr.fEXhigh[p];
               bin.eylow = gr.fEYlow[p];
               bin.eyhigh = gr.fEYhigh[p];
               break;
         }

         if (p === 0) {
            this.xmin = this.xmax = bin.x;
            this.ymin = this.ymax = bin.y;
         }

         if (kind > 0) {
            this.xmin = Math.min(this.xmin, bin.x - bin.exlow, bin.x + bin.exhigh);
            this.xmax = Math.max(this.xmax, bin.x - bin.exlow, bin.x + bin.exhigh);
            this.ymin = Math.min(this.ymin, bin.y - bin.eylow, bin.y + bin.eyhigh);
            this.ymax = Math.max(this.ymax, bin.y - bin.eylow, bin.y + bin.eyhigh);
         } else {
            this.xmin = Math.min(this.xmin, bin.x);
            this.xmax = Math.max(this.xmax, bin.x);
            this.ymin = Math.min(this.ymin, bin.y);
            this.ymax = Math.max(this.ymax, bin.y);
         }
      }

      // workaround, are there better way to show marker at 0,0 on the top of the frame?
      this._frame_layer = true;
      if ((this.xmin === 0) && (this.ymin === 0) && (npoints > 0) && (this.bins[0].x === 0) && (this.bins[0].y === 0) &&
          this.options.Mark && !this.options.Line && !this.options.Curve && !this.options.Fill)
         this._frame_layer = 'upper_layer';
   }

   /** @summary Return margins for histogram ranges */
   getHistRangeMargin() { return 0.1; }

   /** @summary Create histogram for graph
     * @desc graph bins should be created when calling this function
     * @param {boolean} [set_x] - set X axis range
     * @param {boolean} [set_y] - set Y axis range */
   createHistogram(set_x = true, set_y = true) {
      const graph = this.getGraph(),
            xmin = this.xmin,
            margin = this.getHistRangeMargin();
      let xmax = this.xmax, ymin = this.ymin, ymax = this.ymax;

      if (xmin >= xmax) xmax = xmin + 1;
      if (ymin >= ymax) ymax = ymin + 1;
      const dx = (xmax - xmin) * margin, dy = (ymax - ymin) * margin;
      let uxmin = xmin - dx, uxmax = xmax + dx,
          minimum = ymin - dy, maximum = ymax + dy;

      if ((ymin > 0) && (minimum <= 0))
         minimum = (1 - margin) * ymin;
      if ((ymax < 0) && (maximum >= 0))
         maximum = (1 - margin) * ymax;

      const minimum0 = minimum, maximum0 = maximum;
      let histo = this.getHistogram();

      if (!this._not_adjust_hrange && !histo?.fXaxis.fTimeDisplay) {
         const pad_logx = this.getPadPainter()?.getPadLog('x');

         if ((uxmin < 0) && (xmin >= 0))
            uxmin = pad_logx ? xmin * (1 - margin) : 0;
         if ((uxmax > 0) && (xmax <= 0))
            uxmax = pad_logx ? (1 + margin) * xmax : 0;
      }

      if (!histo) {
         histo = this._is_scatter ? createHistogram(clTH2F, 30, 30) : createHistogram(clTH1F, 100);
         histo.fName = graph.fName + '_h';
         histo.fBits |= kNoStats;
         this._own_histogram = true;
         this.setHistogram(histo);
      } else if ((histo.fMaximum !== kNoZoom) && (histo.fMinimum !== kNoZoom)) {
         minimum = histo.fMinimum;
         maximum = histo.fMaximum;
      }

      if (graph.fMinimum !== kNoZoom)
         minimum = ymin = graph.fMinimum;
      if (graph.fMaximum !== kNoZoom)
         maximum = graph.fMaximum;
      if ((minimum < 0) && (ymin >= 0))
         minimum = (1 - margin) * ymin;
      if ((ymax < 0) && (maximum >= 0))
         maximum = (1 - margin) * ymax;

      setHistogramTitle(histo, this.getObject().fTitle);

      if (set_x && !histo.fXaxis.fLabels) {
         histo.fXaxis.fXmin = uxmin;
         histo.fXaxis.fXmax = uxmax;
      }

      if (set_y && !histo.fYaxis.fLabels) {
         histo.fYaxis.fXmin = Math.min(minimum0, minimum);
         histo.fYaxis.fXmax = Math.max(maximum0, maximum);
         if (!this._is_scatter) {
            histo.fMinimum = minimum;
            histo.fMaximum = maximum;
         }
      }

      histo.$xmin_nz = xmin > 0 ? xmin : undefined;
      histo.$ymin_nz = ymin > 0 ? ymin : undefined;

      return histo;
   }

   /** @summary Check if user range can be un-zommed
     * @desc Used when graph points covers larger range than provided histogram */
   unzoomUserRange(dox, doy /* , doz */) {
      const graph = this.getGraph();
      if (this._own_histogram || !graph)
         return false;

      const histo = this.getHistogram();

      dox = dox && histo && ((histo.fXaxis.fXmin > this.xmin) || (histo.fXaxis.fXmax < this.xmax));
      doy = doy && histo && ((histo.fYaxis.fXmin > this.ymin) || (histo.fYaxis.fXmax < this.ymax));
      if (!dox && !doy)
         return false;

      this.createHistogram(dox, doy);
      this.getMainPainter()?.extractAxesProperties(1); // just to enforce ranges extraction

      return true;
   }

   /** @summary Returns true if graph drawing can be optimize */
   canOptimize() {
      return (settings.OptimizeDraw > 0) && !this.options.NoOpt;
   }

   /** @summary Returns optimized bins - if optimization enabled */
   optimizeBins(maxpnt, filter_func) {
      if ((this.bins.length < 30) && !filter_func)
         return this.bins;

      let selbins = null;
      if (isFunc(filter_func)) {
         for (let n = 0; n < this.bins.length; ++n) {
            if (filter_func(this.bins[n], n)) {
               if (!selbins) selbins = (n === 0) ? [] : this.bins.slice(0, n);
            } else
               if (selbins) selbins.push(this.bins[n]);
         }
      }
      if (!selbins) selbins = this.bins;

      if (!maxpnt) maxpnt = 500000;

      if ((selbins.length < maxpnt) || !this.canOptimize()) return selbins;
      let step = Math.floor(selbins.length / maxpnt);
      if (step < 2) step = 2;
      const optbins = [];
      for (let n = 0; n < selbins.length; n+=step)
         optbins.push(selbins[n]);

      return optbins;
   }

   /** @summary Check if such function should be drawn directly */
   needDrawFunc(graph, func) {
      if (func._typename === clTPaveStats)
          return (func.fName !== 'stats') || !graph.TestBit(kNoStats); // kNoStats is same for graph and histogram

       if ((func._typename === clTF1) || (func._typename === clTF2))
          return !func.TestBit(BIT(9)); // TF1::kNotDraw

       return true;
   }

   /** @summary Returns tooltip for specified bin */
   getTooltips(d) {
      const pmain = this.get_main(), lines = [],
            funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y),
            gme = this.get_gme();

      lines.push(this.getObjectHint());

      if (d && funcs) {
         if (d.indx !== undefined)
            lines.push('p = ' + d.indx);
         lines.push('x = ' + funcs.axisAsText('x', d.x), 'y = ' + funcs.axisAsText('y', d.y));
         if (gme)
            lines.push('error x = -' + funcs.axisAsText('x', gme.fExL[d.indx]) + '/+' + funcs.axisAsText('x', gme.fExH[d.indx]));
         else if (this.options.Errors && (funcs.x_handle.kind === kAxisNormal) && (d.exlow || d.exhigh))
            lines.push('error x = -' + funcs.axisAsText('x', d.exlow) + '/+' + funcs.axisAsText('x', d.exhigh));

         if (gme) {
            for (let ny = 0; ny < gme.fNYErrors; ++ny)
               lines.push(`error y${ny} = -${funcs.axisAsText('y', gme.fEyL[ny][d.indx])}/+${funcs.axisAsText('y', gme.fEyH[ny][d.indx])}`);
         } else if ((this.options.Errors || (this.options.EF > 0)) && (funcs.y_handle.kind === kAxisNormal) && (d.eylow || d.eyhigh))
            lines.push('error y = -' + funcs.axisAsText('y', d.eylow) + '/+' + funcs.axisAsText('y', d.eyhigh));
      }
      return lines;
   }

   /** @summary Provide frame painter for graph
     * @desc If not exists, emulate its behavior */
   get_main() {
      let pmain = this.getFramePainter();

      if (pmain?.grx && pmain?.gry)
         return pmain;

      // FIXME: check if needed, can be removed easily
      const pp = this.getPadPainter(),
            rect = pp?.getPadRect() || { width: 800, height: 600 };

      pmain = {
         pad_layer: true,
         pad: pp?.getRootPad(true) ?? create(clTPad),
         pw: rect.width,
         ph: rect.height,
         fX1NDC: 0.1, fX2NDC: 0.9, fY1NDC: 0.1, fY2NDC: 0.9,
         getFrameWidth() { return this.pw; },
         getFrameHeight() { return this.ph; },
         grx(value) {
            if (this.pad.fLogx)
               value = (value > 0) ? Math.log10(value) : this.pad.fUxmin;
            else
               value = (value - this.pad.fX1) / (this.pad.fX2 - this.pad.fX1);
            return value * this.pw;
         },
         gry(value) {
            if (this.pad.fLogv ?? this.pad.fLogy)
               value = (value > 0) ? Math.log10(value) : this.pad.fUymin;
            else
               value = (value - this.pad.fY1) / (this.pad.fY2 - this.pad.fY1);
            return (1 - value) * this.ph;
         },
         revertAxis(name, v) {
            if (name === 'x')
               return v / this.pw * (this.pad.fX2 - this.pad.fX1) + this.pad.fX1;
            if (name === 'y')
               return (1 - v / this.ph) * (this.pad.fY2 - this.pad.fY1) + this.pad.fY1;
            return v;
         },
         getGrFuncs() { return this; }
      };

      return pmain.pad ? pmain : null;
   }

   /** @summary append exclusion area to created path */
   appendExclusion(is_curve, path, drawbins, excl_width) {
      const extrabins = [];
      for (let n = drawbins.length - 1; n >= 0; --n) {
         const bin = drawbins[n],
             dlen = Math.sqrt(bin.dgrx**2 + bin.dgry**2);
         if (dlen > 1e-10) {
            // shift point
            bin.grx += excl_width*bin.dgry/dlen;
            bin.gry -= excl_width*bin.dgrx/dlen;
         }
         extrabins.push(bin);
      }

      const path2 = buildSvgCurve(extrabins, { cmd: 'L', line: !is_curve });

      this.draw_g.append('svg:path')
                 .attr('d', path + path2 + 'Z')
                 .call(this.fillatt.func)
                 .style('opacity', 0.75);
   }

   /** @summary draw TGraph bins with specified options
     * @desc Can be called several times */
   drawBins(funcs, options, draw_g, w, h, lineatt, fillatt, main_block) {
      const graph = this.getGraph();
      if (!graph?.fNpoints) return;

      let excl_width = 0, drawbins = null;
      // if markers or errors drawn - no need handle events for line drawing
      // this improves interactivity like zooming around graph points
      const line_events_handling = !this.isBatchMode() && (options.Line || options.Errors) ? 'none' : null;

      if (main_block && lineatt.excl_side) {
         excl_width = lineatt.excl_width;
         if ((lineatt.width > 0) && !options.Line && !options.Curve) options.Line = 1;
      }

      if (options.EF) {
         drawbins = this.optimizeBins((options.EF > 1) ? 20000 : 0);

         // build lower part
         for (let n = 0; n < drawbins.length; ++n) {
            const bin = drawbins[n];
            bin.grx = funcs.grx(bin.x);
            bin.gry = funcs.gry(bin.y - bin.eylow);
         }

         const path1 = buildSvgCurve(drawbins, { line: options.EF < 2, qubic: true }),
             bins2 = [];

         for (let n = drawbins.length - 1; n >= 0; --n) {
            const bin = drawbins[n];
            bin.gry = funcs.gry(bin.y + bin.eyhigh);
            bins2.push(bin);
         }

         // build upper part (in reverse direction)
         const path2 = buildSvgCurve(bins2, { line: options.EF < 2, cmd: 'L', qubic: true }),
            area = draw_g.append('svg:path')
               .attr('d', path1 + path2 + 'Z')
               .call(fillatt.func);

         // Let behaves as ROOT - see JIRA ROOT-8131
         if (fillatt.empty() && fillatt.colorindx)
            area.style('stroke', this.getColor(fillatt.colorindx));
         if (main_block)
            this.draw_kind = 'lines';
      }

      if (options.Line || options.Fill) {
         let close_symbol = '';
         if (this._cutg) {
            close_symbol = 'Z';
            if (!options.original) options.Fill = 1;
         }

         if (options.Fill) {
            close_symbol = 'Z'; // always close area if we want to fill it
            excl_width = 0;
         }

         if (!drawbins) drawbins = this.optimizeBins(0);

         for (let n = 0; n < drawbins.length; ++n) {
            const bin = drawbins[n];
            bin.grx = funcs.grx(bin.x);
            bin.gry = funcs.gry(bin.y);
         }

         const path = buildSvgCurve(drawbins, { line: true, calc: excl_width });

         if (excl_width)
             this.appendExclusion(false, path, drawbins, excl_width);

         const elem = draw_g.append('svg:path')
                            .attr('d', path + close_symbol)
                            .style('pointer-events', line_events_handling);
         if (options.Line)
            elem.call(lineatt.func);

         if (options.Fill)
            elem.call(fillatt.func);
         else
            elem.style('fill', 'none');

         if (main_block)
            this.draw_kind = 'lines';
      }

      if (options.Curve) {
         let curvebins = drawbins;
         if ((this.draw_kind !== 'lines') || !curvebins || ((options.Curve === 1) && (curvebins.length > 20000))) {
            curvebins = this.optimizeBins((options.Curve === 1) ? 20000 : 0);
            for (let n = 0; n < curvebins.length; ++n) {
               const bin = curvebins[n];
               bin.grx = funcs.grx(bin.x);
               bin.gry = funcs.gry(bin.y);
            }
         }

         const path = buildSvgCurve(curvebins, { qubic: !excl_width });
         if (excl_width)
            this.appendExclusion(true, path, curvebins, excl_width);

         draw_g.append('svg:path')
               .attr('d', path)
               .call(lineatt.func)
               .style('fill', 'none')
               .style('pointer-events', line_events_handling);
         if (main_block)
            this.draw_kind = 'lines'; // handled same way as lines
      }

      let nodes = null;

      if (options.Errors || options.Rect || options.Bar) {
         drawbins = this.optimizeBins(5000, (pnt, i) => {
            const grx = funcs.grx(pnt.x);

            // when drawing bars, take all points
            if (!options.Bar && ((grx < 0) || (grx > w))) return true;

            const gry = funcs.gry(pnt.y);

            if (!options.Bar && !options.OutRange && ((gry < 0) || (gry > h))) return true;

            pnt.grx1 = Math.round(grx);
            pnt.gry1 = Math.round(gry);

            if (this.has_errors) {
               pnt.grx0 = Math.round(funcs.grx(pnt.x - options.ScaleErrX*pnt.exlow) - grx);
               pnt.grx2 = Math.round(funcs.grx(pnt.x + options.ScaleErrX*pnt.exhigh) - grx);
               pnt.gry0 = Math.round(funcs.gry(pnt.y - pnt.eylow) - gry);
               pnt.gry2 = Math.round(funcs.gry(pnt.y + pnt.eyhigh) - gry);

               if (this.is_bent) {
                  pnt.grdx0 = Math.round(funcs.gry(pnt.y + graph.fEXlowd[i]) - gry);
                  pnt.grdx2 = Math.round(funcs.gry(pnt.y + graph.fEXhighd[i]) - gry);
                  pnt.grdy0 = Math.round(funcs.grx(pnt.x + graph.fEYlowd[i]) - grx);
                  pnt.grdy2 = Math.round(funcs.grx(pnt.x + graph.fEYhighd[i]) - grx);
               } else
                  pnt.grdx0 = pnt.grdx2 = pnt.grdy0 = pnt.grdy2 = 0;
            }

            return false;
         });

         if (main_block)
            this.draw_kind = 'nodes';

         nodes = draw_g.selectAll('.grpoint')
                       .data(drawbins)
                       .enter()
                       .append('svg:g')
                       .attr('class', 'grpoint')
                       .attr('transform', d => makeTranslate(d.grx1, d.gry1));
      }

      if (options.Bar) {
         // calculate bar width

         let xmin = 0, xmax = 0;
         for (let i = 0; i < drawbins.length; ++i) {
            if (i === 0)
               xmin = xmax = drawbins[i].grx1;
            else {
               xmin = Math.min(xmin, drawbins[i].grx1);
               xmax = Math.max(xmax, drawbins[i].grx1);
            }
         }

         if (drawbins.length === 1)
            drawbins[0].width = w/4; // pathologic case of single bin
         else {
            for (let i = 0; i < drawbins.length; ++i)
               drawbins[i].width = (xmax - xmin) / drawbins.length * gStyle.fBarWidth;
         }

         const yy0 = Math.round(funcs.gry(0));
         let usefill = fillatt;

         if (main_block) {
            const fp = this.getFramePainter(),
                  fpcol = !fp?.fillatt?.empty() ? fp.fillatt.getFillColor() : -1;

            if (fpcol === fillatt.getFillColor())
               usefill = this.createAttFill({ color: fpcol === 'white' ? kBlack : kWhite, pattern: 1001, std: false });
         }

         nodes.append('svg:path')
              .attr('d', d => {
                 d.bar = true; // element drawn as bar
                 const dx = d.width > 1 ? Math.round(-d.width/2) : 0,
                       dw = d.width > 1 ? Math.round(d.width) : 1,
                       dy = (options.Bar !== 1) ? 0 : ((d.gry1 > yy0) ? yy0-d.gry1 : 0),
                       dh = (options.Bar !== 1) ? (h > d.gry1 ? h - d.gry1 : 0) : Math.abs(yy0 - d.gry1);
                 return `M${dx},${dy}h${dw}v${dh}h${-dw}z`;
              })
            .call(usefill.func);
      }

      if (options.Rect) {
         nodes.filter(d => (d.exlow > 0) && (d.exhigh > 0) && (d.eylow > 0) && (d.eyhigh > 0))
           .append('svg:path')
           .attr('d', d => {
               d.rect = true;
               return `M${d.grx0},${d.gry0}H${d.grx2}V${d.gry2}H${d.grx0}Z`;
            })
           .call(fillatt.func)
           .call(options.Rect === 2 ? lineatt.func : () => {});
      }

      this.error_size = 0;

      if (options.Errors) {
         // to show end of error markers, use line width attribute
         let lw = lineatt.width + gStyle.fEndErrorSize;
         const vv = options.Ends ? `m0,${lw}v${-2*lw}` : '',
               hh = options.Ends ? `m${lw},0h${-2*lw}` : '';
         let vleft = vv, vright = vv, htop = hh, hbottom = hh, bb;

         const mainLine = (dx, dy) => {
            if (!options.MainError) return `M${dx},${dy}`;
            const res = 'M0,0';
            if (dx) return res + (dy ? `L${dx},${dy}` : `H${dx}`);
            return dy ? res + `V${dy}` : res;
         };

         switch (options.Ends) {
            case 2:  // option []
               bb = Math.max(lineatt.width+1, Math.round(lw*0.66));
               vleft = `m${bb},${lw}h${-bb}v${-2*lw}h${bb}`;
               vright = `m${-bb},${lw}h${bb}v${-2*lw}h${-bb}`;
               htop = `m${-lw},${bb}v${-bb}h${2*lw}v${bb}`;
               hbottom = `m${-lw},${-bb}v${bb}h${2*lw}v${-bb}`;
               break;
            case 3: // option |>
               lw = Math.max(lw, Math.round(graph.fMarkerSize*8*0.66));
               bb = Math.max(lineatt.width+1, Math.round(lw*0.66));
               vleft = `l${bb},${lw}v${-2*lw}l${-bb},${lw}`;
               vright = `l${-bb},${lw}v${-2*lw}l${bb},${lw}`;
               htop = `l${-lw},${bb}h${2*lw}l${-lw},${-bb}`;
               hbottom = `l${-lw},${-bb}h${2*lw}l${-lw},${bb}`;
               break;
            case 4: // option >
               lw = Math.max(lw, Math.round(graph.fMarkerSize*8*0.66));
               bb = Math.max(lineatt.width+1, Math.round(lw*0.66));
               vleft = `l${bb},${lw}m0,${-2*lw}l${-bb},${lw}`;
               vright = `l${-bb},${lw}m0,${-2*lw}l${bb},${lw}`;
               htop = `l${-lw},${bb}m${2*lw},0l${-lw},${-bb}`;
               hbottom = `l${-lw},${-bb}m${2*lw},0l${-lw},${bb}`;
               break;
         }

         this.error_size = lw;

         lw = Math.floor((lineatt.width-1)/2); // one should take into account half of end-cup line width

         let visible = nodes.filter(d => (d.exlow > 0) || (d.exhigh > 0) || (d.eylow > 0) || (d.eyhigh > 0));
         if (options.skip_errors_x0 || options.skip_errors_y0)
            visible = visible.filter(d => ((d.x !== 0) || !options.skip_errors_x0) && ((d.y !== 0) || !options.skip_errors_y0));

         if (!this.isBatchMode() && settings.Tooltip && main_block) {
            visible.append('svg:path')
                   .attr('d', d => `M${d.grx0},${d.gry0}h${d.grx2-d.grx0}v${d.gry2-d.gry0}h${d.grx0-d.grx2}z`)
                   .style('fill', 'none')
                   .style('pointer-events', 'visibleFill');
         }

         visible.append('svg:path')
                .attr('d', d => {
                   d.error = true;
                   return ((d.exlow > 0) ? mainLine(d.grx0+lw, d.grdx0) + vleft : '') +
                          ((d.exhigh > 0) ? mainLine(d.grx2-lw, d.grdx2) + vright : '') +
                          ((d.eylow > 0) ? mainLine(d.grdy0, d.gry0-lw) + hbottom : '') +
                          ((d.eyhigh > 0) ? mainLine(d.grdy2, d.gry2+lw) + htop : '');
                })
                .style('fill', 'none')
                .call(lineatt.func);
      }

      if (options.Mark) {
         // for tooltips use markers only if nodes were not created
         this.createAttMarker({ attr: graph, style: options.Mark - 100 });

         this.marker_size = this.markeratt.getFullSize();

         this.markeratt.resetPos();

         const want_tooltip = !this.isBatchMode() && settings.Tooltip && (!this.markeratt.fill || (this.marker_size < 7)) && !nodes && main_block,
               hsz = Math.max(5, Math.round(this.marker_size*0.7)),
               maxnummarker = 1000000 / (this.markeratt.getMarkerLength() + 7); // let produce SVG at maximum 1MB

         let path = '', pnt, grx, gry,
             hints_marker = '', step = 1;

         if (!drawbins)
            drawbins = this.optimizeBins(maxnummarker);
         else if (this.canOptimize() && (drawbins.length > 1.5*maxnummarker))
            step = Math.min(2, Math.round(drawbins.length/maxnummarker));

         for (let n = 0; n < drawbins.length; n += step) {
            pnt = drawbins[n];
            grx = funcs.grx(pnt.x);
            if ((grx > -this.marker_size) && (grx < w + this.marker_size)) {
               gry = funcs.gry(pnt.y);
               if ((gry > -this.marker_size) && (gry < h + this.marker_size)) {
                  path += this.markeratt.create(grx, gry);
                  if (want_tooltip) hints_marker += `M${grx-hsz},${gry-hsz}h${2*hsz}v${2*hsz}h${-2*hsz}z`;
               }
            }
         }

         if (path) {
            draw_g.append('svg:path')
                  .attr('d', path)
                  .call(this.markeratt.func);
            if ((nodes === null) && (this.draw_kind === 'none') && main_block)
               this.draw_kind = (options.Mark === 101) ? 'path' : 'mark';
         }
         if (want_tooltip && hints_marker) {
            draw_g.append('svg:path')
                  .attr('d', hints_marker)
                  .style('fill', 'none')
                  .style('pointer-events', 'visibleFill');
         }
      }
   }

   /** @summary append TGraphQQ part */
   appendQQ(funcs, graph) {
      const xqmin = Math.max(funcs.scale_xmin, graph.fXq1),
            xqmax = Math.min(funcs.scale_xmax, graph.fXq2),
            yqmin = Math.max(funcs.scale_ymin, graph.fYq1),
            yqmax = Math.min(funcs.scale_ymax, graph.fYq2),
            makeLine = (x1, y1, x2, y2) => `M${funcs.grx(x1)},${funcs.gry(y1)}L${funcs.grx(x2)},${funcs.gry(y2)}`,
            yxmin = (graph.fYq2 - graph.fYq1)*(funcs.scale_xmin-graph.fXq1)/(graph.fXq2-graph.fXq1) + graph.fYq1,
            yxmax = (graph.fYq2-graph.fYq1)*(funcs.scale_xmax-graph.fXq1)/(graph.fXq2-graph.fXq1) + graph.fYq1;

      let path2;
      if (yxmin < funcs.scale_ymin) {
         const xymin = (graph.fXq2 - graph.fXq1)*(funcs.scale_ymin-graph.fYq1)/(graph.fYq2-graph.fYq1) + graph.fXq1;
         path2 = makeLine(xymin, funcs.scale_ymin, xqmin, yqmin);
      } else
         path2 = makeLine(funcs.scale_xmin, yxmin, xqmin, yqmin);

      if (yxmax > funcs.scale_ymax) {
         const xymax = (graph.fXq2-graph.fXq1)*(funcs.scale_ymax-graph.fYq1)/(graph.fYq2-graph.fYq1) + graph.fXq1;
         path2 += makeLine(xqmax, yqmax, xymax, funcs.scale_ymax);
      } else
         path2 += makeLine(xqmax, yqmax, funcs.scale_xmax, yxmax);

      const latt1 = this.createAttLine({ style: 1, width: 1, color: kBlack, std: false }),
            latt2 = this.createAttLine({ style: 2, width: 1, color: kBlack, std: false });

      this.draw_g.append('path')
                 .attr('d', makeLine(xqmin, yqmin, xqmax, yqmax))
                 .call(latt1.func)
                 .style('fill', 'none');

      this.draw_g.append('path')
                 .attr('d', path2)
                 .call(latt2.func)
                 .style('fill', 'none');
   }

   drawBins3D(/* fp, graph */) {
      console.log('Load ./hist/TGraphPainter.mjs to draw graph in 3D');
   }

   /** @summary Create necessary histogram draw attributes */
   createGraphDrawAttributes(only_check_auto) {
      const graph = this.getGraph(), o = this.options;
      if (o._pfc > 1 || o._plc > 1 || o._pmc > 1) {
         const pp = this.getPadPainter();
         if (isFunc(pp?.getAutoColor)) {
            const icolor = pp.getAutoColor(graph.$num_graphs);
            this._auto_exec = ''; // can be reused when sending option back to server
            if (o._pfc > 1) { o._pfc = 1; graph.fFillColor = icolor; this._auto_exec += `SetFillColor(${icolor});;`; delete this.fillatt; }
            if (o._plc > 1) { o._plc = 1; graph.fLineColor = icolor; this._auto_exec += `SetLineColor(${icolor});;`; delete this.lineatt; }
            if (o._pmc > 1) { o._pmc = 1; graph.fMarkerColor = icolor; this._auto_exec += `SetMarkerColor(${icolor});;`; delete this.markeratt; }
         }
      }

      if (only_check_auto)
         this.deleteAttr();
      else {
         this.createAttLine({ attr: graph, can_excl: true });
         this.createAttFill({ attr: graph });
      }
   }

   /** @summary draw TGraph */
   drawGraph() {
      const pmain = this.get_main(),
            graph = this.getGraph();
      if (!pmain || !this.options)
         return;

      // special mode for TMultiGraph 3d drawing
      if (this.options.pos3d)
         return this.drawBins3D(pmain, graph);

      const is_gme = Boolean(this.get_gme()),
            funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y),
            w = pmain.getFrameWidth(),
            h = pmain.getFrameHeight();

      this.createG(pmain.pad_layer ? false : this._frame_layer);

      this.createGraphDrawAttributes();

      this.fillatt.used = false; // mark used only when really used

      this.draw_kind = 'none'; // indicate if special svg:g were created for each bin
      this.marker_size = 0; // indicate if markers are drawn
      const draw_g = is_gme ? this.draw_g.append('svg:g') : this.draw_g;

      this.drawBins(funcs, this.options, draw_g, w, h, this.lineatt, this.fillatt, true);

      if (graph._typename === 'TGraphQQ')
         this.appendQQ(funcs, graph);

      if (is_gme) {
         for (let k = 0; k < graph.fNYErrors; ++k) {
            let lineatt = this.lineatt, fillatt = this.fillatt;
            if (this.options.individual_styles) {
               lineatt = this.createAttLine({ attr: graph.fAttLine[k], std: false });
               fillatt = this.createAttFill({ attr: graph.fAttFill[k], std: false });
            }
            const sub_g = this.draw_g.append('svg:g'),
                options = (k < this.options.blocks.length) ? this.options.blocks[k] : this.options;
            this.extractGmeErrors(k);
            this.drawBins(funcs, options, sub_g, w, h, lineatt, fillatt);
         }
         this.extractGmeErrors(0); // ensure that first block kept at the end
      }

      if (!this.isBatchMode()) {
         addMoveHandler(this, this.testEditable());
         assignContextMenu(this, kNoReorder);
      }
   }

   /** @summary Provide tooltip at specified point */
   extractTooltip(pnt) {
      if (!pnt) return null;

      if ((this.draw_kind === 'lines') || (this.draw_kind === 'path') || (this.draw_kind === 'mark'))
         return this.extractTooltipForPath(pnt);

      if (this.draw_kind !== 'nodes') return null;

      const pmain = this.get_main(),
            height = pmain.getFrameHeight(),
            esz = this.error_size,
            isbar1 = (this.options.Bar === 1),
            funcs = isbar1 ? pmain.getGrFuncs(this.options.second_x, this.options.second_y) : null,
            msize = this.marker_size ? Math.round(this.marker_size/2 + 1.5) : 0;
      let findbin = null, best_dist2 = 1e10, best = null;

      this.draw_g.selectAll('.grpoint').each(function() {
         const d = d3_select(this).datum();
         if (d === undefined) return;
         let dist2 = (pnt.x - d.grx1) ** 2;
         if (pnt.nproc === 1) dist2 += (pnt.y - d.gry1) ** 2;
         if (dist2 >= best_dist2) return;

         let rect;

         if (d.error || d.rect || d.marker) {
            rect = { x1: Math.min(-esz, d.grx0, -msize),
                     x2: Math.max(esz, d.grx2, msize),
                     y1: Math.min(-esz, d.gry2, -msize),
                     y2: Math.max(esz, d.gry0, msize) };
         } else if (d.bar) {
             rect = { x1: -d.width/2, x2: d.width/2, y1: 0, y2: height - d.gry1 };

             if (isbar1) {
                const yy0 = funcs.gry(0);
                rect.y1 = (d.gry1 > yy0) ? yy0-d.gry1 : 0;
                rect.y2 = (d.gry1 > yy0) ? 0 : yy0-d.gry1;
             }
          } else
             rect = { x1: -5, x2: 5, y1: -5, y2: 5 };

          const matchx = (pnt.x >= d.grx1 + rect.x1) && (pnt.x <= d.grx1 + rect.x2),
                matchy = (pnt.y >= d.gry1 + rect.y1) && (pnt.y <= d.gry1 + rect.y2);

          if (matchx && (matchy || (pnt.nproc > 1))) {
             best_dist2 = dist2;
             findbin = this;
             best = rect;
             best.exact = /* matchx && */ matchy;
          }
       });

      if (findbin === null) return null;

      const d = d3_select(findbin).datum(),
            gr = this.getGraph(),
            res = { name: gr.fName, title: gr.fTitle,
                    x: d.grx1, y: d.gry1,
                    color1: this.lineatt.color,
                    lines: this.getTooltips(d),
                    rect: best, d3bin: findbin };

       res.user_info = { obj: gr, name: gr.fName, bin: d.indx, cont: d.y, grx: d.grx1, gry: d.gry1 };

      if (this.fillatt?.used && !this.fillatt?.empty())
         res.color2 = this.fillatt.getFillColor();

      if (best.exact) res.exact = true;
      res.menu = res.exact; // activate menu only when exactly locate bin
      res.menu_dist = 3; // distance always fixed
      res.bin = d;
      res.binindx = d.indx;

      return res;
   }

   /** @summary Show tooltip */
   showTooltip(hint) {
      let ttrect = this.draw_g?.selectChild('.tooltip_bin');

      if (!hint || !this.draw_g) {
         ttrect?.remove();
         return;
      }

      if (hint.usepath)
         return this.showTooltipForPath(hint);

      const d = d3_select(hint.d3bin).datum();

      if (ttrect.empty()) {
         ttrect = this.draw_g.append('svg:rect')
                             .attr('class', 'tooltip_bin')
                             .style('pointer-events', 'none')
                             .call(addHighlightStyle);
      }

      hint.changed = ttrect.property('current_bin') !== hint.d3bin;

      if (hint.changed) {
         ttrect.attr('x', d.grx1 + hint.rect.x1)
               .attr('width', hint.rect.x2 - hint.rect.x1)
               .attr('y', d.gry1 + hint.rect.y1)
               .attr('height', hint.rect.y2 - hint.rect.y1)
               .style('opacity', '0.3')
               .property('current_bin', hint.d3bin);
      }
   }

   /** @summary Process tooltip event */
   processTooltipEvent(pnt) {
      const hint = this.extractTooltip(pnt);
      if (!pnt || !pnt.disabled) this.showTooltip(hint);
      return hint;
   }

   /** @summary Find best bin index for specified point */
   findBestBin(pnt) {
      if (!this.bins) return null;

      const islines = (this.draw_kind === 'lines'),
            funcs = this.get_main().getGrFuncs(this.options.second_x, this.options.second_y);
      let bestindx = -1,
          bestbin = null,
          bestdist = 1e10,
          dist, grx, gry, n, bin;

      for (n = 0; n < this.bins.length; ++n) {
         bin = this.bins[n];

         grx = funcs.grx(bin.x);
         gry = funcs.gry(bin.y);

         dist = (pnt.x-grx)**2 + (pnt.y-gry)**2;

         if (dist < bestdist) {
            bestdist = dist;
            bestbin = bin;
            bestindx = n;
         }
      }

      // check last point
      if ((bestdist > 100) && islines) bestbin = null;

      let radius = Math.max(this.lineatt.width + 3, 4);

      if (this.marker_size > 0) radius = Math.max(this.marker_size, radius);

      if (bestbin)
         bestdist = Math.sqrt((pnt.x-funcs.grx(bestbin.x))**2 + (pnt.y-funcs.gry(bestbin.y))**2);

      if (!islines && (bestdist > radius)) bestbin = null;

      if (!bestbin) bestindx = -1;

      const res = { bin: bestbin, indx: bestindx, dist: bestdist, radius: Math.round(radius) };

      if (!bestbin && islines) {
         bestdist = 1e10;

         const IsInside = (x, x1, x2) => ((x1 >= x) && (x >= x2)) || ((x1 <= x) && (x <= x2));

         let bin0 = this.bins[0], grx0 = funcs.grx(bin0.x), gry0, posy;
         for (n = 1; n < this.bins.length; ++n) {
            bin = this.bins[n];
            grx = funcs.grx(bin.x);

            if (IsInside(pnt.x, grx0, grx)) {
               // if inside interval, check Y distance
               gry0 = funcs.gry(bin0.y);
               gry = funcs.gry(bin.y);

               if (Math.abs(grx - grx0) < 1) {
                  // very close x - check only y
                  posy = pnt.y;
                  dist = IsInside(pnt.y, gry0, gry) ? 0 : Math.min(Math.abs(pnt.y-gry0), Math.abs(pnt.y-gry));
               } else {
                  posy = gry0 + (pnt.x - grx0) / (grx - grx0) * (gry - gry0);
                  dist = Math.abs(posy - pnt.y);
               }

               if (dist < bestdist) {
                  bestdist = dist;
                  res.linex = pnt.x;
                  res.liney = posy;
               }
            }

            bin0 = bin;
            grx0 = grx;
         }

         if (bestdist < radius*0.5) {
            res.linedist = bestdist;
            res.closeline = true;
         }
      }

      return res;
   }

   /** @summary Check editable flag for TGraph
     * @desc if arg specified changes or toggles editable flag */
   testEditable(arg) {
      const obj = this.getGraph();
      if (!obj) return false;
      if ((arg === 'toggle') || (arg !== undefined))
         obj.SetBit(kNotEditable, !arg);
      return !obj.TestBit(kNotEditable);
   }

   /** @summary Provide tooltip at specified point for path-based drawing */
   extractTooltipForPath(pnt) {
      if (this.bins === null) return null;

      const best = this.findBestBin(pnt);

      if (!best || (!best.bin && !best.closeline)) return null;

      const islines = (this.draw_kind === 'lines'),
          ismark = (this.draw_kind === 'mark'),
          pmain = this.get_main(),
          funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y),
          gr = this.getGraph(),
          res = { name: gr.fName, title: gr.fTitle,
                  x: best.bin ? funcs.grx(best.bin.x) : best.linex,
                  y: best.bin ? funcs.gry(best.bin.y) : best.liney,
                  color1: this.lineatt.color,
                  lines: this.getTooltips(best.bin),
                  usepath: true };

      res.user_info = { obj: gr, name: gr.fName, bin: 0, cont: 0, grx: res.x, gry: res.y };

      res.ismark = ismark;
      res.islines = islines;

      if (best.closeline) {
         res.menu = res.exact = true;
         res.menu_dist = best.linedist;
      } else if (best.bin) {
         if (this.options.EF && islines) {
            res.gry1 = funcs.gry(best.bin.y - best.bin.eylow);
            res.gry2 = funcs.gry(best.bin.y + best.bin.eyhigh);
         } else
            res.gry1 = res.gry2 = funcs.gry(best.bin.y);


         res.binindx = best.indx;
         res.bin = best.bin;
         res.radius = best.radius;
         res.user_info.bin = best.indx;
         res.user_info.cont = best.bin.y;

         res.exact = (Math.abs(pnt.x - res.x) <= best.radius) &&
            ((Math.abs(pnt.y - res.gry1) <= best.radius) || (Math.abs(pnt.y - res.gry2) <= best.radius));

         res.menu = res.exact;
         res.menu_dist = Math.sqrt((pnt.x-res.x)**2 + Math.min(Math.abs(pnt.y-res.gry1), Math.abs(pnt.y-res.gry2))**2);
      }

      if (this.fillatt?.used && !this.fillatt?.empty())
         res.color2 = this.fillatt.getFillColor();

      if (!islines) {
         res.color1 = this.getColor(gr.fMarkerColor);
         if (!res.color2) res.color2 = res.color1;
      }

      return res;
   }

   /** @summary Show tooltip for path drawing */
   showTooltipForPath(hint) {
      let ttbin = this.draw_g?.selectChild('.tooltip_bin');

      if (!hint?.bin || !this.draw_g) {
         ttbin?.remove();
         return;
      }

      if (ttbin.empty())
         ttbin = this.draw_g.append('svg:g').attr('class', 'tooltip_bin');

      hint.changed = ttbin.property('current_bin') !== hint.bin;

      if (hint.changed) {
         ttbin.selectAll('*').remove(); // first delete all children
         ttbin.property('current_bin', hint.bin);

         if (hint.ismark) {
            ttbin.append('svg:rect')
                 .style('pointer-events', 'none')
                 .call(addHighlightStyle)
                 .style('opacity', '0.3')
                 .attr('x', Math.round(hint.x - hint.radius))
                 .attr('y', Math.round(hint.y - hint.radius))
                 .attr('width', 2*hint.radius)
                 .attr('height', 2*hint.radius);
         } else {
            ttbin.append('svg:circle').attr('cy', Math.round(hint.gry1));
            if (Math.abs(hint.gry1-hint.gry2) > 1)
               ttbin.append('svg:circle').attr('cy', Math.round(hint.gry2));

            const elem = ttbin.selectAll('circle')
                            .attr('r', hint.radius)
                            .attr('cx', Math.round(hint.x));

            if (!hint.islines)
               elem.style('stroke', hint.color1 === 'black' ? 'green' : 'black').style('fill', 'none');
             else {
               if (this.options.Line || this.options.Curve)
                  elem.call(this.lineatt.func);
               else
                  elem.style('stroke', 'black');
               if (this.options.Fill)
                  elem.call(this.fillatt.func);
               else
                  elem.style('fill', 'none');
            }
         }
      }
   }

   /** @summary Check if graph moving is enabled */
   moveEnabled() {
      return this.testEditable();
   }

   /** @summary Start moving of TGraph */
   moveStart(x, y) {
      this.pos_dx = this.pos_dy = 0;
      this.move_funcs = this.get_main().getGrFuncs(this.options.second_x, this.options.second_y);
      const hint = this.extractTooltip({ x, y });
      if (hint && hint.exact && (hint.binindx !== undefined)) {
         this.move_binindx = hint.binindx;
         this.move_bin = hint.bin;
         this.move_x0 = this.move_funcs.grx(this.move_bin.x);
         this.move_y0 = this.move_funcs.gry(this.move_bin.y);
      } else
         delete this.move_binindx;
   }

   /** @summary Perform moving */
   moveDrag(dx, dy) {
      this.pos_dx += dx;
      this.pos_dy += dy;

      if (this.move_binindx === undefined)
         makeTranslate(this.draw_g, this.pos_dx, this.pos_dy);
       else if (this.move_funcs && this.move_bin) {
         this.move_bin.x = this.move_funcs.revertAxis('x', this.move_x0 + this.pos_dx);
         this.move_bin.y = this.move_funcs.revertAxis('y', this.move_y0 + this.pos_dy);
         this.drawGraph();
      }
   }

   /** @summary Complete moving */
   moveEnd(not_changed) {
      const graph = this.getGraph(), last = graph?.fNpoints-1;
      let exec = '';

      const changeBin = bin => {
         exec += `SetPoint(${bin.indx},${bin.x},${bin.y});;`;
         graph.fX[bin.indx] = bin.x;
         graph.fY[bin.indx] = bin.y;
         if ((bin.indx === 0) && this._cutg_lastsame) {
            exec += `SetPoint(${last},${bin.x},${bin.y});;`;
            graph.fX[last] = bin.x;
            graph.fY[last] = bin.y;
         }
      };

      if (this.move_binindx === undefined) {
         this.draw_g.attr('transform', null);

         if (this.move_funcs && this.bins && !not_changed) {
            for (let k = 0; k < this.bins.length; ++k) {
               const bin = this.bins[k];
               bin.x = this.move_funcs.revertAxis('x', this.move_funcs.grx(bin.x) + this.pos_dx);
               bin.y = this.move_funcs.revertAxis('y', this.move_funcs.gry(bin.y) + this.pos_dy);
               changeBin(bin);
            }
            if (graph.$redraw_pad)
               this.redrawPad();
            else
               this.drawGraph();
         }
      } else {
         changeBin(this.move_bin);
         delete this.move_binindx;
         if (graph.$redraw_pad)
            this.redrawPad();
      }

      delete this.move_funcs;

      if (exec && !not_changed)
         this.submitCanvExec(exec);
   }

   /** @summary Fill option object used in TWebCanvas */
   fillWebObjectOptions(res) {
      if (this._auto_exec && res) {
         res.fcust = 'auto_exec:' + this._auto_exec;
         delete this._auto_exec;
      }
   }

   /** @summary Fill context menu */
   fillContextMenuItems(menu) {
      if (!this.snapid) {
         menu.addchk(this.testEditable(), 'Editable', () => { this.testEditable('toggle'); this.drawGraph(); });
         if (this.axes_draw) {
            menu.add('Title', () => menu.input('Enter graph title', this.getObject().fTitle).then(res => {
               this.getObject().fTitle = res;
               const hist_painter = this.getMainPainter();
               if (hist_painter?.isSecondary(this)) {
                  setHistogramTitle(hist_painter.getHisto(), res);
                  this.interactiveRedraw('pad');
               }
            }));
         }
         menu.addRedrawMenu(this.getPrimary());
      }
   }

   /** @summary Execute menu command
     * @private */
   executeMenuCommand(method, args) {
      if (super.executeMenuCommand(method, args))
         return true;

      const canp = this.getCanvPainter(), pmain = this.get_main();

      if ((method.fName === 'RemovePoint') || (method.fName === 'InsertPoint')) {
         if (!canp || canp._readonly) return true; // ignore function

         const pnt = isFunc(pmain?.getLastEventPos) ? pmain.getLastEventPos() : null,
             hint = this.extractTooltip(pnt);

         if (method.fName === 'InsertPoint') {
            if (pnt) {
               const funcs = pmain.getGrFuncs(this.options.second_x, this.options.second_y),
                     userx = funcs.revertAxis('x', pnt.x) ?? 0,
                     usery = funcs.revertAxis('y', pnt.y) ?? 0;
               this.submitCanvExec(`AddPoint(${userx.toFixed(3)}, ${usery.toFixed(3)})`, method.$execid);
            }
         } else if (method.$execid && (hint?.binindx !== undefined))
            this.submitCanvExec(`RemovePoint(${hint.binindx})`, method.$execid);


         return true; // call is processed
      }

      return false;
   }

   /** @summary Update TGraph object members
     * @private */
   _updateMembers(graph, obj) {
      graph.fBits = obj.fBits;
      graph.fTitle = obj.fTitle;
      graph.fX = obj.fX;
      graph.fY = obj.fY;
      ['fEX', 'fEY', 'fExL', 'fExH', 'fEXlow', 'fEXhigh', 'fEYlow', 'fEYhigh',
        'fEXlowd', 'fEXhighd', 'fEYlowd', 'fEYhighd'].forEach(member => {
         if (obj[member] !== undefined)
            graph[member] = obj[member];
      });
      graph.fNpoints = obj.fNpoints;
      graph.fMinimum = obj.fMinimum;
      graph.fMaximum = obj.fMaximum;

      const o = this.options;

      if (this.snapid !== undefined)
         o._pfc = o._plc = o._pmc = 0; // auto colors should be processed in web canvas

      if (!o._pfc)
         graph.fFillColor = obj.fFillColor;
      graph.fFillStyle = obj.fFillStyle;
      if (!o._plc)
         graph.fLineColor = obj.fLineColor;
      graph.fLineStyle = obj.fLineStyle;
      graph.fLineWidth = obj.fLineWidth;
      if (!o._pmc)
         graph.fMarkerColor = obj.fMarkerColor;
      graph.fMarkerSize = obj.fMarkerSize;
      graph.fMarkerStyle = obj.fMarkerStyle;

      return obj.fFunctions;
   }

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

      if (opt && (opt !== this.options.original))
         this.decodeOptions(opt);

      const new_funcs = this._updateMembers(this.getObject(), obj);

      this.createBins();

      delete this.$redraw_hist;

      // if our own histogram was used as axis drawing, we need update histogram as well
      if (this.axes_draw) {
         const histo = this.createHistogram(),
               hist_painter = this.getMainPainter();
         if (hist_painter?.isSecondary(this)) {
            hist_painter.updateObject(histo, this.options.Axis);
            this.$redraw_hist = true;
         }
      }

      this._funcHandler = new FunctionsHandler(this, this.getPadPainter(), new_funcs);

      return true;
   }

   /** @summary Checks if it makes sense to zoom inside specified axis range
     * @desc allow to zoom TGraph only when at least one point in the range */
   canZoomInside(axis, min, max) {
      const gr = this.getGraph();
      if (!gr || ((axis !== 'x') && (axis !== 'y')))
         return false;

      let arr = gr.fX;
      if (this._is_scatter)
         arr = (axis === 'x') ? gr.fX : gr.fY;
      else if (axis !== (this.options.pos3d ? 'y' : 'x'))
         return false;

      for (let n = 0; n < gr.fNpoints; ++n) {
         if ((min < arr[n]) && (arr[n] < max))
            return true;
      }

      return false;
   }

   /** @summary Process click on graph-defined buttons */
   clickButton(funcname) {
      if (funcname !== 'ToggleZoom') return false;

      if ((this.xmin === this.xmax) && (this.ymin === this.ymax)) return false;

      return this.getFramePainter()?.zoom(this.xmin, this.xmax, this.ymin, this.ymax);
   }

   /** @summary Find TF1/TF2 in TGraph list of functions */
   findFunc() {
      return this.getGraph()?.fFunctions?.arr?.find(func => (func._typename === clTF1) || (func._typename === clTF2));
   }

   /** @summary Find stat box in TGraph list of functions */
   findStat() {
      return this.getGraph()?.fFunctions?.arr?.find(func => (func._typename === clTPaveStats) && (func.fName === 'stats'));
   }

   /** @summary Create stat box */
   createStat() {
      const func = this.findFunc();
      if (!func)
         return null;

      let stats = this.findStat();
      if (stats)
         return stats;

      const st = gStyle;

      // do not create stats box when drawing canvas
      if (!st.fOptFit || this.getCanvPainter()?.normal_canvas)
         return null;

      this.create_stats = true;

      stats = create(clTPaveStats);
      Object.assign(stats, { fName: 'stats', fOptStat: 0, fOptFit: st.fOptFit, fBorderSize: 1,
                             fX1NDC: st.fStatX - st.fStatW, fY1NDC: st.fStatY - st.fStatH, fX2NDC: st.fStatX, fY2NDC: st.fStatY,
                             fFillColor: st.fStatColor, fFillStyle: st.fStatStyle });

      stats.fTextAngle = 0;
      stats.fTextSize = st.fStatFontSize; // 9 ??
      stats.fTextAlign = 12;
      stats.fTextColor = st.fStatTextColor;
      stats.fTextFont = st.fStatFont;

      stats.AddText(func.fName);

      // while TF1 was found, one can be sure that stats is existing
      this.getGraph().fFunctions.Add(stats);

      return stats;
   }

   /** @summary Fill statistic */
   fillStatistic(stat, _dostat, dofit) {
      const func = this.findFunc();
      if (!func || !dofit)
         return false;

      stat.clearPave();
      stat.fillFunctionStat(func, (dofit === 1) ? 111 : dofit, 1);
      return true;
   }

   /** @summary Draw axis histogram
     * @private */
   async drawAxisHisto() {
      const need_histo = !this.getHistogram(),
            histo = this.createHistogram(need_histo, need_histo);
      return TH1Painter.draw(this.getDrawDom(), histo, this.options.Axis);
   }

   /** @summary Draw TGraph
     * @private */
   static async _drawGraph(painter, opt) {
      painter.decodeOptions(opt, true);
      painter.createBins();
      painter.createStat();
      const graph = painter.getGraph();
      if (!settings.DragGraphs)
         graph?.SetBit(kNotEditable, true);

      let promise = Promise.resolve();

      if ((!painter.getMainPainter() || painter.options.second_x || painter.options.second_y) && painter.options.Axis) {
         promise = painter.drawAxisHisto().then(hist_painter => {
            hist_painter?.setSecondaryId(painter, 'hist');
            painter.axes_draw = Boolean(hist_painter);
         });
      }

      return promise.then(() => {
         painter.addToPadPrimitives();
         return painter.drawGraph();
      }).then(() => {
         const handler = new FunctionsHandler(painter, painter.getPadPainter(), graph.fFunctions, true);
         return handler.drawNext(0); // returns painter
      });
   }

   /** @summary Draw TGraph in 2D only */
   static async draw(dom, graph, opt) {
      return TGraphPainter._drawGraph(new TGraphPainter(dom, graph), opt);
   }

} // class TGraphPainter

export { clTGraphAsymmErrors, TGraphPainter };