import { gStyle, settings, internals, isFunc, isStr, postponePromise, browser,
clTAxis, clTFrame, kNoZoom, urlClassPrefix } from '../core.mjs';
import { select as d3_select, pointer as d3_pointer,
pointers as d3_pointers, drag as d3_drag, rgb as d3_rgb } from '../d3.mjs';
import { getElementRect, getAbsPosInCanvas, makeTranslate, addHighlightStyle, getBoxDecorations } from '../base/BasePainter.mjs';
import { getActivePad, ObjectPainter, EAxisBits, kAxisLabels } from '../base/ObjectPainter.mjs';
import { getSvgLineStyle } from '../base/TAttLineHandler.mjs';
import { TAxisPainter } from './TAxisPainter.mjs';
import { FontHandler } from '../base/FontHandler.mjs';
import { createMenu, closeMenu, showPainterMenu, hasMenu } from '../gui/menu.mjs';
import { detectRightButton } from '../gui/utils.mjs';
const logminfactorX = 0.0001, logminfactorY = 3e-4;
/** @summary Configure tooltip enable flag for painter
* @private */
function setPainterTooltipEnabled(painter, on) {
if (!painter) return;
const fp = painter.getFramePainter();
if (isFunc(fp?.setTooltipEnabled)) {
fp.setTooltipEnabled(on);
fp.processFrameTooltipEvent(null);
}
// this is 3D control object
if (isFunc(painter.control?.setTooltipEnabled))
painter.control.setTooltipEnabled(on);
}
/** @summary Return pointers on touch event
* @private */
function get_touch_pointers(event, node) {
return event.$touch_arr ?? d3_pointers(event, node);
}
/** @summary Returns coordinates transformation func
* @private */
function getEarthProjectionFunc(id) {
switch (id) {
// Aitoff2xy
case 1: return (l, b) => {
const DegToRad = Math.PI/180,
alpha2 = (l/2)*DegToRad,
delta = b*DegToRad,
r2 = Math.sqrt(2),
f = 2*r2/Math.PI,
cdec = Math.cos(delta),
denom = Math.sqrt(1.0 + cdec*Math.cos(alpha2));
return {
x: cdec*Math.sin(alpha2)*2.0*r2/denom/f/DegToRad,
y: Math.sin(delta)*r2/denom/f/DegToRad
};
};
// mercator
case 2: return (l, b) => { return { x: l, y: Math.log(Math.tan((Math.PI/2 + b/180*Math.PI)/2)) }; };
// sinusoidal
case 3: return (l, b) => { return { x: l*Math.cos(b/180*Math.PI), y: b }; };
// parabolic
case 4: return (l, b) => { return { x: l*(2.0*Math.cos(2*b/180*Math.PI/3) - 1), y: 180*Math.sin(b/180*Math.PI/3) }; };
// Mollweide projection
case 5: return (l, b) => {
const theta0 = b * Math.PI/180;
let theta = theta0, num, den;
for (let i = 0; i < 100; i++) {
num = 2 * theta + Math.sin(2 * theta) - Math.PI * Math.sin(theta0);
den = 4 * (Math.cos(theta)**2);
if (den < 1e-20) {
theta = theta0;
break;
}
theta -= num / den;
if (Math.abs(num / den) < 1e-4) break;
}
return {
x: l * Math.cos(theta),
y: 90 * Math.sin(theta)
};
};
}
}
/** @summary Unzoom preselected range for main histogram painter
* @desc Used with TGraph where Y zooming selected with fMinimum/fMaximum but histogram
* axis range can be wider. Or for normal histogram drawing when preselected range smaller than histogram range
* @private */
function unzoomHistogramYRange(main) {
if (!isFunc(main?.getDimension) || main.getDimension() !== 1) return;
const ymin = main.draw_content ? main.hmin : main.ymin,
ymax = main.draw_content ? main.hmax : main.ymax;
if ((main.zoom_ymin !== main.zoom_ymax) && (ymin !== ymax) &&
(ymin <= main.zoom_ymin) && (main.zoom_ymax <= ymax))
main.zoom_ymin = main.zoom_ymax = 0;
}
// global, allow single drag at once
let drag_rect = null, drag_kind = '', drag_painter = null;
/** @summary Check if dragging performed currently
* @private */
function is_dragging(painter, kind) {
return drag_rect && (drag_painter === painter) && (drag_kind === kind);
}
/** @summary Add drag for interactive rectangular elements for painter
* @private */
function addDragHandler(_painter, arg) {
if (!settings.MoveResize) return;
const painter = _painter, pp = painter.getPadPainter();
if (pp?._fast_drawing || pp?.isBatchMode()) return;
// cleanup all drag elements when canvas is not editable
if (pp?.isEditable() === false)
arg.cleanup = true;
if (!isFunc(arg.getDrawG))
arg.getDrawG = () => painter?.draw_g;
function makeResizeElements(group, handler) {
function addElement(cursor, d) {
const clname = 'js_' + cursor.replace(/[-]/g, '_');
let elem = group.selectChild('.' + clname);
if (arg.cleanup) return elem.remove();
if (elem.empty()) elem = group.append('path').classed(clname, true);
elem.style('opacity', 0).style('cursor', cursor).attr('d', d);
if (handler) elem.call(handler);
}
addElement('nw-resize', 'M2,2h15v-5h-20v20h5Z');
addElement('ne-resize', `M${arg.width-2},2h-15v-5h20v20h-5 Z`);
addElement('sw-resize', `M2,${arg.height-2}h15v5h-20v-20h5Z`);
addElement('se-resize', `M${arg.width-2},${arg.height-2}h-15v5h20v-20h-5Z`);
if (!arg.no_change_x) {
addElement('w-resize', `M-3,18h5v${Math.max(0, arg.height-2*18)}h-5Z`);
addElement('e-resize', `M${arg.width+3},18h-5v${Math.max(0, arg.height-2*18)}h5Z`);
}
if (!arg.no_change_y) {
addElement('n-resize', `M18,-3v5h${Math.max(0, arg.width-2*18)}v-5Z`);
addElement('s-resize', `M18,${arg.height+3}v-5h${Math.max(0, arg.width-2*18)}v5Z`);
}
}
const complete_drag = (newx, newy, newwidth, newheight) => {
drag_painter = null;
drag_kind = '';
if (drag_rect) {
drag_rect.remove();
drag_rect = null;
}
const draw_g = arg.getDrawG();
if (!draw_g)
return false;
const oldx = arg.x, oldy = arg.y;
if (arg.minwidth && newwidth < arg.minwidth) newwidth = arg.minwidth;
if (arg.minheight && newheight < arg.minheight) newheight = arg.minheight;
const change_size = (newwidth !== arg.width) || (newheight !== arg.height),
change_pos = (newx !== oldx) || (newy !== oldy);
arg.x = newx; arg.y = newy; arg.width = newwidth; arg.height = newheight;
if (!arg.no_transform)
makeTranslate(draw_g, newx, newy);
setPainterTooltipEnabled(painter, true);
makeResizeElements(draw_g);
if (change_size || change_pos) {
if (change_size && isFunc(arg.resize))
arg.resize(newwidth, newheight);
if (change_pos && isFunc(arg.move))
arg.move(newx, newy, newx - oldx, newy - oldy);
if (change_size || change_pos) {
if (arg.obj) {
const rect = arg.pad_rect ?? pp.getPadRect();
arg.obj.fX1NDC = newx / rect.width;
arg.obj.fX2NDC = (newx + newwidth) / rect.width;
arg.obj.fY1NDC = 1 - (newy + newheight) / rect.height;
arg.obj.fY2NDC = 1 - newy / rect.height;
arg.obj.$modifiedNDC = true; // indicate that NDC was interactively changed, block in updated
} else if (isFunc(arg.move_resize))
arg.move_resize(newx, newy, newwidth, newheight);
if (isFunc(arg.redraw))
arg.redraw(arg);
}
}
return change_size || change_pos;
},
drag_move = d3_drag().subject(Object),
drag_move_off = d3_drag().subject(Object);
drag_move_off.on('start', null).on('drag', null).on('end', null);
drag_move
.on('start', evnt => {
if (detectRightButton(evnt.sourceEvent) || drag_kind) return;
if (isFunc(arg.is_disabled) && arg.is_disabled('move')) return;
closeMenu(); // close menu
setPainterTooltipEnabled(painter, false); // disable tooltip
evnt.sourceEvent.preventDefault();
evnt.sourceEvent.stopPropagation();
const pad_rect = arg.pad_rect ?? pp.getPadRect(), handle = {
x: arg.x, y: arg.y, width: arg.width, height: arg.height,
acc_x1: arg.x, acc_y1: arg.y,
pad_w: pad_rect.width - arg.width,
pad_h: pad_rect.height - arg.height,
drag_tm: new Date(),
path: `v${arg.height}h${arg.width}v${-arg.height}z`,
evnt_x: evnt.x, evnt_y: evnt.y
};
drag_painter = painter;
drag_kind = 'move';
drag_rect = d3_select(arg.getDrawG().node().parentNode).append('path')
.attr('d', `M${handle.acc_x1},${handle.acc_y1}${handle.path}`)
.style('cursor', 'move')
.style('pointer-events', 'none') // let forward double click to underlying elements
.property('drag_handle', handle)
.call(addHighlightStyle, true);
}).on('drag', evnt => {
if (!is_dragging(painter, 'move'))
return;
evnt.sourceEvent.preventDefault();
evnt.sourceEvent.stopPropagation();
const handle = drag_rect.property('drag_handle');
if (!arg.no_change_x)
handle.acc_x1 += evnt.dx;
if (!arg.no_change_y)
handle.acc_y1 += evnt.dy;
handle.x = Math.min(Math.max(handle.acc_x1, 0), handle.pad_w);
handle.y = Math.min(Math.max(handle.acc_y1, 0), handle.pad_h);
drag_rect.attr('d', `M${handle.x},${handle.y}${handle.path}`);
}).on('end', evnt => {
if (!is_dragging(painter, 'move'))
return;
evnt.sourceEvent.stopPropagation();
evnt.sourceEvent.preventDefault();
const handle = drag_rect.property('drag_handle');
if (complete_drag(handle.x, handle.y, arg.width, arg.height) === false) {
const spent = (new Date()).getTime() - handle.drag_tm.getTime();
if (arg.ctxmenu && (spent > 600))
showPainterMenu({ clientX: handle.evnt_x, clientY: handle.evnt_y, skip_close: 1 }, painter);
else if (arg.canselect && (spent <= 600))
painter.getPadPainter()?.selectObjectPainter(painter);
}
});
const drag_resize = d3_drag().subject(Object);
drag_resize
.on('start', function(evnt) {
if (detectRightButton(evnt.sourceEvent) || drag_kind) return;
if (isFunc(arg.is_disabled) && arg.is_disabled('resize')) return;
closeMenu(); // close menu
setPainterTooltipEnabled(painter, false); // disable tooltip
evnt.sourceEvent.stopPropagation();
evnt.sourceEvent.preventDefault();
const pad_rect = arg.pad_rect ?? pp.getPadRect(), handle = {
x: arg.x, y: arg.y, width: arg.width, height: arg.height,
acc_x1: arg.x, acc_y1: arg.y,
acc_x2: arg.x + arg.width, acc_y2: arg.y + arg.height,
pad_w: pad_rect.width, pad_h: pad_rect.height
};
drag_painter = painter;
drag_kind = 'resize';
drag_rect = d3_select(arg.getDrawG().node().parentNode)
.append('rect')
.style('cursor', d3_select(this).style('cursor'))
.attr('x', handle.acc_x1)
.attr('y', handle.acc_y1)
.attr('width', handle.acc_x2 - handle.acc_x1)
.attr('height', handle.acc_y2 - handle.acc_y1)
.property('drag_handle', handle)
.call(addHighlightStyle, true);
}).on('drag', function(evnt) {
if (!is_dragging(painter, 'resize')) return;
evnt.sourceEvent.preventDefault();
evnt.sourceEvent.stopPropagation();
const handle = drag_rect.property('drag_handle'),
elem = d3_select(this);
let dx = evnt.dx, dy = evnt.dy;
if (arg.no_change_x) dx = 0;
if (arg.no_change_y) dy = 0;
if (elem.classed('js_nw_resize')) {
handle.acc_x1 += dx; handle.acc_y1 += dy;
} else if (elem.classed('js_ne_resize')) {
handle.acc_x2 += dx; handle.acc_y1 += dy;
} else if (elem.classed('js_sw_resize')) {
handle.acc_x1 += dx; handle.acc_y2 += dy;
} else if (elem.classed('js_se_resize')) {
handle.acc_x2 += dx; handle.acc_y2 += dy;
} else if (elem.classed('js_w_resize'))
handle.acc_x1 += dx;
else if (elem.classed('js_n_resize'))
handle.acc_y1 += dy;
else if (elem.classed('js_e_resize'))
handle.acc_x2 += dx;
else if (elem.classed('js_s_resize'))
handle.acc_y2 += dy;
const x1 = Math.max(0, handle.acc_x1), x2 = Math.min(handle.acc_x2, handle.pad_w),
y1 = Math.max(0, handle.acc_y1), y2 = Math.min(handle.acc_y2, handle.pad_h);
handle.x = Math.min(x1, x2);
handle.y = Math.min(y1, y2);
handle.width = Math.abs(x2 - x1);
handle.height = Math.abs(y2 - y1);
drag_rect.attr('x', handle.x).attr('y', handle.y).attr('width', handle.width).attr('height', handle.height);
}).on('end', evnt => {
if (!is_dragging(painter, 'resize'))
return;
evnt.sourceEvent.preventDefault();
const handle = drag_rect.property('drag_handle');
complete_drag(handle.x, handle.y, handle.width, handle.height);
});
if (!arg.only_resize)
arg.getDrawG().style('cursor', arg.cleanup ? null : 'move').call(arg.cleanup ? drag_move_off : drag_move);
if (!arg.only_move)
makeResizeElements(arg.getDrawG(), drag_resize);
}
const TooltipHandler = {
/** @desc only canvas info_layer can be used while other pads can overlay
* @return layer where frame tooltips are shown */
hints_layer() {
return this.getCanvPainter()?.getLayerSvg('info_layer') ?? d3_select(null);
},
/** @return true if tooltip is shown, use to prevent some other action */
isTooltipShown() {
if (!this.tooltip_enabled || !this.isTooltipAllowed())
return false;
const hintsg = this.hints_layer().selectChild('.objects_hints');
return hintsg.empty() ? false : hintsg.property('hints_pad') === this.getPadName();
},
/** @summary set tooltips enabled on/off */
setTooltipEnabled(enabled) {
if (enabled !== undefined)
this.tooltip_enabled = enabled;
},
/** @summary central function which let show selected hints for the object */
processFrameTooltipEvent(pnt, evnt) {
if (pnt?.handler) {
// special use of interactive handler in the frame painter
const rect = this.draw_g?.selectChild('.main_layer');
if (!rect || rect.empty())
pnt = null; // disable
else if (pnt.touch && evnt) {
const pos = get_touch_pointers(evnt, rect.node());
pnt = (pos && pos.length === 1) ? { touch: true, x: pos[0][0], y: pos[0][1] } : null;
} else if (evnt) {
const pos = d3_pointer(evnt, rect.node());
pnt = { touch: false, x: pos[0], y: pos[1] };
}
}
let nhints = 0, nexact = 0, maxlen = 0, lastcolor1 = 0, usecolor1 = false;
const hmargin = 3, wmargin = 3, hstep = 1.2,
frame_rect = this.getFrameRect(),
pp = this.getPadPainter(),
pad_width = pp?.getPadWidth(),
scale = pp?.getPadScale() ?? 1,
textheight = (pnt?.touch ? 15 : 11) * scale,
font = new FontHandler(160, textheight),
disable_tootlips = !this.isTooltipAllowed() || !this.tooltip_enabled;
if (pnt) {
pnt.disabled = disable_tootlips; // indicate that highlighting is not required
pnt.painters = true; // get also painter
}
// collect tooltips from pad painter - it has list of all drawn objects
const hints = pp?.processPadTooltipEvent(pnt) ?? [];
if (pnt && frame_rect)
pp.deliverWebCanvasEvent('move', frame_rect.x + pnt.x, frame_rect.y + pnt.y, hints ? hints[0]?.painter?.snapid : '');
for (let n = 0; n < hints.length; ++n) {
const hint = hints[n];
if (!hint) continue;
if (hint.user_info !== undefined)
hint.painter?.provideUserTooltip(hint.user_info);
if (!hint.lines || (hint.lines.length === 0)) {
hints[n] = null;
continue;
}
// check if fully duplicated hint already exists
for (let k = 0; k < n; ++k) {
const hprev = hints[k];
let diff = false;
if (!hprev || (hprev.lines.length !== hint.lines.length)) continue;
for (let l = 0; l < hint.lines.length && !diff; ++l)
if (hprev.lines[l] !== hint.lines[l]) diff = true;
if (!diff) { hints[n] = null; break; }
}
if (!hints[n]) continue;
nhints++;
if (hint.exact) nexact++;
hint.lines.forEach(line => { maxlen = Math.max(maxlen, line.length); });
hint.height = Math.round(hint.lines.length * textheight * hstep + 2 * hmargin - textheight * (hstep - 1));
if ((hint.color1 !== undefined) && (hint.color1 !== 'none')) {
if ((lastcolor1 !== 0) && (lastcolor1 !== hint.color1))
usecolor1 = true;
lastcolor1 = hint.color1;
}
}
let path_name = null, same_path = hints.length > 1;
for (let n = 0; n < hints.length; ++n) {
const hint = hints[n], p = hint?.lines ? hint.lines[0]?.lastIndexOf('/') : -1;
if (p > 0) {
const path = hint.lines[0].slice(0, p + 1);
if (path_name === null)
path_name = path;
else if (path_name !== path)
same_path = false;
} else
same_path = false;
}
const layer = this.hints_layer(),
show_only_best = nhints > 15,
coordinates = pnt ? Math.round(pnt.x) + ',' + Math.round(pnt.y) : '';
let hintsg = layer.selectChild('.objects_hints'), // group with all tooltips
title = '', name = '', info = '',
hint0 = null, best_dist2 = 1e10, best_hint = null;
// try to select hint with exact match of the position when several hints available
for (let k = 0; k < hints.length; ++k) {
if (!hints[k])
continue;
if (!hint0)
hint0 = hints[k];
// select exact hint if this is the only one
if (hints[k].exact && (nexact < 2) && (!hint0 || !hint0.exact)) {
hint0 = hints[k];
break;
}
if (!pnt || (hints[k].x === undefined) || (hints[k].y === undefined))
continue;
const dist2 = (pnt.x - hints[k].x) ** 2 + (pnt.y - hints[k].y) ** 2;
if (dist2 < best_dist2) { best_dist2 = dist2; best_hint = hints[k]; }
}
if ((!hint0 || !hint0.exact) && (best_dist2 < 400))
hint0 = best_hint;
if (hint0) {
name = (hint0.lines && hint0.lines.length > 1) ? hint0.lines[0] : hint0.name;
title = hint0.title || '';
info = hint0.line;
if (!info && hint0.lines)
info = hint0.lines.slice(1).join(' ');
}
this.showObjectStatus(name, title, info, coordinates);
// end of closing tooltips
if (!pnt || disable_tootlips || (hints.length === 0) || (maxlen === 0) || (show_only_best && !best_hint)) {
hintsg.remove();
return;
}
// we need to set pointer-events=none for all elements while hints
// placed in front of so-called interactive rect in frame, used to catch mouse events
if (hintsg.empty()) {
hintsg = layer.append('svg:g')
.attr('class', 'objects_hints')
.style('pointer-events', 'none');
}
let frame_shift = { x: 0, y: 0 }, trans = frame_rect.transform || '';
if (!pp.iscan) {
frame_shift = getAbsPosInCanvas(this.getPadSvg(), frame_shift);
trans = `translate(${frame_shift.x},${frame_shift.y}) ${trans}`;
}
// copy transform attributes from frame itself
hintsg.attr('transform', trans)
.property('last_point', pnt)
.property('hints_pad', this.getPadName());
let viewmode = hintsg.property('viewmode') || '',
actualw = 0, posx = pnt.x + frame_rect.hint_delta_x;
if (show_only_best || (nhints === 1)) {
viewmode = 'single';
posx += 15;
} else {
// if there are many hints, place them left or right
let bleft = 0.5, bright = 0.5;
if (viewmode === 'left')
bright = 0.7;
else if (viewmode === 'right')
bleft = 0.3;
if (posx <= bleft * frame_rect.width) {
viewmode = 'left';
posx = 20;
} else if (posx >= bright * frame_rect.width) {
viewmode = 'right';
posx = frame_rect.width - 60;
} else
posx = hintsg.property('startx');
}
if (viewmode !== hintsg.property('viewmode')) {
hintsg.property('viewmode', viewmode);
hintsg.selectAll('*').remove();
}
let curry = 10, // normal y coordinate
gapy = 10, // y coordinate, taking into account all gaps
gapminx = -1111, gapmaxx = -1111;
const minhinty = -frame_shift.y,
cp = this.getCanvPainter(),
maxhinty = cp.getPadHeight() - frame_rect.y - frame_shift.y;
for (let n = 0; n < hints.length; ++n) {
let hint = hints[n],
group = hintsg.selectChild(`.painter_hint_${n}`);
if (show_only_best && (hint !== best_hint))
hint = null;
if (hint === null) {
group.remove();
continue;
}
const was_empty = group.empty();
if (was_empty) {
group = hintsg.append('svg:svg')
.attr('class', `painter_hint_${n}`)
.attr('opacity', 0) // use attribute, not style to make animation with d3.transition()
.style('overflow', 'hidden')
.style('pointer-events', 'none');
}
if (viewmode === 'single')
curry = pnt.touch ? (pnt.y - hint.height - 5) : Math.min(pnt.y + 15, maxhinty - hint.height - 3) + frame_rect.hint_delta_y;
else {
for (let n2 = 0; (n2 < hints.length) && (gapy < maxhinty); ++n2) {
const hint2 = hints[n2];
if (!hint2)
continue;
if ((hint2.y >= gapy - 5) && (hint2.y <= gapy + hint2.height + 5)) {
gapy = hint2.y + 10;
n2 = -1;
}
}
if ((gapminx === -1111) && (gapmaxx === -1111))
gapminx = gapmaxx = hint.x;
gapminx = Math.min(gapminx, hint.x);
gapmaxx = Math.min(gapmaxx, hint.x);
}
group.attr('x', posx)
.attr('y', curry)
.property('curry', curry)
.property('gapy', gapy);
curry += hint.height + 5;
gapy += hint.height + 5;
if (!was_empty)
group.selectAll('*').remove();
group.attr('width', 60)
.attr('height', hint.height);
const r = group.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', 60)
.attr('height', hint.height)
.style('fill', 'lightgrey')
.style('pointer-events', 'none');
if (nhints > 1) {
const col = usecolor1 ? hint.color1 : hint.color2;
if (col && (col !== 'none'))
r.style('stroke', col);
}
r.attr('stroke-width', hint.exact ? 3 : 1);
for (let l = 0; l < (hint.lines?.length ?? 0); l++) {
let line = hint.lines[l];
if (l === 0 && path_name && same_path)
line = line.slice(path_name.length);
if (line) {
const txt = group.append('svg:text')
.attr('text-anchor', 'start')
.attr('x', wmargin)
.attr('y', hmargin + l * textheight * hstep)
.attr('dy', '.8em')
.style('fill', 'black')
.style('pointer-events', 'none')
.call(font.func)
.text(line),
box = getElementRect(txt, 'bbox');
actualw = Math.max(actualw, box.width);
}
}
function translateFn() {
// We only use 'd', but list d,i,a as params just to show can have them as params.
// Code only really uses d and t.
return function(/* d, i, a */) {
return function(t) {
return t < 0.8 ? '0' : (t - 0.8) * 5;
};
};
}
if (was_empty) {
if (settings.TooltipAnimation > 0)
group.transition().duration(settings.TooltipAnimation).attrTween('opacity', translateFn());
else
group.attr('opacity', 1);
}
}
actualw += 2 * wmargin;
const svgs = hintsg.selectAll('svg');
if ((viewmode === 'right') && (posx + actualw > frame_rect.width - 20)) {
posx = frame_rect.width - actualw - 20;
svgs.attr('x', posx);
}
if ((viewmode === 'single') && (posx + actualw > pad_width - frame_rect.x) && (posx > actualw + 20)) {
posx -= (actualw + 20);
svgs.attr('x', posx);
}
// if gap not very big, apply gapy coordinate to open view on the histogram
if ((viewmode !== 'single') && (gapy < maxhinty) && (gapy !== curry)) {
if ((gapminx <= posx + actualw + 5) && (gapmaxx >= posx - 5))
svgs.attr('y', function() { return d3_select(this).property('gapy'); });
} else if ((viewmode !== 'single') && (curry > maxhinty)) {
const shift = Math.max((maxhinty - curry - 10), minhinty);
if (shift < 0)
svgs.attr('y', function() { return d3_select(this).property('curry') + shift; });
}
if (actualw > 10)
svgs.attr('width', actualw).select('rect').attr('width', actualw);
hintsg.property('startx', posx);
if (cp._highlight_connect && isFunc(cp.processHighlightConnect))
cp.processHighlightConnect(hints);
},
/** @summary Assigns tooltip methods */
assign(painter) {
Object.assign(painter, this, { tooltip_enabled: true });
}
}, // TooltipHandler
/** @summary Set of frame interactivity methods
* @private */
FrameInteractive = {
/** @summary Adding basic interactivity */
addBasicInteractivity() {
TooltipHandler.assign(this);
if (!this._frame_rotate && !this._frame_fixpos) {
addDragHandler(this, { obj: this, x: this._frame_x, y: this._frame_y, width: this.getFrameWidth(), height: this.getFrameHeight(),
is_disabled: kind => { return (kind === 'move') && this.mode3d; },
only_resize: true, minwidth: 20, minheight: 20, redraw: () => this.sizeChanged() });
}
const top_rect = this.draw_g.selectChild('path'),
main_svg = this.draw_g.selectChild('.main_layer');
top_rect.style('pointer-events', 'visibleFill') // let process mouse events inside frame
.style('cursor', 'default'); // show normal cursor
main_svg.style('pointer-events', 'visibleFill')
.style('cursor', 'default')
.property('handlers_set', 0);
const pp = this.getPadPainter(),
handlers_set = pp?._fast_drawing ? 0 : 1;
if (main_svg.property('handlers_set') !== handlers_set) {
const close_handler = handlers_set ? this.processFrameTooltipEvent.bind(this, null) : null,
mouse_handler = handlers_set ? this.processFrameTooltipEvent.bind(this, { handler: true, touch: false }) : null;
main_svg.property('handlers_set', handlers_set)
.on('mouseenter', mouse_handler)
.on('mousemove', mouse_handler)
.on('mouseleave', close_handler);
if (browser.touches) {
const touch_handler = handlers_set ? this.processFrameTooltipEvent.bind(this, { handler: true, touch: true }) : null;
main_svg.on('touchstart', touch_handler)
.on('touchmove', touch_handler)
.on('touchend', close_handler)
.on('touchcancel', close_handler);
}
}
main_svg.attr('x', 0)
.attr('y', 0)
.attr('width', this.getFrameWidth())
.attr('height', this.getFrameHeight());
const hintsg = this.hints_layer().selectChild('.objects_hints');
// if tooltips were visible before, try to reconstruct them after short timeout
if (!hintsg.empty() && this.isTooltipAllowed() && (hintsg.property('hints_pad') === this.getPadName()))
setTimeout(this.processFrameTooltipEvent.bind(this, hintsg.property('last_point'), null), 10);
},
/** @summary Add interactive handlers */
async addFrameInteractivity(for_second_axes) {
const pp = this.getPadPainter(),
svg = this.getFrameSvg();
if (pp?._fast_drawing || svg.empty())
return this;
if (for_second_axes) {
// add extra handlers for second axes
const svg_x2 = svg.selectAll('.x2axis_container'),
svg_y2 = svg.selectAll('.y2axis_container');
if (settings.ContextMenu) {
svg_x2.on('contextmenu', evnt => this.showContextMenu('x2', evnt));
svg_y2.on('contextmenu', evnt => this.showContextMenu('y2', evnt));
}
svg_x2.on('mousemove', evnt => this.showAxisStatus('x2', evnt));
svg_y2.on('mousemove', evnt => this.showAxisStatus('y2', evnt));
return this;
}
const svg_x = svg.selectAll('.xaxis_container'),
svg_y = svg.selectAll('.yaxis_container');
this.can_zoom_x = this.can_zoom_y = settings.Zooming;
if (pp?.options) {
if (pp.options.NoZoomX) this.can_zoom_x = false;
if (pp.options.NoZoomY) this.can_zoom_y = false;
}
if (!svg.property('interactive_set')) {
this.addFrameKeysHandler();
this.zoom_kind = 0; // 0 - none, 1 - XY, 2 - only X, 3 - only Y, (+100 for touches)
this.zoom_rect = null;
this.zoom_origin = null; // original point where zooming started
this.zoom_curr = null; // current point for zooming
}
if (settings.Zooming) {
if (settings.ZoomMouse) {
svg.on('mousedown', evnt => this.startRectSel(evnt));
svg.on('dblclick', evnt => this.mouseDoubleClick(evnt));
}
if (settings.ZoomWheel)
svg.on('wheel', evnt => this.mouseWheel(evnt));
}
if (browser.touches && ((settings.Zooming && settings.ZoomTouch) || settings.ContextMenu))
svg.on('touchstart', evnt => this.startTouchZoom(evnt));
if (settings.ContextMenu) {
if (browser.touches) {
svg_x.on('touchstart', evnt => this.startSingleTouchHandling('x', evnt));
svg_y.on('touchstart', evnt => this.startSingleTouchHandling('y', evnt));
}
svg.on('contextmenu', evnt => this.showContextMenu('', evnt));
svg_x.on('contextmenu', evnt => this.showContextMenu('x', evnt));
svg_y.on('contextmenu', evnt => this.showContextMenu('y', evnt));
}
svg_x.on('mousemove', evnt => this.showAxisStatus('x', evnt));
svg_y.on('mousemove', evnt => this.showAxisStatus('y', evnt));
svg.property('interactive_set', true);
return this;
},
/** @summary Add keys handler */
addFrameKeysHandler() {
if (this.keys_handler || (typeof window === 'undefined')) return;
this.keys_handler = evnt => this.processKeyPress(evnt);
window.addEventListener('keydown', this.keys_handler, false);
},
/** @summary Handle key press */
processKeyPress(evnt) {
// no custom keys handling when menu is present
if (hasMenu())
return true;
const allowed = ['PageUp', 'PageDown', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'PrintScreen', 'Escape', '*'],
main = this.selectDom(),
pp = this.getPadPainter();
let key = evnt.key;
if (!settings.HandleKeys || main.empty() || (this.enabledKeys === false) ||
(getActivePad() !== pp) || (allowed.indexOf(key) < 0)) return false;
if (evnt.shiftKey) key = `Shift ${key}`;
if (evnt.altKey) key = `Alt ${key}`;
if (evnt.ctrlKey) key = `Ctrl ${key}`;
const zoom = { name: 'x', dleft: 0, dright: 0 };
switch (key) {
case 'ArrowLeft': zoom.dleft = -1; zoom.dright = 1; break;
case 'ArrowRight': zoom.dleft = 1; zoom.dright = -1; break;
case 'Ctrl ArrowLeft': zoom.dleft = zoom.dright = -1; break;
case 'Ctrl ArrowRight': zoom.dleft = zoom.dright = 1; break;
case 'ArrowUp': zoom.name = 'y'; zoom.dleft = 1; zoom.dright = -1; break;
case 'ArrowDown': zoom.name = 'y'; zoom.dleft = -1; zoom.dright = 1; break;
case 'Ctrl ArrowUp': zoom.name = 'y'; zoom.dleft = zoom.dright = 1; break;
case 'Ctrl ArrowDown': zoom.name = 'y'; zoom.dleft = zoom.dright = -1; break;
case 'Escape': pp?.enlargePad(null, false, true); return true;
}
if (zoom.dleft || zoom.dright) {
if (!settings.Zooming)
return false;
// in 3d mode with orbit control ignore simple arrows
if (this.mode3d && (key.indexOf('Ctrl') !== 0))
return false;
this.analyzeMouseWheelEvent(null, zoom, 0.5);
if (zoom.changed)
this.zoomSingle(zoom.name, zoom.min, zoom.max, true);
evnt.stopPropagation();
evnt.preventDefault();
} else {
const func = pp?.findPadButton(key);
if (func) {
pp.clickPadButton(func);
evnt.stopPropagation();
evnt.preventDefault();
}
}
return true; // just process any key press
},
/** @summary Function called when frame is clicked and object selection can be performed
* @desc such event can be used to select */
processFrameClick(pnt, dblckick) {
const pp = this.getPadPainter();
if (!pp) return;
pnt.painters = true; // provide painters reference in the hints
pnt.disabled = true; // do not invoke graphics
// collect tooltips from pad painter - it has list of all drawn objects
const hints = pp.processPadTooltipEvent(pnt);
let exact = null, res;
for (let k = 0; (k < hints.length) && !exact; ++k) {
if (hints[k] && hints[k].exact)
exact = hints[k];
}
if (exact) {
const handler = dblckick ? this._dblclick_handler : this._click_handler;
if (isFunc(handler))
res = handler(exact.user_info, pnt);
}
if (!dblckick) {
pp.selectObjectPainter(exact ? exact.painter : this,
{ x: pnt.x + (this._frame_x || 0), y: pnt.y + (this._frame_y || 0) });
}
return res;
},
/** @summary Check mouse moving */
shiftMoveHanlder(evnt, pos0) {
if (evnt.buttons === this._shifting_buttons) {
const frame = this.getFrameSvg(),
pos = d3_pointer(evnt, frame.node()),
main_svg = this.draw_g.selectChild('.main_layer'),
dx = pos0[0] - pos[0],
dy = (this.scales_ndim === 1) ? 0 : pos0[1] - pos[1],
w = this.getFrameWidth(), h = this.getFrameHeight();
this._shifting_dx = dx;
this._shifting_dy = dy;
main_svg.attr('viewBox', `${dx} ${dy} ${w} ${h}`);
evnt.preventDefault();
evnt.stopPropagation();
}
},
/** @summary mouse up handler for shifting */
shiftUpHanlder(evnt) {
evnt.preventDefault();
d3_select(window).on('mousemove.shiftHandler', null)
.on('mouseup.shiftHandler', null);
if ((this._shifting_dx !== undefined) && (this._shifting_dy !== undefined))
this.performScalesShift();
},
/** @summary Shift scales on defined positions */
performScalesShift() {
const w = this.getFrameWidth(), h = this.getFrameHeight(),
main_svg = this.draw_g.selectChild('.main_layer'),
gr = this.getGrFuncs(),
xmin = gr.revertAxis('x', this._shifting_dx),
xmax = gr.revertAxis('x', this._shifting_dx + w),
ymin = gr.revertAxis('y', this._shifting_dy + h),
ymax = gr.revertAxis('y', this._shifting_dy);
main_svg.attr('viewBox', `0 0 ${w} ${h}`);
delete this._shifting_dx;
delete this._shifting_dy;
setPainterTooltipEnabled(this, true);
if (this.scales_ndim === 1)
this.zoomSingle('x', xmin, xmax);
else
this.zoom(xmin, xmax, ymin, ymax);
},
/** @summary Start mouse rect zooming */
startRectSel(evnt) {
// ignore when touch selection is activated
if (this.zoom_kind > 100) return;
const frame = this.getFrameSvg(),
pos = d3_pointer(evnt, frame.node());
if ((evnt.buttons === 3) || (evnt.button === 1)) {
this.clearInteractiveElements();
this._shifting_buttons = evnt.buttons;
if (!evnt.$emul) {
d3_select(window).on('mousemove.shiftHandler', evnt2 => this.shiftMoveHanlder(evnt2, pos))
.on('mouseup.shiftHandler', evnt2 => this.shiftUpHanlder(evnt2), true);
}
setPainterTooltipEnabled(this, false);
evnt.preventDefault();
evnt.stopPropagation();
return;
}
// ignore all events from non-left button
if (evnt.button !== 0)
return;
evnt.preventDefault();
this.clearInteractiveElements();
const w = this.getFrameWidth(), h = this.getFrameHeight();
this.zoom_lastpos = pos;
this.zoom_curr = [Math.max(0, Math.min(w, pos[0])), Math.max(0, Math.min(h, pos[1]))];
this.zoom_origin = [0, 0];
this.zoom_second = false;
if ((pos[0] < 0) || (pos[0] > w)) {
this.zoom_second = (pos[0] > w) && this.y2_handle;
this.zoom_kind = 3; // only y
this.zoom_origin[1] = this.zoom_curr[1];
this.zoom_curr[0] = w;
this.zoom_curr[1] += 1;
} else if ((pos[1] < 0) || (pos[1] > h)) {
this.zoom_second = (pos[1] < 0) && this.x2_handle;
this.zoom_kind = 2; // only x
this.zoom_origin[0] = this.zoom_curr[0];
this.zoom_curr[0] += 1;
this.zoom_curr[1] = h;
} else {
this.zoom_kind = 1; // x and y
this.zoom_origin[0] = this.zoom_curr[0];
this.zoom_origin[1] = this.zoom_curr[1];
}
if (!evnt.$emul) {
d3_select(window).on('mousemove.zoomRect', evnt2 => this.moveRectSel(evnt2))
.on('mouseup.zoomRect', evnt2 => this.endRectSel(evnt2), true);
}
this.zoom_rect = null;
// disable tooltips in frame painter
setPainterTooltipEnabled(this, false);
evnt.stopPropagation();
if (this.zoom_kind !== 1)
return postponePromise(() => this.startLabelsMove(), 500);
},
/** @summary Starts labels move */
startLabelsMove() {
if (this.zoom_rect) return;
const handle = (this.zoom_kind === 2) ? this.x_handle : this.y_handle;
if (!isFunc(handle?.processLabelsMove) || !this.zoom_lastpos) return;
if (handle.processLabelsMove('start', this.zoom_lastpos))
this.zoom_labels = handle;
},
/** @summary Process mouse rect zooming */
moveRectSel(evnt) {
if ((this.zoom_kind === 0) || (this.zoom_kind > 100)) return;
evnt.preventDefault();
const m = d3_pointer(evnt, this.getFrameSvg().node());
if (this.zoom_labels)
return this.zoom_labels.processLabelsMove('move', m);
this.zoom_lastpos[0] = m[0];
this.zoom_lastpos[1] = m[1];
m[0] = Math.max(0, Math.min(this.getFrameWidth(), m[0]));
m[1] = Math.max(0, Math.min(this.getFrameHeight(), m[1]));
switch (this.zoom_kind) {
case 1: this.zoom_curr[0] = m[0]; this.zoom_curr[1] = m[1]; break;
case 2: this.zoom_curr[0] = m[0]; break;
case 3: this.zoom_curr[1] = m[1]; break;
}
const x = Math.min(this.zoom_origin[0], this.zoom_curr[0]),
y = Math.min(this.zoom_origin[1], this.zoom_curr[1]),
w = Math.abs(this.zoom_curr[0] - this.zoom_origin[0]),
h = Math.abs(this.zoom_curr[1] - this.zoom_origin[1]);
if (!this.zoom_rect) {
// ignore small changes, can be switching to labels move
if ((this.zoom_kind !== 1) && ((w < 2) || (h < 2))) return;
this.zoom_rect = this.getFrameSvg()
.append('rect')
.style('pointer-events', 'none')
.call(addHighlightStyle, true);
}
this.zoom_rect.attr('x', x).attr('y', y).attr('width', w).attr('height', h);
},
/** @summary Finish mouse rect zooming */
endRectSel(evnt) {
if ((this.zoom_kind === 0) || (this.zoom_kind > 100)) return;
evnt.preventDefault();
if (!evnt.$emul) {
d3_select(window).on('mousemove.zoomRect', null)
.on('mouseup.zoomRect', null);
}
const m = d3_pointer(evnt, this.getFrameSvg().node());
let kind = this.zoom_kind, pr;
if (this.zoom_labels)
this.zoom_labels.processLabelsMove('stop', m);
else {
const changed = [this.can_zoom_x, this.can_zoom_y];
m[0] = Math.max(0, Math.min(this.getFrameWidth(), m[0]));
m[1] = Math.max(0, Math.min(this.getFrameHeight(), m[1]));
switch (this.zoom_kind) {
case 1: this.zoom_curr[0] = m[0]; this.zoom_curr[1] = m[1]; break;
case 2: this.zoom_curr[0] = m[0]; changed[1] = false; break; // only X
case 3: this.zoom_curr[1] = m[1]; changed[0] = false; break; // only Y
}
let xmin, xmax, ymin, ymax, isany = false,
namex = 'x', namey = 'y';
if (changed[0] && (Math.abs(this.zoom_curr[0] - this.zoom_origin[0]) > 5)) {
if (this.zoom_second && (this.zoom_kind === 2))
namex = 'x2';
const v1 = this.revertAxis(namex, this.zoom_origin[0]),
v2 = this.revertAxis(namex, this.zoom_curr[0]);
xmin = Math.min(v1, v2);
xmax = Math.max(v1, v2);
isany = true;
}
if (changed[1] && (Math.abs(this.zoom_curr[1] - this.zoom_origin[1]) > 5)) {
if (this.zoom_second && (this.zoom_kind === 3))
namey = 'y2';
const v1 = this.revertAxis(namey, this.zoom_origin[1]),
v2 = this.revertAxis(namey, this.zoom_curr[1]);
ymin = Math.min(v1, v2);
ymax = Math.max(v1, v2);
isany = true;
}
if (this.swap_xy && !this.zoom_second)
[xmin, xmax, ymin, ymax] = [ymin, ymax, xmin, xmax];
if (namex === 'x2') {
pr = this.zoomSingle(namex, xmin, xmax, true);
kind = 0;
} else if (namey === 'y2') {
pr = this.zoomSingle(namey, ymin, ymax, true);
kind = 0;
} else if (isany) {
pr = this.zoom(xmin, xmax, ymin, ymax, undefined, undefined, true);
kind = 0;
}
}
const pnt = (kind === 1) ? { x: this.zoom_origin[0], y: this.zoom_origin[1] } : null;
this.clearInteractiveElements();
// if no zooming was done, select active object instead
switch (kind) {
case 1:
this.processFrameClick(pnt);
break;
case 2:
this.getPadPainter()?.selectObjectPainter(this.x_handle);
break;
case 3:
this.getPadPainter()?.selectObjectPainter(this.y_handle);
break;
}
// return promise - if any
return pr;
},
/** @summary Handle mouse double click on frame */
mouseDoubleClick(evnt) {
evnt.preventDefault();
const m = d3_pointer(evnt, this.getFrameSvg().node()),
fw = this.getFrameWidth(), fh = this.getFrameHeight();
this.clearInteractiveElements();
const valid_x = (m[0] >= 0) && (m[0] <= fw),
valid_y = (m[1] >= 0) && (m[1] <= fh);
if (valid_x && valid_y && this._dblclick_handler)
if (this.processFrameClick({ x: m[0], y: m[1] }, true)) return;
let kind = (this.can_zoom_x ? 'x' : '') + (this.can_zoom_y ? 'y' : '') + 'z';
if (!valid_x) {
if (!this.can_zoom_y) return;
kind = this.swap_xy ? 'x' : 'y';
if ((m[0] > fw) && this[kind+'2_handle']) kind += '2'; // let unzoom second axis
} else if (!valid_y) {
if (!this.can_zoom_x) return;
kind = this.swap_xy ? 'y' : 'x';
if ((m[1] < 0) && this[kind+'2_handle']) kind += '2'; // let unzoom second axis
}
return this.unzoom(kind).then(changed => {
if (changed) return;
const pp = this.getPadPainter(), rect = this.getFrameRect();
return pp?.selectObjectPainter(pp, { x: m[0] + rect.x, y: m[1] + rect.y, dbl: true });
});
},
/** @summary Start touch zoom */
startTouchZoom(evnt) {
evnt.preventDefault();
evnt.stopPropagation();
// in case when zooming was started, block any other kind of events
// also prevent zooming together with active dragging
if ((this.zoom_kind !== 0) || drag_kind)
return;
const arr = get_touch_pointers(evnt, this.getFrameSvg().node());
// normally double-touch will be handled
// touch with single click used for context menu
if (arr.length === 1) {
// this is touch with single element
const now = new Date().getTime();
let tmdiff = 1e10, dx = 100, dy = 100;
if (this.last_touch_time && this.last_touch_pos) {
tmdiff = now - this.last_touch_time;
dx = Math.abs(arr[0][0] - this.last_touch_pos[0]);
dy = Math.abs(arr[0][1] - this.last_touch_pos[1]);
}
this.last_touch_time = now;
this.last_touch_pos = arr[0];
if ((tmdiff < 500) && (dx < 20) && (dy < 20)) {
this.clearInteractiveElements();
this.unzoom('xyz');
delete this.last_touch_time;
} else if (settings.ContextMenu)
this.startSingleTouchHandling('', evnt);
}
if ((arr.length !== 2) || !settings.Zooming || !settings.ZoomTouch)
return;
this.clearInteractiveElements();
// clear single touch handler
this.endSingleTouchHandling(null);
const pnt1 = arr[0], pnt2 = arr[1], w = this.getFrameWidth(), h = this.getFrameHeight();
this.zoom_curr = [Math.min(pnt1[0], pnt2[0]), Math.min(pnt1[1], pnt2[1])];
this.zoom_origin = [Math.max(pnt1[0], pnt2[0]), Math.max(pnt1[1], pnt2[1])];
this.zoom_second = false;
if ((this.zoom_curr[0] < 0) || (this.zoom_curr[0] > w)) {
this.zoom_second = (this.zoom_curr[0] > w) && this.y2_handle;
this.zoom_kind = 103; // only y
this.zoom_curr[0] = 0;
this.zoom_origin[0] = w;
} else if ((this.zoom_origin[1] > h) || (this.zoom_origin[1] < 0)) {
this.zoom_second = (this.zoom_origin[1] < 0) && this.x2_handle;
this.zoom_kind = 102; // only x
this.zoom_curr[1] = 0;
this.zoom_origin[1] = h;
} else
this.zoom_kind = 101; // x and y
drag_kind = 'zoom'; // block other possible dragging
setPainterTooltipEnabled(this, false);
this.zoom_rect = this.getFrameSvg().append('rect')
.attr('id', 'zoomRect')
.attr('x', this.zoom_curr[0])
.attr('y', this.zoom_curr[1])
.attr('width', this.zoom_origin[0] - this.zoom_curr[0])
.attr('height', this.zoom_origin[1] - this.zoom_curr[1])
.call(addHighlightStyle, true);
if (!evnt.$emul) {
d3_select(window).on('touchmove.zoomRect', evnt2 => this.moveTouchZoom(evnt2))
.on('touchcancel.zoomRect', evnt2 => this.endTouchZoom(evnt2))
.on('touchend.zoomRect', evnt2 => this.endTouchZoom(evnt2));
}
},
/** @summary Move touch zooming */
moveTouchZoom(evnt) {
if (this.zoom_kind < 100) return;
evnt.preventDefault();
const arr = get_touch_pointers(evnt, this.getFrameSvg().node());
if (arr.length !== 2)
return this.clearInteractiveElements();
const pnt1 = arr[0], pnt2 = arr[1];
if (this.zoom_kind !== 103) {
this.zoom_curr[0] = Math.min(pnt1[0], pnt2[0]);
this.zoom_origin[0] = Math.max(pnt1[0], pnt2[0]);
}
if (this.zoom_kind !== 102) {
this.zoom_curr[1] = Math.min(pnt1[1], pnt2[1]);
this.zoom_origin[1] = Math.max(pnt1[1], pnt2[1]);
}
this.zoom_rect.attr('x', this.zoom_curr[0])
.attr('y', this.zoom_curr[1])
.attr('width', this.zoom_origin[0] - this.zoom_curr[0])
.attr('height', this.zoom_origin[1] - this.zoom_curr[1]);
if ((this.zoom_origin[0] - this.zoom_curr[0] > 10) || (this.zoom_origin[1] - this.zoom_curr[1] > 10))
setPainterTooltipEnabled(this, false);
evnt.stopPropagation();
},
/** @summary End touch zooming handler */
endTouchZoom(evnt) {
if (this.zoom_kind < 100) return;
drag_kind = ''; // reset global flag
evnt.preventDefault();
if (!evnt.$emul) {
d3_select(window).on('touchmove.zoomRect', null)
.on('touchend.zoomRect', null)
.on('touchcancel.zoomRect', null);
}
let xmin, xmax, ymin, ymax, isany = false, namex = 'x', namey = 'y';
const xid = this.swap_xy ? 1 : 0, yid = 1 - xid, changed = [true, true];
if (this.zoom_kind === 102) changed[1] = false;
if (this.zoom_kind === 103) changed[0] = false;
if (changed[xid] && (Math.abs(this.zoom_curr[xid] - this.zoom_origin[xid]) > 10)) {
if (this.zoom_second && (this.zoom_kind === 102)) namex = 'x2';
xmin = Math.min(this.revertAxis(namex, this.zoom_origin[xid]), this.revertAxis(namex, this.zoom_curr[xid]));
xmax = Math.max(this.revertAxis(namex, this.zoom_origin[xid]), this.revertAxis(namex, this.zoom_curr[xid]));
isany = true;
}
if (changed[yid] && (Math.abs(this.zoom_curr[yid] - this.zoom_origin[yid]) > 10)) {
if (this.zoom_second && (this.zoom_kind === 103)) namey = 'y2';
ymin = Math.min(this.revertAxis(namey, this.zoom_origin[yid]), this.revertAxis(namey, this.zoom_curr[yid]));
ymax = Math.max(this.revertAxis(namey, this.zoom_origin[yid]), this.revertAxis(namey, this.zoom_curr[yid]));
isany = true;
}
this.clearInteractiveElements();
delete this.last_touch_time;
if (namex === 'x2')
this.zoomSingle(namex, xmin, xmax, true);
else if (namey === 'y2')
this.zoomSingle(namey, ymin, ymax, true);
else if (isany)
this.zoom(xmin, xmax, ymin, ymax, undefined, undefined, true);
evnt.stopPropagation();
},
/** @summary Analyze zooming with mouse wheel */
analyzeMouseWheelEvent(event, item, dmin, test_ignore, second_side) {
// if there is second handle, use it
const handle2 = second_side ? this[item.name + '2_handle'] : null;
if (handle2) {
item.second = Object.assign({}, item);
return handle2.analyzeWheelEvent(event, dmin, item.second, test_ignore);
}
const handle = this[item.name + '_handle'];
return handle?.analyzeWheelEvent(event, dmin, item, test_ignore);
},
/** @summary return true if default Y zooming should be enabled
* @desc it is typically for 2-Dim histograms or
* when histogram not draw, defined by other painters */
isAllowedDefaultYZooming() {
if (this.self_drawaxes) return true;
const pad_painter = this.getPadPainter();
if (pad_painter?.painters) {
for (let k = 0; k < pad_painter.painters.length; ++k) {
const subpainter = pad_painter.painters[k];
if (subpainter?.wheel_zoomy !== undefined)
return subpainter.wheel_zoomy;
}
}
return false;
},
/** @summary Handles mouse wheel event */
mouseWheel(evnt) {
evnt.stopPropagation();
evnt.preventDefault();
this.clearInteractiveElements();
const itemx = { name: 'x', reverse: this.reverse_x },
itemy = { name: 'y', reverse: this.reverse_y, ignore: !this.isAllowedDefaultYZooming() },
cur = d3_pointer(evnt, this.getFrameSvg().node()),
w = this.getFrameWidth(), h = this.getFrameHeight();
if (this.can_zoom_x)
this.analyzeMouseWheelEvent(evnt, this.swap_xy ? itemy : itemx, cur[0] / w, (cur[1] >= 0) && (cur[1] <= h), cur[1] < 0);
if (this.can_zoom_y)
this.analyzeMouseWheelEvent(evnt, this.swap_xy ? itemx : itemy, 1 - cur[1] / h, (cur[0] >= 0) && (cur[0] <= w), cur[0] > w);
let pr = this.zoom(itemx.min, itemx.max, itemy.min, itemy.max, undefined, undefined, itemx.changed || itemy.changed);
if (itemx.second)
pr = pr.then(() => this.zoomSingle('x2', itemx.second.min, itemx.second.max, itemx.second.changed));
if (itemy.second)
pr = pr.then(() => this.zoomSingle('y2', itemy.second.min, itemy.second.max, itemy.second.changed));
return pr;
},
/** @summary Show frame context menu */
showContextMenu(kind, evnt, obj) {
// disable context menu left/right buttons clicked
if (evnt?.buttons === 3)
return evnt.preventDefault();
// ignore context menu when touches zooming is ongoing or
if (('zoom_kind' in this) && (this.zoom_kind > 100)) return;
let pnt, menu_painter = this, exec_painter = null,
frame_corner = false, fp = null; // object used to show context menu
const svg_node = this.getFrameSvg().node();
if (isFunc(evnt?.stopPropagation)) {
evnt.preventDefault();
evnt.stopPropagation(); // disable main context menu
const ms = d3_pointer(evnt, svg_node),
tch = get_touch_pointers(evnt, svg_node);
if (tch.length === 1)
pnt = { x: tch[0][0], y: tch[0][1], touch: true };
else if (ms.length === 2)
pnt = { x: ms[0], y: ms[1], touch: false };
} else if ((evnt?.x !== undefined) && (evnt?.y !== undefined) && (evnt?.clientX === undefined)) {
pnt = evnt;
const rect = svg_node.getBoundingClientRect();
evnt = { clientX: rect.left + pnt.x, clientY: rect.top + pnt.y };
}
if ((kind === 'painter') && obj) {
menu_painter = obj;
kind = '';
} else if (kind === 'main') {
menu_painter = this.getMainPainter(true);
kind = '';
} else if (!kind) {
const pp = this.getPadPainter();
let sel = null;
fp = this;
if (pnt && pp) {
pnt.painters = true; // assign painter for every tooltip
const hints = pp.processPadTooltipEvent(pnt);
let bestdist = 1000;
for (let n = 0; n < hints.length; ++n) {
if (hints[n]?.menu) {
const dist = hints[n].menu_dist ?? 7;
if (dist < bestdist) { sel = hints[n].painter; bestdist = dist; }
}
}
}
if (sel) menu_painter = sel;
else kind = 'frame';
if (pnt) frame_corner = (pnt.x > 0) && (pnt.x < 20) && (pnt.y > 0) && (pnt.y < 20);
fp.setLastEventPos(pnt);
} else if ((kind === 'x') || (kind === 'y') || (kind === 'z') || (kind === 'pal')) {
exec_painter = this.getMainPainter(true); // histogram painter delivers items for axis menu
if (this.v7_frame && isFunc(exec_painter?.v7EvalAttr))
exec_painter = null;
}
if (!exec_painter) exec_painter = menu_painter;
if (!isFunc(menu_painter?.fillContextMenu)) return;
this.clearInteractiveElements();
return createMenu(evnt, menu_painter).then(menu => {
let domenu = menu.painter.fillContextMenu(menu, kind, obj);
// fill frame menu by default - or append frame elements when activated in the frame corner
if (fp && (!domenu || (frame_corner && (kind !== 'frame'))))
domenu = fp.fillContextMenu(menu);
if (domenu) {
return exec_painter.fillObjectExecMenu(menu, kind).then(menu2 => {
// suppress any running zooming
setPainterTooltipEnabled(menu2.painter, false);
return menu2.show().then(() => setPainterTooltipEnabled(menu2.painter, true));
});
}
});
},
/** @summary Activate touch handling on frame
* @private */
startSingleTouchHandling(kind, evnt) {
const arr = get_touch_pointers(evnt, this.getFrameSvg().node());
if (arr.length !== 1) return;
evnt.preventDefault();
evnt.stopPropagation();
closeMenu();
const tm = new Date().getTime();
this._shifting_dx = 0;
this._shifting_dy = 0;
setPainterTooltipEnabled(this, false);
d3_select(window).on('touchmove.singleTouch', kind ? null : evnt2 => this.moveTouchHandling(evnt2, kind, arr[0]))
.on('touchcancel.singleTouch', evnt2 => this.endSingleTouchHandling(evnt2, kind, arr[0], tm))
.on('touchend.singleTouch', evnt2 => this.endSingleTouchHandling(evnt2, kind, arr[0], tm));
},
/** @summary Moving of touch pointer
* @private */
moveTouchHandling(evnt, kind, pos0) {
const frame = this.getFrameSvg(),
main_svg = this.draw_g.selectChild('.main_layer');
let pos;
try {
pos = get_touch_pointers(evnt, frame.node())[0];
} catch {
pos = [0, 0];
if (evnt?.changedTouches)
pos = [evnt.changedTouches[0].clientX, evnt.changedTouches[0].clientY];
}
const dx = pos0[0] - pos[0],
dy = (this.scales_ndim === 1) ? 0 : pos0[1] - pos[1],
w = this.getFrameWidth(), h = this.getFrameHeight();
this._shifting_dx = dx;
this._shifting_dy = dy;
main_svg.attr('viewBox', `${dx} ${dy} ${w} ${h}`);
},
/** @summary Process end-touch event, which can cause content menu to appear
* @private */
endSingleTouchHandling(evnt, kind, pos, tm) {
evnt?.preventDefault();
evnt?.stopPropagation();
setPainterTooltipEnabled(this, true);
d3_select(window).on('touchmove.singleTouch', null)
.on('touchcancel.singleTouch', null)
.on('touchend.singleTouch', null);
if (evnt === null) return;
if (Math.abs(this._shifting_dx) > 2 || Math.abs(this._shifting_dy) > 2)
this.performScalesShift();
else if (new Date().getTime() - tm > 700)
this.showContextMenu(kind, { x: pos[0], y: pos[1] });
},
/** @summary Clear frame interactive elements */
clearInteractiveElements() {
closeMenu();
this.zoom_kind = 0;
this.zoom_rect?.remove();
delete this.zoom_rect;
delete this.zoom_curr;
delete this.zoom_origin;
delete this.zoom_lastpos;
delete this.zoom_labels;
// enable tooltip in frame painter
setPainterTooltipEnabled(this, true);
},
/** @summary Assign frame interactive methods */
assign(painter) {
Object.assign(painter, this);
}
}; // FrameInteractive
/**
* @summary Painter class for TFrame, main handler for interactivity
* @private
*/
class TFramePainter extends ObjectPainter {
/** @summary constructor
* @param {object|string} dom - DOM element for drawing or element id
* @param {object} frame - TFrame object */
constructor(dom, frame) {
super(dom, frame?.$dummy ? null : frame);
this.zoom_kind = 0;
this.mode3d = false;
this.shrink_frame_left = 0.0;
this.xmin = this.xmax = 0; // no scale specified, wait for objects drawing
this.ymin = this.ymax = 0; // no scale specified, wait for objects drawing
this.ranges_set = false;
this.axes_drawn = false;
this.axes2_drawn = false;
this.keys_handler = null;
this._borderMode = gStyle.fFrameBorderMode;
this._borderSize = gStyle.fFrameBorderSize;
this.projection = 0; // different projections
}
/** @summary Returns frame painter - object itself */
getFramePainter() { return this; }
/** @summary Returns true if it is ROOT6 frame
* @private */
is_root6() { return true; }
/** @summary Returns frame or sub-objects, used in GED editor */
getObject(place) {
if (place === 'xaxis') return this.xaxis;
if (place === 'yaxis') return this.yaxis;
return super.getObject();
}
/** @summary Set active flag for frame - can block some events
* @private */
setFrameActive(on) {
this.enabledKeys = on && settings.HandleKeys;
// used only in 3D mode where control is used
if (this.control)
this.control.enableKeys = this.enabledKeys;
}
/** @summary Shrink frame size
* @private */
shrinkFrame(shrink_left, shrink_right) {
this.fX1NDC += shrink_left;
this.fX2NDC -= shrink_right;
}
/** @summary Set position of last context menu event */
setLastEventPos(pnt) {
this.fLastEventPnt = pnt;
}
/** @summary Return position of last event
* @private */
getLastEventPos() { return this.fLastEventPnt; }
/** @summary Returns coordinates transformation func */
getProjectionFunc() { return getEarthProjectionFunc(this.projection); }
/** @summary Recalculate frame ranges using specified projection functions */
recalculateRange(Proj, change_x, change_y) {
this.projection = Proj || 0;
if ((this.projection === 2) && ((this.scale_ymin <= -90) || (this.scale_ymax >= 90))) {
console.warn(`Mercator Projection: Latitude out of range ${this.scale_ymin} ${this.scale_ymax}`);
this.projection = 0;
}
const func = this.getProjectionFunc();
if (!func) return;
const pnts = [func(this.scale_xmin, this.scale_ymin),
func(this.scale_xmin, this.scale_ymax),
func(this.scale_xmax, this.scale_ymax),
func(this.scale_xmax, this.scale_ymin)];
if (this.scale_xmin < 0 && this.scale_xmax > 0) {
pnts.push(func(0, this.scale_ymin));
pnts.push(func(0, this.scale_ymax));
}
if (this.scale_ymin < 0 && this.scale_ymax > 0) {
pnts.push(func(this.scale_xmin, 0));
pnts.push(func(this.scale_xmax, 0));
}
this.original_xmin = this.scale_xmin;
this.original_xmax = this.scale_xmax;
this.original_ymin = this.scale_ymin;
this.original_ymax = this.scale_ymax;
if (change_x)
this.scale_xmin = this.scale_xmax = pnts[0].x;
if (change_y)
this.scale_ymin = this.scale_ymax = pnts[0].y;
for (let n = 1; n < pnts.length; ++n) {
if (change_x) {
this.scale_xmin = Math.min(this.scale_xmin, pnts[n].x);
this.scale_xmax = Math.max(this.scale_xmax, pnts[n].x);
}
if (change_y) {
this.scale_ymin = Math.min(this.scale_ymin, pnts[n].y);
this.scale_ymax = Math.max(this.scale_ymax, pnts[n].y);
}
}
}
/** @summary Configure frame axes ranges */
setAxesRanges(xaxis, xmin, xmax, yaxis, ymin, ymax, zaxis, zmin, zmax, hpainter) {
this.ranges_set = true;
this.xaxis = xaxis;
this.xmin = xmin;
this.xmax = xmax;
this.yaxis = yaxis;
this.ymin = ymin;
this.ymax = ymax;
this.zaxis = zaxis;
this.zmin = zmin;
this.zmax = zmax;
if (hpainter?.check_pad_range) {
delete hpainter.check_pad_range;
const ndim = hpainter.getDimension();
this.applyAxisZoom('x');
if (ndim > 1)
this.applyAxisZoom('y');
if (ndim > 2)
this.applyAxisZoom('z');
}
if (hpainter && !hpainter._checked_zooming) {
hpainter._checked_zooming = true;
if (hpainter.options.minimum !== kNoZoom) {
this.zoom_zmin = hpainter.options.minimum;
this.zoom_zmax = this.zmax;
}
if (hpainter.options.maximum !== kNoZoom) {
this.zoom_zmax = hpainter.options.maximum;
if (this.zoom_zmin === undefined) this.zoom_zmin = this.zmin;
}
}
}
/** @summary Configure secondary frame axes ranges */
setAxes2Ranges(second_x, xaxis, xmin, xmax, second_y, yaxis, ymin, ymax) {
if (second_x) {
this.x2axis = xaxis;
this.x2min = xmin;
this.x2max = xmax;
}
if (second_y) {
this.y2axis = yaxis;
this.y2min = ymin;
this.y2max = ymax;
}
}
/** @summary Returns associated axis object */
getAxis(name) {
switch (name) {
case 'x': return this.xaxis;
case 'y': return this.yaxis;
case 'z': return this.zaxis;
case 'x2': return this.x2axis;
case 'y2': return this.y2axis;
}
return null;
}
/** @summary Apply axis zooming from pad user range
* @private */
applyPadUserRange(pad, name) {
if (!pad) return;
// seems to be, not always user range calculated
let umin = pad[`fU${name}min`],
umax = pad[`fU${name}max`],
eps = 1e-7;
if (name === 'x') {
if ((Math.abs(pad.fX1) > eps) || (Math.abs(pad.fX2 - 1) > eps)) {
const dx = pad.fX2 - pad.fX1;
umin = pad.fX1 + dx*pad.fLeftMargin;
umax = pad.fX2 - dx*pad.fRightMargin;
}
} else if ((Math.abs(pad.fY1) > eps) || (Math.abs(pad.fY2 - 1) > eps)) {
const dy = pad.fY2 - pad.fY1;
umin = pad.fY1 + dy*pad.fBottomMargin;
umax = pad.fY2 - dy*pad.fTopMargin;
}
if ((umin >= umax) || (Math.abs(umin) < eps && Math.abs(umax-1) < eps))
return;
if (pad[`fLog${name}`] > 0) {
umin = Math.exp(umin * Math.log(10));
umax = Math.exp(umax * Math.log(10));
}
const aname = !this.swap_xy ? name : (name === 'x' ? 'y' : 'x'),
smin = this[`scale_${aname}min`],
smax = this[`scale_${aname}max`];
eps = (smax - smin) * 1e-7;
if ((Math.abs(umin - smin) > eps) || (Math.abs(umax - smax) > eps)) {
this[`zoom_${aname}min`] = umin;
this[`zoom_${aname}max`] = umax;
}
}
/** @summary Apply zooming from TAxis attributes */
applyAxisZoom(name) {
if (this.zoomChangedInteractive(name)) return;
this[`zoom_${name}min`] = this[`zoom_${name}max`] = 0;
const axis = this.getAxis(name);
if (axis?.TestBit(EAxisBits.kAxisRange)) {
if ((axis.fFirst !== axis.fLast) && ((axis.fFirst > 1) || (axis.fLast < axis.fNbins))) {
this[`zoom_${name}min`] = axis.fFirst > 1 ? axis.GetBinLowEdge(axis.fFirst) : axis.fXmin;
this[`zoom_${name}max`] = axis.fLast < axis.fNbins ? axis.GetBinLowEdge(axis.fLast + 1) : axis.fXmax;
// reset user range for main painter
axis.SetBit(EAxisBits.kAxisRange, false);
axis.fFirst = 1;
axis.fLast = axis.fNbins;
}
}
}
/** @summary Create x,y objects which maps user coordinates into pixels
* @desc While only first painter really need such object, all others just reuse it
* following functions are introduced
* this.GetBin[X/Y] return bin coordinate
* this.[x,y] these are d3.scale objects
* this.gr[x,y] converts root scale into graphical value
* @private */
createXY(opts) {
this.cleanXY(); // remove all previous configurations
if (!opts) opts = { ndim: 1 };
this.swap_xy = opts.swap_xy || false;
this.reverse_x = opts.reverse_x || false;
this.reverse_y = opts.reverse_y || false;
this.logx = this.logy = 0;
const w = this.getFrameWidth(), h = this.getFrameHeight(),
pp = this.getPadPainter(), pad = pp.getRootPad(),
pad_logx = pad.fLogx,
pad_logy = (opts.ndim === 1 ? pad.fLogv : undefined) ?? pad.fLogy;
this.scales_ndim = opts.ndim;
this.scale_xmin = this.xmin;
this.scale_xmax = this.xmax;
this.scale_ymin = this.ymin;
this.scale_ymax = this.ymax;
if (opts.extra_y_space) {
const log_scale = this.swap_xy ? pad_logx : pad_logy;
if (log_scale && (this.scale_ymax > 0))
this.scale_ymax = Math.exp(Math.log(this.scale_ymax)*1.1);
else
this.scale_ymax += (this.scale_ymax - this.scale_ymin)*0.1;
}
if (opts.check_pad_range) {
// take zooming out of pad or axis attributes
this.applyAxisZoom('x');
if (opts.ndim > 1) this.applyAxisZoom('y');
if (opts.ndim > 2) this.applyAxisZoom('z');
// Use configured pad range - only when main histogram drawn with SAME draw option
if (opts.check_pad_range === 'pad_range') {
this.applyPadUserRange(pad, 'x');
this.applyPadUserRange(pad, 'y');
}
}
if ((opts.zoom_xmin !== opts.zoom_xmax) && ((this.zoom_xmin === this.zoom_xmax) || !this.zoomChangedInteractive('x'))) {
this.zoom_xmin = opts.zoom_xmin;
this.zoom_xmax = opts.zoom_xmax;
}
if ((opts.zoom_ymin !== opts.zoom_ymax) && ((this.zoom_ymin === this.zoom_ymax) || !this.zoomChangedInteractive('y'))) {
this.zoom_ymin = opts.zoom_ymin;
this.zoom_ymax = opts.zoom_ymax;
}
let orig_x = true, orig_y = true;
if (this.zoom_xmin !== this.zoom_xmax) {
this.scale_xmin = this.zoom_xmin;
this.scale_xmax = this.zoom_xmax;
orig_x = false;
}
if (this.zoom_ymin !== this.zoom_ymax) {
this.scale_ymin = this.zoom_ymin;
this.scale_ymax = this.zoom_ymax;
orig_y = false;
}
// projection should be assigned
this.recalculateRange(opts.Proj, orig_x, orig_y);
this.x_handle = new TAxisPainter(pp, this.xaxis, true);
this.x_handle.setHistPainter(opts.hist_painter, 'x');
this.x_handle.configureAxis('xaxis', this.xmin, this.xmax, this.scale_xmin, this.scale_xmax, this.swap_xy, this.swap_xy ? [0, h] : [0, w],
{ reverse: this.reverse_x,
log: this.swap_xy ? pad_logy : pad_logx,
ignore_labels: this.x_ignore_labels,
noexp_changed: this.x_noexp_changed,
symlog: this.swap_xy ? opts.symlog_y : opts.symlog_x,
log_min_nz: opts.xmin_nz && (opts.xmin_nz <= this.xmax) ? 0.9*opts.xmin_nz : 0,
logcheckmin: (opts.ndim > 1) || !this.swap_xy,
logminfactor: logminfactorX });
this.x_handle.assignFrameMembers(this, 'x');
this.y_handle = new TAxisPainter(pp, this.yaxis, true);
this.y_handle.setHistPainter(opts.hist_painter, 'y');
this.y_handle.configureAxis('yaxis', this.ymin, this.ymax, this.scale_ymin, this.scale_ymax, !this.swap_xy, this.swap_xy ? [0, w] : [0, h],
{ value_axis: opts.ndim === 1,
reverse: this.reverse_y,
log: this.swap_xy ? pad_logx : pad_logy,
ignore_labels: this.y_ignore_labels,
noexp_changed: this.y_noexp_changed,
symlog: this.swap_xy ? opts.symlog_x : opts.symlog_y,
log_min_nz: opts.ymin_nz && (opts.ymin_nz <= this.ymax) ? 0.5*opts.ymin_nz : 0,
logcheckmin: (opts.ndim > 1) || this.swap_xy,
logminfactor: logminfactorY });
this.y_handle.assignFrameMembers(this, 'y');
this.setRootPadRange(pad);
}
/** @summary Create x,y objects for drawing of second axes
* @private */
createXY2(opts) {
if (!opts) opts = { ndim: this.scales_ndim ?? 1 };
this.reverse_x2 = opts.reverse_x || false;
this.reverse_y2 = opts.reverse_y || false;
this.logx2 = this.logy2 = 0;
const w = this.getFrameWidth(), h = this.getFrameHeight(),
pp = this.getPadPainter(), pad = pp.getRootPad();
if (opts.second_x) {
this.scale_x2min = this.x2min;
this.scale_x2max = this.x2max;
}
if (opts.second_y) {
this.scale_y2min = this.y2min;
this.scale_y2max = this.y2max;
}
if (opts.extra_y_space && opts.second_y) {
const log_scale = this.swap_xy ? pad.fLogx : pad.fLogy;
if (log_scale && (this.scale_y2max > 0))
this.scale_y2max = Math.exp(Math.log(this.scale_y2max)*1.1);
else
this.scale_y2max += (this.scale_y2max - this.scale_y2min)*0.1;
}
if ((this.zoom_x2min !== this.zoom_x2max) && opts.second_x) {
this.scale_x2min = this.zoom_x2min;
this.scale_x2max = this.zoom_x2max;
}
if ((this.zoom_y2min !== this.zoom_y2max) && opts.second_y) {
this.scale_y2min = this.zoom_y2min;
this.scale_y2max = this.zoom_y2max;
}
if (opts.second_x) {
this.x2_handle = new TAxisPainter(pp, this.x2axis, true);
this.x2_handle.setHistPainter(opts.hist_painter, 'x');
this.x2_handle.configureAxis('x2axis', this.x2min, this.x2max, this.scale_x2min, this.scale_x2max, this.swap_xy, this.swap_xy ? [0, h] : [0, w],
{ reverse: this.reverse_x2,
log: this.swap_xy ? pad.fLogy : pad.fLogx,
ignore_labels: this.x2_ignore_labels,
noexp_changed: this.x2_noexp_changed,
logcheckmin: (opts.ndim > 1) || !this.swap_xy,
logminfactor: logminfactorX });
this.x2_handle.assignFrameMembers(this, 'x2');
}
if (opts.second_y) {
this.y2_handle = new TAxisPainter(pp, this.y2axis, true);
this.y2_handle.setHistPainter(opts.hist_painter, 'y');
this.y2_handle.configureAxis('y2axis', this.y2min, this.y2max, this.scale_y2min, this.scale_y2max, !this.swap_xy, this.swap_xy ? [0, w] : [0, h],
{ reverse: this.reverse_y2,
log: this.swap_xy ? pad.fLogx : pad.fLogy,
ignore_labels: this.y2_ignore_labels,
noexp_changed: this.y2_noexp_changed,
logcheckmin: (opts.ndim > 1) || this.swap_xy,
log_min_nz: opts.ymin_nz && (opts.ymin_nz < this.y2max) ? 0.5 * opts.ymin_nz : 0,
logminfactor: logminfactorY });
this.y2_handle.assignFrameMembers(this, 'y2');
}
}
/** @summary Return functions to create x/y points based on coordinates
* @desc In default case returns frame painter itself
* @private */
getGrFuncs(second_x, second_y) {
const use_x2 = second_x && this.grx2,
use_y2 = second_y && this.gry2;
if (!use_x2 && !use_y2)
return this;
return {
use_x2,
grx: use_x2 ? this.grx2 : this.grx,
logx: this.logx,
x_handle: use_x2 ? this.x2_handle : this.x_handle,
scale_xmin: use_x2 ? this.scale_x2min : this.scale_xmin,
scale_xmax: use_x2 ? this.scale_x2max : this.scale_xmax,
use_y2,
gry: use_y2 ? this.gry2 : this.gry,
logy: this.logy,
y_handle: use_y2 ? this.y2_handle : this.y_handle,
scale_ymin: use_y2 ? this.scale_y2min : this.scale_ymin,
scale_ymax: use_y2 ? this.scale_y2max : this.scale_ymax,
swap_xy: this.swap_xy,
fp: this,
revertAxis(name, v) {
if ((name === 'x') && this.use_x2) name = 'x2';
if ((name === 'y') && this.use_y2) name = 'y2';
return this.fp.revertAxis(name, v);
},
axisAsText(name, v) {
if ((name === 'x') && this.use_x2) name = 'x2';
if ((name === 'y') && this.use_y2) name = 'y2';
return this.fp.axisAsText(name, v);
},
getFrameWidth() { return this.fp.getFrameWidth(); },
getFrameHeight() { return this.fp.getFrameHeight(); }
};
}
/** @summary Set selected range back to TPad object
* @private */
setRootPadRange(pad, is3d) {
if (!pad || !this.ranges_set) return;
if (is3d) {
// this is fake values, algorithm should be copied from TView3D class of ROOT
pad.fUxmin = pad.fUymin = -0.9;
pad.fUxmax = pad.fUymax = 0.9;
} else {
pad.fLogx = this.swap_xy ? this.logy : this.logx;
pad.fUxmin = pad.fLogx ? Math.log10(this.scale_xmin) : this.scale_xmin;
pad.fUxmax = pad.fLogx ? Math.log10(this.scale_xmax) : this.scale_xmax;
pad.fLogy = this.swap_xy ? this.logx : this.logy;
pad.fUymin = pad.fLogy ? Math.log10(this.scale_ymin) : this.scale_ymin;
pad.fUymax = pad.fLogy ? Math.log10(this.scale_ymax) : this.scale_ymax;
}
const rx = pad.fUxmax - pad.fUxmin,
ry = pad.fUymax - pad.fUymin;
let mx = 1 - pad.fLeftMargin - pad.fRightMargin,
my = 1 - pad.fBottomMargin - pad.fTopMargin;
if (mx <= 0) mx = 0.01; // to prevent overflow
if (my <= 0) my = 0.01;
pad.fX1 = pad.fUxmin - rx/mx*pad.fLeftMargin;
pad.fX2 = pad.fUxmax + rx/mx*pad.fRightMargin;
pad.fY1 = pad.fUymin - ry/my*pad.fBottomMargin;
pad.fY2 = pad.fUymax + ry/my*pad.fTopMargin;
}
/** @summary Draw axes grids
* @desc Called immediately after axes drawing */
drawGrids(draw_grids) {
const layer = this.getFrameSvg().selectChild('.axis_layer');
layer.selectAll('.xgrid').remove();
layer.selectAll('.ygrid').remove();
const pp = this.getPadPainter(),
pad = pp?.getRootPad(true),
h = this.getFrameHeight(),
w = this.getFrameWidth(),
grid_style = gStyle.fGridStyle;
// add a grid on x axis, if the option is set
if (pad?.fGridx && draw_grids && this.x_handle?.ticks) {
const colid = (gStyle.fGridColor > 0) ? gStyle.fGridColor : (this.getAxis('x')?.fAxisColor ?? 1);
let gridx = '';
this.x_handle.ticks.forEach(pos => {
gridx += this.swap_xy ? `M0,${pos}h${w}` : `M${pos},0v${h}`;
});
layer.append('svg:path')
.attr('class', 'xgrid')
.attr('d', gridx)
.style('stroke', this.getColor(colid) || 'black')
.style('stroke-width', gStyle.fGridWidth)
.style('stroke-dasharray', getSvgLineStyle(grid_style));
}
// add a grid on y axis, if the option is set
if (pad?.fGridy && draw_grids && this.y_handle?.ticks) {
const colid = (gStyle.fGridColor > 0) ? gStyle.fGridColor : (this.getAxis('y')?.fAxisColor ?? 1);
let gridy = '';
this.y_handle.ticks.forEach(pos => {
gridy += this.swap_xy ? `M${pos},0v${h}` : `M0,${pos}h${w}`;
});
layer.append('svg:path')
.attr('class', 'ygrid')
.attr('d', gridy)
.style('stroke', this.getColor(colid) || 'black')
.style('stroke-width', gStyle.fGridWidth)
.style('stroke-dasharray', getSvgLineStyle(grid_style));
}
}
/** @summary Converts 'raw' axis value into text */
axisAsText(axis, value) {
const handle = this[`${axis}_handle`];
if (handle)
return handle.axisAsText(value, settings[axis.toUpperCase() + 'ValuesFormat']);
return value.toPrecision(4);
}
/** @summary Identify if requested axes are drawn
* @desc Checks if x/y axes are drawn. Also if second side is already there */
hasDrawnAxes(second_x, second_y) {
return !second_x && !second_y ? this.axes_drawn : this.axes2_drawn;
}
/** @summary draw axes,
* @return {Promise} which ready when drawing is completed */
async drawAxes(shrink_forbidden, disable_x_draw, disable_y_draw,
AxisPos, has_x_obstacle, has_y_obstacle, enable_grids) {
this.cleanAxesDrawings();
if ((this.xmin === this.xmax) || (this.ymin === this.ymax))
return false;
if (AxisPos === undefined) AxisPos = 0;
const layer = this.getFrameSvg().selectChild('.axis_layer'),
w = this.getFrameWidth(),
h = this.getFrameHeight(),
pp = this.getPadPainter(),
pad = pp.getRootPad(true),
draw_grids = enable_grids && (pad?.fGridx || pad?.fGridy);
this.x_handle.invert_side = (AxisPos >= 10);
this.x_handle.lbls_both_sides = !this.x_handle.invert_side && (pad?.fTickx > 1); // labels on both sides
this.x_handle.has_obstacle = has_x_obstacle;
this.y_handle.invert_side = ((AxisPos % 10) === 1);
this.y_handle.lbls_both_sides = !this.y_handle.invert_side && (pad?.fTicky > 1); // labels on both sides
this.y_handle.has_obstacle = has_y_obstacle;
const draw_horiz = this.swap_xy ? this.y_handle : this.x_handle,
draw_vertical = this.swap_xy ? this.x_handle : this.y_handle;
if ((!disable_x_draw || !disable_y_draw) && pp._fast_drawing)
disable_x_draw = disable_y_draw = true;
let pr = Promise.resolve(true);
if (!disable_x_draw || !disable_y_draw || draw_grids) {
draw_vertical.optionLeft = draw_vertical.invert_side; // text align
const can_adjust_frame = !shrink_forbidden && settings.CanAdjustFrame,
pr1 = draw_horiz.drawAxis(layer, w, h,
draw_horiz.invert_side ? null : `translate(0,${h})`,
pad?.fTickx ? -h : 0, disable_x_draw,
undefined, false, pp.getPadHeight() - h - this.getFrameY()),
pr2 = draw_vertical.drawAxis(layer, w, h,
draw_vertical.invert_side ? `translate(${w})` : null,
pad?.fTicky ? w : 0, disable_y_draw,
draw_vertical.invert_side ? 0 : this._frame_x, can_adjust_frame);
pr = Promise.all([pr1, pr2]).then(() => {
this.drawGrids(draw_grids);
if (!can_adjust_frame) return;
let shrink = 0.0;
const ypos = draw_vertical.position;
if ((-0.2 * w < ypos) && (ypos < 0)) {
shrink = -ypos / w + 0.001;
this.shrink_frame_left += shrink;
} else if ((ypos > 0) && (ypos < 0.3 * w) && (this.shrink_frame_left > 0) && (ypos / w > this.shrink_frame_left)) {
shrink = -this.shrink_frame_left;
this.shrink_frame_left = 0.0;
}
if (!shrink) return;
this.shrinkFrame(shrink, 0);
return this.redraw().then(() => this.drawAxes(true));
});
}
return pr.then(() => {
if (!shrink_forbidden)
this.axes_drawn = true;
return true;
});
}
/** @summary draw second axes (if any) */
drawAxes2(second_x, second_y) {
const layer = this.getFrameSvg().selectChild('.axis_layer'),
w = this.getFrameWidth(),
h = this.getFrameHeight(),
pp = this.getPadPainter(),
pad = pp.getRootPad(true);
if (second_x) {
this.x2_handle.invert_side = true;
this.x2_handle.lbls_both_sides = false;
this.x2_handle.has_obstacle = false;
}
if (second_y) {
this.y2_handle.invert_side = true;
this.y2_handle.lbls_both_sides = false;
}
let draw_horiz = this.swap_xy ? this.y2_handle : this.x2_handle,
draw_vertical = this.swap_xy ? this.x2_handle : this.y2_handle;
if ((draw_horiz || draw_vertical) && pp._fast_drawing)
draw_horiz = draw_vertical = null;
let pr1, pr2;
if (draw_horiz) {
pr1 = draw_horiz.drawAxis(layer, w, h,
draw_horiz.invert_side ? null : `translate(0,${h})`,
pad?.fTickx ? -h : 0, false,
undefined, false);
}
if (draw_vertical) {
draw_vertical.optionLeft = draw_vertical.invert_side;
pr2 = draw_vertical.drawAxis(layer, w, h,
draw_vertical.invert_side ? `translate(${w})` : null,
pad?.fTicky ? w : 0, false,
draw_vertical.invert_side ? 0 : this._frame_x, false);
}
return Promise.all([pr1, pr2]).then(() => {
this.axes2_drawn = true;
return true;
});
}
/** @summary Update frame attributes
* @private */
updateAttributes(force) {
const pp = this.getPadPainter(),
pad = pp?.getRootPad(true),
tframe = this.getObject();
if ((this.fX1NDC === undefined) || (force && !this.$modifiedNDC)) {
if (!pad) {
this.fX1NDC = gStyle.fPadLeftMargin;
this.fX2NDC = 1 - gStyle.fPadRightMargin;
this.fY1NDC = gStyle.fPadBottomMargin;
this.fY2NDC = 1 - gStyle.fPadTopMargin;
} else {
this.fX1NDC = pad.fLeftMargin;
this.fX2NDC = 1 - pad.fRightMargin;
this.fY1NDC = pad.fBottomMargin;
this.fY2NDC = 1 - pad.fTopMargin;
}
}
if (tframe) {
this.createAttFill({ attr: tframe });
this._borderMode = tframe.fBorderMode;
this._borderSize = tframe.fBorderSize;
} else if (this.fillatt === undefined) {
if (pad?.fFrameFillColor)
this.createAttFill({ pattern: pad.fFrameFillStyle, color: pad.fFrameFillColor });
else if (pad)
this.createAttFill({ attr: pad });
else
this.createAttFill({ pattern: gStyle.fFrameFillStyle, color: gStyle.fFrameFillColor });
// force white color for the canvas frame
if (!tframe && this.fillatt.empty() && pp?.iscan)
this.fillatt.setSolidColor('white');
else if ((pad?.fFillStyle === 4000) && !this.fillatt.empty()) // special case of transpad.C macro, which set transparent pad
this.fillatt.setOpacity(0);
}
if (!tframe && (pad?.fFrameLineColor !== undefined))
this.createAttLine({ color: pad.fFrameLineColor, width: pad.fFrameLineWidth, style: pad.fFrameLineStyle });
else if (tframe)
this.createAttLine({ attr: tframe, color: 'black' });
else
this.createAttLine({ color: gStyle.fFrameLineColor, width: gStyle.fFrameLineWidth, style: gStyle.fFrameLineStyle });
}
/** @summary Function called at the end of resize of frame
* @desc One should apply changes to the pad
* @private */
sizeChanged() {
const pad = this.getPadPainter()?.getRootPad(true);
if (pad) {
pad.fLeftMargin = this.fX1NDC;
pad.fRightMargin = 1 - this.fX2NDC;
pad.fBottomMargin = this.fY1NDC;
pad.fTopMargin = 1 - this.fY2NDC;
this.setRootPadRange(pad);
}
this.interactiveRedraw('pad', 'frame');
}
/** @summary Remove all kinds of X/Y function for axes transformation */
cleanXY() {
delete this.grx;
delete this.gry;
delete this.grz;
delete this.grx2;
delete this.gry2;
this.x_handle?.cleanup();
this.y_handle?.cleanup();
this.z_handle?.cleanup();
this.x2_handle?.cleanup();
this.y2_handle?.cleanup();
delete this.x_handle;
delete this.y_handle;
delete this.z_handle;
delete this.x2_handle;
delete this.y2_handle;
}
/** @summary remove all axes drawings */
cleanAxesDrawings() {
this.x_handle?.removeG();
this.y_handle?.removeG();
this.z_handle?.removeG();
this.x2_handle?.removeG();
this.y2_handle?.removeG();
this.draw_g?.selectChild('.axis_layer').selectAll('*').remove();
this.axes_drawn = this.axes2_drawn = false;
}
/** @summary Returns frame rectangle plus extra info for hint display */
cleanFrameDrawings() {
// cleanup all 3D drawings if any
if (isFunc(this.create3DScene))
this.create3DScene(-1);
this.cleanAxesDrawings();
this.cleanXY();
this.ranges_set = false;
this.xmin = this.xmax = 0;
this.ymin = this.ymax = 0;
this.zmin = this.zmax = 0;
this.zoom_xmin = this.zoom_xmax = 0;
this.zoom_ymin = this.zoom_ymax = 0;
this.zoom_zmin = this.zoom_zmax = 0;
this.scale_xmin = this.scale_xmax = 0;
this.scale_ymin = this.scale_ymax = 0;
this.scale_zmin = this.scale_zmax = 0;
this.draw_g?.selectChild('.main_layer').selectAll('*').remove();
this.draw_g?.selectChild('.upper_layer').selectAll('*').remove();
this.xaxis = null;
this.yaxis = null;
this.zaxis = null;
if (this.draw_g) {
this.draw_g.selectAll('*').remove();
this.draw_g.on('mousedown', null)
.on('dblclick', null)
.on('wheel', null)
.on('contextmenu', null)
.property('interactive_set', null);
this.draw_g.remove();
}
delete this.draw_g; // frame <g> element managed by the pad
if (this.keys_handler) {
window.removeEventListener('keydown', this.keys_handler, false);
this.keys_handler = null;
}
}
/** @summary Cleanup frame */
cleanup() {
this.cleanFrameDrawings();
delete this._click_handler;
delete this._dblclick_handler;
delete this.enabledKeys;
const pp = this.getPadPainter();
if (pp?.frame_painter_ref === this)
delete pp.frame_painter_ref;
super.cleanup();
}
/** @summary Redraw TFrame */
redraw(/* reason */) {
const pp = this.getPadPainter();
if (pp) pp.frame_painter_ref = this; // keep direct reference to the frame painter
// first update all attributes from objects
this.updateAttributes();
const rect = pp?.getPadRect() ?? { width: 10, height: 10 },
lm = Math.round(rect.width * this.fX1NDC),
tm = Math.round(rect.height * (1 - this.fY2NDC));
let w = Math.round(rect.width * (this.fX2NDC - this.fX1NDC)),
h = Math.round(rect.height * (this.fY2NDC - this.fY1NDC)),
rotate = false, fixpos = false, trans;
if (pp?.options) {
if (pp.options.RotateFrame) rotate = true;
if (pp.options.FixFrame) fixpos = true;
}
if (rotate) {
trans = `rotate(-90,${lm},${tm}) translate(${lm-h},${tm})`;
[w, h] = [h, w];
} else
trans = makeTranslate(lm, tm);
this._frame_x = lm;
this._frame_y = tm;
this._frame_width = w;
this._frame_height = h;
this._frame_rotate = rotate;
this._frame_fixpos = fixpos;
this._frame_trans = trans;
return this.mode3d ? this : this.createFrameG();
}
/** @summary Create frame element and update all attributes
* @private */
createFrameG() {
// this is svg:g object - container for every other items belonging to frame
this.draw_g = this.getFrameSvg();
let top_rect, main_svg;
if (this.draw_g.empty()) {
this.draw_g = this.getLayerSvg('primitives_layer').append('svg:g').attr('class', 'root_frame');
// empty title on the frame required to suppress title of the canvas
if (!this.isBatchMode())
this.draw_g.append('svg:title').text('');
top_rect = this.draw_g.append('svg:path');
main_svg = this.draw_g.append('svg:svg')
.attr('class', 'main_layer')
.attr('x', 0)
.attr('y', 0)
.attr('overflow', 'hidden');
this.draw_g.append('svg:g').attr('class', 'axis_layer');
this.draw_g.append('svg:g').attr('class', 'upper_layer');
} else {
top_rect = this.draw_g.selectChild('path');
main_svg = this.draw_g.selectChild('.main_layer');
}
this.axes_drawn = this.axes2_drawn = false;
this.draw_g.attr('transform', this._frame_trans);
top_rect.attr('d', `M0,0H${this._frame_width}V${this._frame_height}H0Z`)
.call(this.fillatt.func)
.call(this.lineatt.func);
main_svg.attr('width', this._frame_width)
.attr('height', this._frame_height)
.attr('viewBox', `0 0 ${this._frame_width} ${this._frame_height}`);
this.draw_g.selectAll('.frame_deco').remove();
if (this._borderMode && this.fillatt.hasColor()) {
const paths = getBoxDecorations(0, 0, this._frame_width, this._frame_height, this._borderMode, this._borderSize || 2, this._borderSize || 2);
this.draw_g.insert('svg:path', '.main_layer')
.attr('class', 'frame_deco')
.attr('d', paths[0])
.call(this.fillatt.func)
.style('fill', d3_rgb(this.fillatt.color).brighter(0.5).formatRgb());
this.draw_g.insert('svg:path', '.main_layer')
.attr('class', 'frame_deco')
.attr('d', paths[1])
.call(this.fillatt.func)
.style('fill', d3_rgb(this.fillatt.color).darker(0.5).formatRgb());
}
return this;
}
/** @summary Change log state of specified axis
* @param {string} axis - name of axis like 'x' or 'y'
* @param {number} value - 0 (linear), 1 (log) or 2 (log2) */
changeAxisLog(axis, value) {
const pp = this.getPadPainter(),
pad = pp?.getRootPad(true);
if (!pad) return;
pp._interactively_changed = true;
const name = `fLog${axis}`;
// do not allow log scale for labels
if (!pad[name]) {
if (this.swap_xy && axis === 'x')
axis = 'y';
else if (this.swap_xy && axis === 'y')
axis = 'x';
const handle = this[`${axis}_handle`];
if (handle?.kind === kAxisLabels) return;
}
if ((value === 'toggle') || (value === undefined))
value = pad[name] ? 0 : 1;
// directly change attribute in the pad
pad[name] = value;
return this.interactiveRedraw('pad', `log${axis}`);
}
/** @summary Toggle log state on the specified axis */
toggleAxisLog(axis) {
return this.changeAxisLog(axis, 'toggle');
}
/** @summary Fill context menu for the frame
* @desc It could be appended to the histogram menus */
fillContextMenu(menu, kind, obj) {
const main = this.getMainPainter(true),
wrk = main?.$stack_hist ? main.getPrimary() : main,
pp = this.getPadPainter(),
pad = pp?.getRootPad(true),
is_pal = kind === 'pal';
if (is_pal) kind = 'z';
if ((kind === 'x') || (kind === 'y') || (kind === 'z') || (kind === 'x2') || (kind === 'y2')) {
const faxis = obj || this[kind+'axis'],
handle = this[`${kind}_handle`];
if (!isFunc(faxis?.TestBit))
return false;
const hist_painter = handle?.hist_painter || main;
menu.header(`${kind.toUpperCase()} axis`, `${urlClassPrefix}${clTAxis}.html`);
menu.sub('Range');
menu.add('Zoom', () => {
let min = this[`zoom_${kind}min`] ?? this[`${kind}min`],
max = this[`zoom_${kind}max`] ?? this[`${kind}max`];
if (min === max) {
min = this[`${kind}min`];
max = this[`${kind}max`];
}
menu.input('Enter zoom range like: [min, max]', `[${min}, ${max}]`).then(v => {
const arr = JSON.parse(v);
if (arr && Array.isArray(arr) && (arr.length === 2)) {
let flag = false;
if (arr[0] < faxis.fXmin) {
faxis.fFirst = 0;
flag = true;
} else
faxis.fFirst = 1;
if (arr[1] > faxis.fXmax) {
faxis.fLast = faxis.fNbins + 1;
flag = true;
} else
faxis.fLast = faxis.fNbins;
faxis.SetBit(EAxisBits.kAxisRange, flag);
hist_painter?.scanContent();
this.zoomSingle(kind, arr[0], arr[1], true).then(res => {
if (!res && flag)
this.interactiveRedraw('pad');
});
}
});
});
menu.add('Unzoom', () => {
this.unzoomSingle(kind).then(res => {
if (!res && (faxis.fFirst !== faxis.fLast)) {
faxis.fFirst = faxis.fLast = 0;
hist_painter?.scanContent();
this.interactiveRedraw('pad');
}
});
});
if (handle?.value_axis && isFunc(wrk?.accessMM)) {
menu.add('Minimum', () => {
menu.input(`Enter minimum value or ${kNoZoom} as default`, wrk.accessMM(true), 'float').then(v => {
this[`zoom_${kind}min`] = this[`zoom_${kind}max`] = undefined;
wrk.accessMM(true, v);
});
});
menu.add('Maximum', () => {
menu.input(`Enter maximum value or ${kNoZoom} as default`, wrk.accessMM(false), 'float').then(v => {
this[`zoom_${kind}min`] = this[`zoom_${kind}max`] = undefined;
wrk.accessMM(false, v);
});
});
}
menu.endsub();
if (pad) {
const member = 'fLog'+kind[0];
menu.sub('SetLog '+kind[0], () => {
menu.input('Enter log kind: 0 - off, 1 - log10, 2 - log2, 3 - ln, ...', pad[member], 'int', 0, 10000).then(v => {
this.changeAxisLog(kind[0], v);
});
});
menu.addchk(pad[member] === 0, 'linear', () => this.changeAxisLog(kind[0], 0));
menu.addchk(pad[member] === 1, 'log10', () => this.changeAxisLog(kind[0], 1));
menu.addchk(pad[member] === 2, 'log2', () => this.changeAxisLog(kind[0], 2));
menu.addchk(pad[member] === 3, 'ln', () => this.changeAxisLog(kind[0], 3));
menu.addchk(pad[member] === 4, 'log4', () => this.changeAxisLog(kind[0], 4));
menu.addchk(pad[member] === 8, 'log8', () => this.changeAxisLog(kind[0], 8));
menu.endsub();
}
menu.addchk(faxis.TestBit(EAxisBits.kMoreLogLabels), 'More log', flag => {
faxis.SetBit(EAxisBits.kMoreLogLabels, flag);
if (hist_painter?.snapid && (kind.length === 1))
hist_painter.interactiveRedraw('pad', `exec:SetMoreLogLabels(${flag})`, kind);
else
this.interactiveRedraw('pad');
});
menu.addchk(handle?.noexp ?? faxis.TestBit(EAxisBits.kNoExponent), 'No exponent', flag => {
faxis.SetBit(EAxisBits.kNoExponent, flag);
if (handle) handle.noexp_changed = true;
this[`${kind}_noexp_changed`] = true;
if (hist_painter?.snapid && (kind.length === 1))
hist_painter.interactiveRedraw('pad', `exec:SetNoExponent(${flag})`, kind);
else
this.interactiveRedraw('pad');
});
if ((kind === 'z') && isFunc(hist_painter?.fillPaletteMenu))
hist_painter.fillPaletteMenu(menu, !is_pal);
menu.addTAxisMenu(EAxisBits, hist_painter || this, faxis, kind, handle, this);
return true;
}
const alone = menu.size() === 0;
if (alone)
menu.header('Frame', `${urlClassPrefix}${clTFrame}.html`);
else
menu.separator();
if (this.zoom_xmin !== this.zoom_xmax)
menu.add('Unzoom X', () => this.unzoom('x'));
if (this.zoom_ymin !== this.zoom_ymax)
menu.add('Unzoom Y', () => this.unzoom('y'));
if (this.zoom_zmin !== this.zoom_zmax)
menu.add('Unzoom Z', () => this.unzoom('z'));
if (this.zoom_x2min !== this.zoom_x2max)
menu.add('Unzoom X2', () => this.unzoom('x2'));
if (this.zoom_y2min !== this.zoom_y2max)
menu.add('Unzoom Y2', () => this.unzoom('y2'));
menu.add('Unzoom all', () => this.unzoom('all'));
if (pad) {
menu.addchk(pad.fLogx, 'SetLogx', () => this.toggleAxisLog('x'));
menu.addchk(pad.fLogy, 'SetLogy', () => this.toggleAxisLog('y'));
if (isFunc(main?.getDimension) && (main.getDimension() > 1))
menu.addchk(pad.fLogz, 'SetLogz', () => this.toggleAxisLog('z'));
menu.separator();
}
menu.addchk(this.isTooltipAllowed(), 'Show tooltips', () => this.setTooltipAllowed('toggle'));
menu.addAttributesMenu(this, alone ? '' : 'Frame ');
menu.sub('Border');
menu.addSelectMenu('Mode', ['Down', 'Off', 'Up'], this._borderMode + 1, v => {
this._borderMode = v - 1;
this.interactiveRedraw(true, `exec:SetBorderMode(${v-1})`);
}, 'Frame border mode');
menu.addSizeMenu('Size', 0, 20, 2, this._borderSize, v => {
this._borderSize = v;
this.interactiveRedraw(true, `exec:SetBorderSize(${v})`);
}, 'Frame border size');
menu.endsub();
menu.add('Save to gStyle', () => {
gStyle.fPadBottomMargin = this.fY1NDC;
gStyle.fPadTopMargin = 1 - this.fY2NDC;
gStyle.fPadLeftMargin = this.fX1NDC;
gStyle.fPadRightMargin = 1 - this.fX2NDC;
this.fillatt?.saveToStyle('fFrameFillColor', 'fFrameFillStyle');
this.lineatt?.saveToStyle('fFrameLineColor', 'fFrameLineWidth', 'fFrameLineStyle');
gStyle.fFrameBorderMode = this._borderMode;
gStyle.fFrameBorderSize = this._borderSize;
}, 'Store frame position and graphical attributes to gStyle');
menu.separator();
menu.sub('Save as');
const fmts = ['svg', 'png', 'jpeg', 'webp'];
if (internals.makePDF) fmts.push('pdf');
fmts.forEach(fmt => menu.add(`frame.${fmt}`, () => pp.saveAs(fmt, 'frame', `frame.${fmt}`)));
menu.endsub();
return true;
}
/** @summary Fill option object used in TWebCanvas
* @private */
fillWebObjectOptions(res) {
res.fcust = 'frame';
res.fopt = [this.scale_xmin || 0, this.scale_ymin || 0, this.scale_xmax || 0, this.scale_ymax || 0];
}
/** @summary Returns frame X position */
getFrameX() { return this._frame_x || 0; }
/** @summary Returns frame Y position */
getFrameY() { return this._frame_y || 0; }
/** @summary Returns frame width */
getFrameWidth() { return this._frame_width || 0; }
/** @summary Returns frame height */
getFrameHeight() { return this._frame_height || 0; }
/** @summary Returns frame rectangle plus extra info for hint display */
getFrameRect() {
return {
x: this._frame_x || 0,
y: this._frame_y || 0,
width: this.getFrameWidth(),
height: this.getFrameHeight(),
transform: this.draw_g?.attr('transform') || '',
hint_delta_x: 0,
hint_delta_y: 0
};
}
/** @summary Configure user-defined click handler
* @desc Function will be called every time when frame click was performed
* As argument, tooltip object with selected bins will be provided
* If handler function returns true, default handling of click will be disabled */
configureUserClickHandler(handler) {
this._click_handler = isFunc(handler) ? handler : null;
}
/** @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 */
configureUserDblclickHandler(handler) {
this._dblclick_handler = isFunc(handler) ? handler : null;
}
/** @summary Function can be used for zooming into specified range
* @desc if both limits for each axis 0 (like xmin === xmax === 0), axis will be un-zoomed
* @param {number} xmin
* @param {number} xmax
* @param {number} [ymin]
* @param {number} [ymax]
* @param {number} [zmin]
* @param {number} [zmax]
* @param [interactive] - if changes was performed interactively
* @return {Promise} with boolean flag if zoom operation was performed */
async zoom(xmin, xmax, ymin, ymax, zmin, zmax, interactive) {
if (xmin === 'x') {
xmin = xmax; xmax = ymin; interactive = ymax; ymin = ymax = undefined;
} else if (xmin === 'y') {
interactive = ymax; ymax = ymin; ymin = xmax; xmin = xmax = undefined;
} else if (xmin === 'z') {
interactive = ymax; zmin = xmax; zmax = ymin; xmin = xmax = ymin = ymax = undefined;
}
let zoom_x = (xmin !== xmax), zoom_y = (ymin !== ymax), zoom_z = (zmin !== zmax),
unzoom_x = false, unzoom_y = false, unzoom_z = false;
if (zoom_x) {
let cnt = 0;
xmin = this.x_handle?.checkZoomMin(xmin) ?? xmin;
if (xmin <= this.xmin) { xmin = this.xmin; cnt++; }
if (xmax >= this.xmax) { xmax = this.xmax; cnt++; }
if (cnt === 2) { zoom_x = false; unzoom_x = true; }
} else
unzoom_x = (xmin === xmax) && (xmin === 0);
if (zoom_y) {
let cnt = 0;
ymin = this.y_handle?.checkZoomMin(ymin) ?? ymin;
if (ymin <= this.ymin) { ymin = this.ymin; cnt++; }
if (ymax >= this.ymax) { ymax = this.ymax; cnt++; }
if ((cnt === 2) && (this.scales_ndim !== 1)) {
zoom_y = false;
unzoom_y = true;
}
} else
unzoom_y = (ymin === ymax) && (ymin === 0);
if (zoom_z) {
let cnt = 0;
zmin = this.z_handle?.checkZoomMin(zmin) ?? zmin;
if (zmin <= this.zmin) { zmin = this.zmin; cnt++; }
if (zmax >= this.zmax) { zmax = this.zmax; cnt++; }
if ((cnt === 2) && (this.scales_ndim > 2)) { zoom_z = false; unzoom_z = true; }
} else
unzoom_z = (zmin === zmax) && (zmin === 0);
let changed = false;
// first process zooming (if any)
if (zoom_x || zoom_y || zoom_z) {
this.forEachPainter(obj => {
if (!isFunc(obj.canZoomInside)) return;
if (zoom_x && obj.canZoomInside('x', xmin, xmax)) {
this.zoom_xmin = xmin;
this.zoom_xmax = xmax;
changed = true;
zoom_x = false;
if (interactive)
this.zoomChangedInteractive('x', interactive);
}
if (zoom_y && obj.canZoomInside('y', ymin, ymax)) {
this.zoom_ymin = ymin;
this.zoom_ymax = ymax;
changed = true;
zoom_y = false;
if (interactive)
this.zoomChangedInteractive('y', interactive);
}
if (zoom_z && obj.canZoomInside('z', zmin, zmax)) {
this.zoom_zmin = zmin;
this.zoom_zmax = zmax;
changed = true;
zoom_z = false;
if (interactive)
this.zoomChangedInteractive('y', interactive);
}
});
}
// and process unzoom, if any
if (unzoom_x || unzoom_y || unzoom_z) {
if (unzoom_x) {
if (this.zoom_xmin !== this.zoom_xmax)
changed = true;
this.zoom_xmin = this.zoom_xmax = 0;
if (interactive)
this.zoomChangedInteractive('x', interactive);
}
if (unzoom_y) {
if (this.zoom_ymin !== this.zoom_ymax) {
changed = true;
unzoomHistogramYRange(this.getMainPainter());
}
this.zoom_ymin = this.zoom_ymax = 0;
if (interactive)
this.zoomChangedInteractive('y', interactive);
}
if (unzoom_z) {
if (this.zoom_zmin !== this.zoom_zmax)
changed = true;
this.zoom_zmin = this.zoom_zmax = 0;
if (interactive)
this.zoomChangedInteractive('z', interactive);
}
// than try to unzoom all overlapped objects
if (!changed) {
this.getPadPainter()?.painters?.forEach(painter => {
if (isFunc(painter?.unzoomUserRange)) {
if (painter.unzoomUserRange(unzoom_x, unzoom_y, unzoom_z))
changed = true;
}
});
}
}
return changed ? this.interactiveRedraw('pad', 'zoom').then(() => true) : false;
}
/** @summary Zooming of single axis
* @param {String} name - axis name like x/y/z but also second axis x2 or y2
* @param {Number} vmin - axis minimal value, 0 for unzoom
* @param {Number} vmax - axis maximal value, 0 for unzoom
* @param {Boolean} [interactive] - if change was performed interactively
* @protected */
async zoomSingle(name, vmin, vmax, interactive) {
const handle = this[`${name}_handle`];
if (!handle && (name !== 'z'))
return false;
let zoom_v = (vmin !== vmax), unzoom_v = false;
if (zoom_v) {
let cnt = 0;
vmin = handle?.checkZoomMin(vmin) ?? vmin;
if (vmin <= this[name+'min']) { vmin = this[name+'min']; cnt++; }
if (vmax >= this[name+'max']) { vmax = this[name+'max']; cnt++; }
if (cnt === 2) { zoom_v = false; unzoom_v = true; }
} else
unzoom_v = (vmin === vmax) && (vmin === 0);
let changed = false;
// first process zooming
if (zoom_v) {
this.forEachPainter(obj => {
if (!isFunc(obj.canZoomInside)) return;
if (zoom_v && obj.canZoomInside(name[0], vmin, vmax)) {
this[`zoom_${name}min`] = vmin;
this[`zoom_${name}max`] = vmax;
changed = true;
zoom_v = false;
}
});
}
// and process unzoom, if any
if (unzoom_v) {
if (this[`zoom_${name}min`] !== this[`zoom_${name}max`]) {
changed = true;
if (name === 'y') unzoomHistogramYRange(this.getMainPainter());
}
this[`zoom_${name}min`] = this[`zoom_${name}max`] = 0;
}
if (!changed)
return false;
if (interactive)
this.zoomChangedInteractive(name, interactive);
return this.interactiveRedraw('pad', 'zoom').then(() => true);
}
/** @summary Unzoom single axis */
async unzoomSingle(name, interactive) {
return this.zoomSingle(name, 0, 0, typeof interactive === 'undefined' ? 'unzoom' : interactive);
}
/** @summary Checks if specified axis zoomed */
isAxisZoomed(axis) {
return this[`zoom_${axis}min`] !== this[`zoom_${axis}max`];
}
/** @summary Unzoom specified axes
* @return {Promise} with boolean flag if zooming changed */
async unzoom(dox, doy, doz) {
if (dox === 'all')
return this.unzoomSingle('x2').then(() => this.unzoomSingle('y2')).then(() => this.unzoom('xyz'));
if ((dox === 'x2') || (dox === 'y2'))
return this.unzoomSingle(dox);
if (typeof dox === 'undefined')
dox = doy = doz = true;
else if (isStr(dox)) {
doz = dox.indexOf('z') >= 0;
doy = dox.indexOf('y') >= 0;
dox = dox.indexOf('x') >= 0;
}
return this.zoom(dox ? 0 : undefined, dox ? 0 : undefined,
doy ? 0 : undefined, doy ? 0 : undefined,
doz ? 0 : undefined, doz ? 0 : undefined,
'unzoom');
}
/** @summary Reset all zoom attributes
* @private */
resetZoom() {
['x', 'y', 'z', 'x2', 'y2'].forEach(n => {
this[`zoom_${n}min`] = undefined;
this[`zoom_${n}max`] = undefined;
this[`zoom_changed_${n}`] = undefined;
});
}
/** @summary Mark/check if zoom for specific axis was changed interactively
* @private */
zoomChangedInteractive(axis, value) {
if (axis === 'reset') {
this.zoom_changed_x = this.zoom_changed_y = this.zoom_changed_z = undefined;
return;
}
if (!axis || axis === 'any')
return this.zoom_changed_x || this.zoom_changed_y || this.zoom_changed_z;
if ((axis !== 'x') && (axis !== 'y') && (axis !== 'z')) return;
const fld = 'zoom_changed_' + axis;
if (value === undefined)
return this[fld];
if (value === 'unzoom') {
// special handling of unzoom, only if was never changed before flag set to true
this[fld] = (this[fld] === undefined);
return;
}
if (value)
this[fld] = true;
}
/** @summary Convert graphical coordinate into axis value */
revertAxis(axis, pnt) {
if (this.swap_xy)
axis = (axis[0] === 'x') ? 'y' : 'x';
return this[`${axis}_handle`]?.revertPoint(pnt) ?? 0;
}
/** @summary Show axis status message
* @desc method called normally when mouse enter main object element
* @private */
showAxisStatus(axis_name, evnt) {
const taxis = this.getAxis(axis_name),
m = d3_pointer(evnt, this.getFrameSvg().node());
let hint_name = axis_name,
hint_title = clTAxis,
id = (axis_name === 'x') ? 0 : 1;
if (taxis) {
hint_name = taxis.fName;
hint_title = taxis.fTitle || `TAxis object for ${axis_name}`;
}
if (this.swap_xy) id = 1 - id;
const axis_value = this.revertAxis(axis_name, m[id]);
this.showObjectStatus(hint_name, hint_title, `${axis_name} : ${this.axisAsText(axis_name, axis_value)}`, `${m[0]},${m[1]}`);
}
/** @summary Add interactive keys handlers
* @private */
addKeysHandler() {
if (this.isBatchMode()) return;
FrameInteractive.assign(this);
this.addFrameKeysHandler();
}
/** @summary Add interactive functionality to the frame
* @private */
addInteractivity(for_second_axes) {
if (this.isBatchMode() || (!settings.Zooming && !settings.ContextMenu))
return false;
FrameInteractive.assign(this);
if (!for_second_axes)
this.addBasicInteractivity();
return this.addFrameInteractivity(for_second_axes);
}
} // class TFramePainter
export { addDragHandler, TooltipHandler, FrameInteractive, TFramePainter, getEarthProjectionFunc };