jsroot
Version:
JavaScript ROOT
1,365 lines (1,096 loc) • 116 kB
JavaScript
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) {
d