draw/TGaxisPainter.mjs

import { clTF1 } from '../core.mjs';
import { scaleLinear as d3_scaleLinear, scaleLog as d3_scaleLog } from '../d3.mjs';
import { makeTranslate } from '../base/BasePainter.mjs';
import { EAxisBits, TAxisPainter } from '../gpad/TAxisPainter.mjs';
import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs';
import { addMoveHandler } from '../gui/utils.mjs';
import { assignContextMenu, kNoReorder } from '../gui/menu.mjs';
import { getHPainter } from '../gui/display.mjs';
import { proivdeEvalPar } from '../base/func.mjs';


/** @summary Drawing TGaxis
  * @private */
class TGaxisPainter extends TAxisPainter {

   /** @summary Convert TGaxis position into NDC to fix it when frame zoomed */
   convertTo(opt) {
      const gaxis = this.getObject(),
            x1 = this.axisToSvg('x', gaxis.fX1),
            y1 = this.axisToSvg('y', gaxis.fY1),
            x2 = this.axisToSvg('x', gaxis.fX2),
            y2 = this.axisToSvg('y', gaxis.fY2);

      if (opt === 'ndc') {
          const pw = this.getPadPainter().getPadWidth(),
                ph = this.getPadPainter().getPadHeight();

          gaxis.fX1 = x1 / pw;
          gaxis.fX2 = x2 / pw;
          gaxis.fY1 = (ph - y1) / ph;
          gaxis.fY2 = (ph - y2)/ ph;
          this.use_ndc = true;
      } else if (opt === 'frame') {
         const rect = this.getFramePainter().getFrameRect();
         gaxis.fX1 = (x1 - rect.x) / rect.width;
         gaxis.fX2 = (x2 - rect.x) / rect.width;
         gaxis.fY1 = (y1 - rect.y) / rect.height;
         gaxis.fY2 = (y2 - rect.y) / rect.height;
         this.bind_frame = true;
      }
   }

   /** @summary Drag moving handle */
   moveDrag(dx, dy) {
      this.gaxis_x += dx;
      this.gaxis_y += dy;
      makeTranslate(this.getG(), this.gaxis_x, this.gaxis_y);
   }

   /** @summary Drag end handle */
   moveEnd(not_changed) {
      if (not_changed) return;

      const gaxis = this.getObject();

      let fx, fy;
      if (this.bind_frame) {
         const rect = this.getFramePainter().getFrameRect();
         fx = (this.gaxis_x - rect.x) / rect.width;
         fy = (this.gaxis_y - rect.y) / rect.height;
      } else {
         fx = this.svgToAxis('x', this.gaxis_x, this.use_ndc);
         fy = this.svgToAxis('y', this.gaxis_y, this.use_ndc);
      }

      if (this.vertical) {
         gaxis.fX1 = gaxis.fX2 = fx;
         if (this.reverse) {
            gaxis.fY2 = fy + (gaxis.fY2 - gaxis.fY1);
            gaxis.fY1 = fy;
         } else {
            gaxis.fY1 = fy + (gaxis.fY1 - gaxis.fY2);
            gaxis.fY2 = fy;
         }
      } else {
         if (this.reverse) {
            gaxis.fX1 = fx + (gaxis.fX1 - gaxis.fX2);
            gaxis.fX2 = fx;
         } else {
            gaxis.fX2 = fx + (gaxis.fX2 - gaxis.fX1);
            gaxis.fX1 = fx;
         }
         gaxis.fY1 = gaxis.fY2 = fy;
      }

      this.submitAxisExec(`SetX1(${gaxis.fX1});;SetX2(${gaxis.fX2});;SetY1(${gaxis.fY1});;SetY2(${gaxis.fY2})`, true);
   }

   /** @summary Redraw axis, used in standalone mode for TGaxis */
   redraw() {
      const gaxis = this.getObject(),
            min = gaxis.fWmin,
            max = gaxis.fWmax;
      let x1, y1, x2, y2;

      if (this.bind_frame) {
         const rect = this.getFramePainter().getFrameRect();
         x1 = Math.round(rect.x + gaxis.fX1 * rect.width);
         x2 = Math.round(rect.x + gaxis.fX2 * rect.width);
         y1 = Math.round(rect.y + gaxis.fY1 * rect.height);
         y2 = Math.round(rect.y + gaxis.fY2 * rect.height);
      } else {
         x1 = this.axisToSvg('x', gaxis.fX1, this.use_ndc);
         y1 = this.axisToSvg('y', gaxis.fY1, this.use_ndc);
         x2 = this.axisToSvg('x', gaxis.fX2, this.use_ndc);
         y2 = this.axisToSvg('y', gaxis.fY2, this.use_ndc);
      }

      const w = x2 - x1, h = y1 - y2,
            vertical = Math.abs(w) < Math.abs(h);
      let sz = vertical ? h : w, reverse = false;

      if (sz < 0) {
         reverse = true;
         sz = -sz;
         if (vertical)
            y2 = y1;
         else
            x1 = x2;
      }

      this.configureAxis(vertical ? 'yaxis' : 'xaxis', min, max, min, max, vertical, [0, sz], {
         time_scale: gaxis.fChopt.indexOf('t') >= 0,
         log: (gaxis.fChopt.indexOf('G') >= 0) ? 1 : 0,
         reverse,
         swap_side: reverse,
         axis_func: this.axis_func
      });

      this.createG();

      this.gaxis_x = x1;
      this.gaxis_y = y2;

      return this.drawAxis(this.getG(), Math.abs(w), Math.abs(h), makeTranslate(this.gaxis_x, this.gaxis_y) || '').then(() => {
         addMoveHandler(this);
         assignContextMenu(this, kNoReorder);
         return this;
      });
   }

   /** @summary Fill TGaxis context menu items */
   fillContextMenuItems(menu) {
      menu.addTAxisMenu(EAxisBits, this, this.getObject(), '');
   }

   /** @summary Check if there is function for TGaxis can be found */
   async checkFuncion() {
      const gaxis = this.getObject();
      if (!gaxis.fFunctionName) {
         this.axis_func = null;
         return;
      }
      const func = this.getPadPainter()?.findInPrimitives(gaxis.fFunctionName, clTF1);

      let promise = Promise.resolve(func);
      if (!func) {
         const h = getHPainter(),
               item = h?.findItem({ name: gaxis.fFunctionName, check_keys: true });
         if (item) {
            promise = h.getObject({ item }).then(res => {
               return res?.obj?._typename === clTF1 ? res.obj : null;
            });
         }
      }

      return promise.then(f => {
         this.axis_func = f;
         if (f)
            proivdeEvalPar(f);
      });
   }

   /** @summary Create handle for custom function in the axis */
   createFuncHandle(func, logbase, smin, smax) {
      const res = function(v) { return res.toGraph(v); };
      res._func = func;
      res._domain = [smin, smax];
      res._scale = logbase ? d3_scaleLog().base(logbase) : d3_scaleLinear();
      res._scale.domain(res._domain).range([0, 100]);
      res.eval = function(v) {
         try {
            v = res._func.evalPar(v);
         } catch {
            v = 0;
         }
         return Number.isFinite(v) ? v : 0;
      };

      const vmin = res.eval(smin), vmax = res.eval(smax);
      if ((vmin < vmax) === (smin < smax)) {
         res._vmin = vmin;
         res._vk = 1/(vmax - vmin);
      } else if (vmin === vmax) {
         res._vmin = 0;
         res._vk = 1;
      } else {
         res._vmin = vmax;
         res._vk = 1/(vmin - vmax);
      }
      res._range = [0, 100];
      res.range = function(arr) {
         if (arr) {
            res._range = arr;
            return res;
         }
         return res._range;
      };

      res.domain = function() { return res._domain; };

      res.toGraph = function(v) {
         const rel = (res.eval(v) - res._vmin) * res._vk;
         return res._range[0] * (1 - rel) + res._range[1] * rel;
      };

      res.ticks = function(arg) { return res._scale.ticks(arg); };

      return res;
   }

   /** @summary Draw TGaxis object */
   static async draw(dom, obj, opt) {
      const painter = new TGaxisPainter(dom, obj, false);

      return ensureTCanvas(painter, false).then(() => {
         if (opt) painter.convertTo(opt);
         return painter.checkFuncion();
      }).then(() => painter.redraw());
   }

} // class TGaxisPainter

export { TGaxisPainter };