import { select as d3_select, pointer as d3_pointer,
drag as d3_drag, timeFormat as d3_timeFormat,
scaleTime as d3_scaleTime, scaleSymlog as d3_scaleSymlog,
scaleLog as d3_scaleLog, scaleLinear as d3_scaleLinear } from '../d3.mjs';
import { settings, isFunc, urlClassPrefix } from '../core.mjs';
import { makeTranslate, addHighlightStyle } from '../base/BasePainter.mjs';
import { AxisPainterMethods, chooseTimeFormat } from './TAxisPainter.mjs';
import { createMenu } from '../gui/menu.mjs';
import { addDragHandler } from './TFramePainter.mjs';
import { kAxisLabels, kAxisNormal, kAxisTime } from '../base/ObjectPainter.mjs';
import { RObjectPainter } from '../base/RObjectPainter.mjs';
/**
* @summary Axis painter for v7
*
* @private
*/
class RAxisPainter extends RObjectPainter {
/** @summary constructor */
constructor(dom, arg1, axis, cssprefix) {
const drawable = cssprefix ? arg1.getObject() : arg1;
super(dom, drawable, '', cssprefix ? arg1.csstype : 'axis');
Object.assign(this, AxisPainterMethods);
this.initAxisPainter();
this.axis = axis;
if (cssprefix) { // drawing from the frame
this.embedded = true; // indicate that painter embedded into the histo painter
// this.csstype = arg1.csstype; // for the moment only via frame one can set axis attributes
this.cssprefix = cssprefix;
this.rstyle = arg1.rstyle;
} else {
// this.csstype = 'axis';
this.cssprefix = 'axis_';
}
}
/** @summary cleanup painter */
cleanup() {
delete this.axis;
delete this.axis_g;
this.cleanupAxisPainter();
super.cleanup();
}
/** @summary Use in GED to identify kind of axis */
getAxisType() { return 'RAttrAxis'; }
/** @summary Configure only base parameters, later same handle will be used for drawing */
configureZAxis(name, fp) {
this.name = name;
this.kind = kAxisNormal;
this.log = false;
const _log = this.v7EvalAttr('log', 0);
if (_log) {
this.log = true;
this.logbase = 10;
if (Math.abs(_log - Math.exp(1)) < 0.1)
this.logbase = Math.exp(1);
else if (_log > 1.9)
this.logbase = Math.round(_log);
}
fp.logz = this.log;
}
/** @summary Configure axis painter
* @desc Axis can be drawn inside frame <g> group with offset to 0 point for the frame
* Therefore one should distinguish when calculated coordinates used for axis drawing itself or for calculation of frame coordinates
* @private */
configureAxis(name, min, max, smin, smax, vertical, frame_range, axis_range, opts) {
if (!opts) opts = {};
this.name = name;
this.full_min = min;
this.full_max = max;
this.kind = kAxisNormal;
this.vertical = vertical;
this.log = false;
const _log = this.v7EvalAttr('log', 0),
_symlog = this.v7EvalAttr('symlog', 0);
this.reverse = opts.reverse || false;
if (this.v7EvalAttr('time')) {
this.kind = kAxisTime;
this.timeoffset = 0;
let toffset = this.v7EvalAttr('timeOffset');
if (toffset !== undefined) {
toffset = parseFloat(toffset);
if (Number.isFinite(toffset)) this.timeoffset = toffset*1000;
}
} else if (this.axis?.fLabelsIndex) {
this.kind = kAxisLabels;
delete this.own_labels;
} else if (opts.labels)
this.kind = kAxisLabels;
else
this.kind = kAxisNormal;
if (this.kind === kAxisTime)
this.func = d3_scaleTime().domain([this.convertDate(smin), this.convertDate(smax)]);
else if (_symlog && (_symlog > 0)) {
this.symlog = _symlog;
this.func = d3_scaleSymlog().constant(_symlog).domain([smin, smax]);
} else if (_log) {
if (smax <= 0) smax = 1;
if ((smin <= 0) || (smin >= smax))
smin = smax * 0.0001;
this.log = true;
this.logbase = 10;
if (Math.abs(_log - Math.exp(1)) < 0.1)
this.logbase = Math.exp(1);
else if (_log > 1.9)
this.logbase = Math.round(_log);
this.func = d3_scaleLog().base(this.logbase).domain([smin, smax]);
} else
this.func = d3_scaleLinear().domain([smin, smax]);
this.scale_min = smin;
this.scale_max = smax;
this.gr_range = axis_range || 1000; // when not specified, one can ignore it
const range = frame_range ?? [0, this.gr_range];
this.axis_shift = range[1] - this.gr_range;
if (this.reverse)
this.func.range([range[1], range[0]]);
else
this.func.range(range);
if (this.kind === kAxisTime)
this.gr = val => this.func(this.convertDate(val));
else if (this.log)
this.gr = val => (val < this.scale_min) ? (this.vertical ? this.func.range()[0]+5 : -5) : this.func(val);
else
this.gr = this.func;
delete this.format;// remove formatting func
const ndiv = this.v7EvalAttr('ndiv', 508);
this.nticks = ndiv % 100;
this.nticks2 = (ndiv % 10000 - this.nticks) / 100;
this.nticks3 = Math.floor(ndiv/10000);
if (this.nticks > 20) this.nticks = 20;
const gr_range = Math.abs(this.gr_range) || 100;
if (this.kind === kAxisTime) {
if (this.nticks > 8) this.nticks = 8;
const scale_range = this.scale_max - this.scale_min,
tf2 = chooseTimeFormat(scale_range / gr_range, false);
let tf1 = this.v7EvalAttr('timeFormat', '');
if (!tf1 || (scale_range < 0.1 * (this.full_max - this.full_min)))
tf1 = chooseTimeFormat(scale_range / this.nticks, true);
this.tfunc1 = this.tfunc2 = d3_timeFormat(tf1);
if (tf2 !== tf1)
this.tfunc2 = d3_timeFormat(tf2);
this.format = this.formatTime;
} else if (this.log) {
if (this.nticks2 > 1) {
this.nticks *= this.nticks2; // all log ticks (major or minor) created centrally
this.nticks2 = 1;
}
this.noexp = this.v7EvalAttr('noexp', false);
if ((this.scale_max < 300) && (this.scale_min > 0.3) && (this.logbase === 10)) this.noexp = true;
this.moreloglabels = this.v7EvalAttr('moreloglbls', false);
this.format = this.formatLog;
} else if (this.kind === kAxisLabels) {
this.nticks = 50; // for text output allow max 50 names
const scale_range = this.scale_max - this.scale_min;
if (this.nticks > scale_range)
this.nticks = Math.round(scale_range);
this.nticks2 = 1;
this.format = this.formatLabels;
} else {
this.order = 0;
this.ndig = 0;
this.format = this.formatNormal;
}
}
/** @summary Return scale min */
getScaleMin() {
return this.func ? this.func.domain()[0] : 0;
}
/** @summary Return scale max */
getScaleMax() {
return this.func ? this.func.domain()[1] : 0;
}
/** @summary Provide label for axis value */
formatLabels(d) {
const indx = Math.round(d);
if (this.axis?.fLabelsIndex) {
if ((indx < 0) || (indx >= this.axis.fNBinsNoOver)) return null;
for (let i = 0; i < this.axis.fLabelsIndex.length; ++i) {
const pair = this.axis.fLabelsIndex[i];
if (pair.second === indx) return pair.first;
}
} else {
const labels = this.getObject().fLabels;
if (labels && (indx >= 0) && (indx < labels.length))
return labels[indx];
}
return null;
}
/** @summary Creates array with minor/middle/major ticks */
createTicks(only_major_as_array, optionNoexp, optionNoopt, optionInt) {
if (optionNoopt && this.nticks && (this.kind === kAxisNormal)) this.noticksopt = true;
const ticks = this.produceTicks(this.nticks),
handle = { nminor: 0, nmiddle: 0, nmajor: 0, func: this.func, minor: ticks, middle: ticks, major: ticks };
if (only_major_as_array) {
const res = handle.major, delta = (this.scale_max - this.scale_min) * 1e-5;
if (res.at(0) > this.scale_min + delta)
res.unshift(this.scale_min);
if (res.at(-1) < this.scale_max - delta)
res.push(this.scale_max);
return res;
}
if ((this.nticks2 > 1) && (!this.log || (this.logbase === 10))) {
handle.minor = handle.middle = this.produceTicks(handle.major.length, this.nticks2);
const gr_range = Math.abs(this.func.range()[1] - this.func.range()[0]);
// avoid black filling by middle-size
if ((handle.middle.length <= handle.major.length) || (handle.middle.length > gr_range))
handle.minor = handle.middle = handle.major;
else if ((this.nticks3 > 1) && !this.log) {
handle.minor = this.produceTicks(handle.middle.length, this.nticks3);
if ((handle.minor.length <= handle.middle.length) || (handle.minor.length > gr_range))
handle.minor = handle.middle;
}
}
handle.reset = function() {
this.nminor = this.nmiddle = this.nmajor = 0;
};
handle.next = function(doround) {
if (this.nminor >= this.minor.length) return false;
this.tick = this.minor[this.nminor++];
this.grpos = this.func(this.tick);
if (doround) this.grpos = Math.round(this.grpos);
this.kind = 3;
if ((this.nmiddle < this.middle.length) && (Math.abs(this.grpos - this.func(this.middle[this.nmiddle])) < 1)) {
this.nmiddle++;
this.kind = 2;
}
if ((this.nmajor < this.major.length) && (Math.abs(this.grpos - this.func(this.major[this.nmajor])) < 1)) {
this.nmajor++;
this.kind = 1;
}
return true;
};
handle.last_major = function() {
return (this.kind !== 1) ? false : this.nmajor === this.major.length;
};
handle.next_major_grpos = function() {
if (this.nmajor >= this.major.length) return null;
return this.func(this.major[this.nmajor]);
};
handle.get_modifier = function() { return null; };
this.order = 0;
this.ndig = 0;
// at the moment when drawing labels, we can try to find most optimal text representation for them
if ((this.kind === kAxisNormal) && !this.log && (handle.major.length > 0)) {
let maxorder = 0, minorder = 0, exclorder3 = false;
if (!optionNoexp) {
const maxtick = Math.max(Math.abs(handle.major.at(0)), Math.abs(handle.major.at(-1))),
mintick = Math.min(Math.abs(handle.major.at(0)), Math.abs(handle.major.at(-1))),
ord1 = (maxtick > 0) ? Math.round(Math.log10(maxtick)/3)*3 : 0,
ord2 = (mintick > 0) ? Math.round(Math.log10(mintick)/3)*3 : 0;
exclorder3 = (maxtick < 2e4); // do not show 10^3 for values below 20000
if (maxtick || mintick) {
maxorder = Math.max(ord1, ord2) + 3;
minorder = Math.min(ord1, ord2) - 3;
}
}
// now try to find best combination of order and ndig for labels
let bestorder = 0, bestndig = this.ndig, bestlen = 1e10;
for (let order = minorder; order <= maxorder; order+=3) {
if (exclorder3 && (order===3)) continue;
this.order = order;
this.ndig = 0;
let lbls = [], indx = 0, totallen = 0;
while (indx<handle.major.length) {
const lbl = this.format(handle.major[indx], true);
if (lbls.indexOf(lbl) < 0) {
lbls.push(lbl);
totallen += lbl.length;
indx++;
continue;
}
if (++this.ndig > 11) break; // not too many digits, anyway it will be exponential
lbls = []; indx = 0; totallen = 0;
}
// for order === 0 we should virtually remove '0.' and extra label on top
if (!order && (this.ndig < 4))
totallen -= (handle.major.length * 2 + 3);
if (totallen < bestlen) {
bestlen = totallen;
bestorder = this.order;
bestndig = this.ndig;
}
}
this.order = bestorder;
this.ndig = bestndig;
if (optionInt) {
if (this.order) console.warn(`Axis painter - integer labels are configured, but axis order ${this.order} is preferable`);
if (this.ndig) console.warn(`Axis painter - integer labels are configured, but ${this.ndig} decimal digits are required`);
this.ndig = 0;
this.order = 0;
}
}
return handle;
}
/** @summary Is labels should be centered */
isCenteredLabels() {
if (this.kind === kAxisLabels) return true;
if (this.kind === 'log') return false;
return this.v7EvalAttr('labels_center', false);
}
/** @summary Is labels should be rotated */
isRotateLabels() { return false; }
/** @summary Used to move axis labels instead of zooming
* @private */
processLabelsMove(arg, pos) {
if (this.optionUnlab || !this.axis_g) return false;
const label_g = this.axis_g.select('.axis_labels');
if (!label_g || (label_g.size() !== 1)) return false;
if (arg === 'start') {
// no moving without labels
const box = label_g.node().getBBox();
label_g.append('rect')
.classed('drag', true)
.attr('x', box.x)
.attr('y', box.y)
.attr('width', box.width)
.attr('height', box.height)
.style('cursor', 'move')
.call(addHighlightStyle, true);
if (this.vertical)
this.drag_pos0 = pos[0];
else
this.drag_pos0 = pos[1];
return true;
}
let offset = label_g.property('fix_offset');
if (this.vertical) {
offset += Math.round(pos[0] - this.drag_pos0);
makeTranslate(label_g, offset);
} else {
offset += Math.round(pos[1] - this.drag_pos0);
makeTranslate(label_g, 0, offset);
}
if (!offset)
makeTranslate(label_g);
if (arg === 'stop') {
label_g.select('rect.drag').remove();
delete this.drag_pos0;
if (offset !== label_g.property('fix_offset')) {
label_g.property('fix_offset', offset);
const side = label_g.property('side') || 1;
this.labelsOffset = offset / (this.vertical ? -side : side);
this.changeAxisAttr(1, 'labels_offset', this.labelsOffset / this.scalingSize);
}
}
return true;
}
/** @summary Add interactive elements to draw axes title */
addTitleDrag(title_g, side) {
if (!settings.MoveResize || this.isBatchMode()) return;
let drag_rect = null,
acc_x, acc_y, new_x, new_y, alt_pos, curr_indx;
const drag_move = d3_drag().subject(Object);
drag_move
.on('start', evnt => {
evnt.sourceEvent.preventDefault();
evnt.sourceEvent.stopPropagation();
const box = title_g.node().getBBox(), // check that elements visible, request precise value
title_length = this.vertical ? box.height : box.width;
new_x = acc_x = title_g.property('shift_x');
new_y = acc_y = title_g.property('shift_y');
if (this.titlePos === 'center')
curr_indx = 1;
else
curr_indx = (this.titlePos === 'left') ? 0 : 2;
// let d = ((this.gr_range > 0) && this.vertical) ? title_length : 0;
alt_pos = [0, this.gr_range/2, this.gr_range]; // possible positions
const off = this.vertical ? -title_length : title_length,
swap = this.isReverseAxis() ? 2 : 0;
if (this.title_align === 'middle') {
alt_pos[swap] += off/2;
alt_pos[2-swap] -= off/2;
} else if ((this.title_align === 'begin') ^ this.isTitleRotated()) {
alt_pos[1] -= off/2;
alt_pos[2-swap] -= off;
} else { // end
alt_pos[swap] += off;
alt_pos[1] += off/2;
}
alt_pos[curr_indx] = this.vertical ? acc_y : acc_x;
drag_rect = title_g.append('rect')
.attr('x', box.x)
.attr('y', box.y)
.attr('width', box.width)
.attr('height', box.height)
.style('cursor', 'move')
.call(addHighlightStyle, true);
// .style('pointer-events','none'); // let forward double click to underlying elements
}).on('drag', evnt => {
if (!drag_rect) return;
evnt.sourceEvent.preventDefault();
evnt.sourceEvent.stopPropagation();
acc_x += evnt.dx;
acc_y += evnt.dy;
const p = this.vertical ? acc_y : acc_x;
let set_x, set_y, besti = 0;
for (let i = 1; i < 3; ++i)
if (Math.abs(p - alt_pos[i]) < Math.abs(p - alt_pos[besti])) besti = i;
if (this.vertical) {
set_x = acc_x;
set_y = alt_pos[besti];
} else {
set_x = alt_pos[besti];
set_y = acc_y;
}
new_x = set_x; new_y = set_y; curr_indx = besti;
makeTranslate(title_g, new_x, new_y);
}).on('end', evnt => {
if (!drag_rect) return;
evnt.sourceEvent.preventDefault();
evnt.sourceEvent.stopPropagation();
const basepos = title_g.property('basepos') || 0;
title_g.property('shift_x', new_x)
.property('shift_y', new_y);
this.titleOffset = (this.vertical ? basepos - new_x : new_y - basepos) * side;
if (curr_indx === 1)
this.titlePos = 'center';
else if (curr_indx === 0)
this.titlePos = 'left';
else
this.titlePos = 'right';
this.changeAxisAttr(0, 'title_position', this.titlePos, 'title_offset', this.titleOffset / this.scalingSize);
drag_rect.remove();
drag_rect = null;
});
title_g.style('cursor', 'move').call(drag_move);
}
/** @summary checks if value inside graphical range, taking into account delta */
isInsideGrRange(pos, delta1, delta2) {
if (!delta1) delta1 = 0;
if (delta2 === undefined) delta2 = delta1;
if (this.gr_range < 0)
return (pos >= this.gr_range - delta2) && (pos <= delta1);
return (pos >= -delta1) && (pos <= this.gr_range + delta2);
}
/** @summary returns graphical range */
getGrRange(delta) {
if (!delta) delta = 0;
if (this.gr_range < 0)
return this.gr_range - delta;
return this.gr_range + delta;
}
/** @summary If axis direction is negative coordinates direction */
isReverseAxis() {
return !this.vertical !== (this.getGrRange() > 0);
}
/** @summary Draw axis ticks
* @private */
drawMainLine(axis_g) {
let ending = '';
if (this.endingSize && this.endingStyle) {
let sz = (this.gr_range > 0) ? -this.endingSize : this.endingSize;
const sz7 = Math.round(sz*0.7);
sz = Math.round(sz);
if (this.vertical)
ending = `l${sz7},${sz}M0,${this.gr_range}l${-sz7},${sz}`;
else
ending = `l${sz},${sz7}M${this.gr_range},0l${sz},${-sz7}`;
}
axis_g.append('svg:path')
.attr('d', 'M0,0' + (this.vertical ? 'v' : 'h') + this.gr_range + ending)
.call(this.lineatt.func)
.style('fill', ending ? 'none' : null);
}
/** @summary Draw axis ticks
* @return {Object} with gaps on left and right side
* @private */
drawTicks(axis_g, side, main_draw) {
if (main_draw) this.ticks = [];
this.handle.reset();
let res = '', ticks_plusminus = 0;
if (this.ticksSide === 'both') {
side = 1;
ticks_plusminus = 1;
}
while (this.handle.next(true)) {
let h1 = Math.round(this.ticksSize/4), h2;
if (this.handle.kind < 3)
h1 = Math.round(this.ticksSize/2);
const grpos = this.handle.grpos - this.axis_shift;
if ((this.startingSize || this.endingSize) && !this.isInsideGrRange(grpos, -Math.abs(this.startingSize), -Math.abs(this.endingSize))) continue;
if (this.handle.kind === 1) {
// if not showing labels, not show large tick
if ((this.kind === kAxisLabels) || (this.format(this.handle.tick, true) !== null)) h1 = this.ticksSize;
if (main_draw) this.ticks.push(grpos); // keep graphical positions of major ticks
}
if (ticks_plusminus > 0)
h2 = -h1;
else if (side < 0) {
h2 = -h1; h1 = 0;
} else
h2 = 0;
res += this.vertical ? `M${h1},${grpos}H${h2}` : `M${grpos},${-h1}V${-h2}`;
}
if (res) {
axis_g.append('svg:path')
.attr('d', res)
.style('stroke', this.ticksColor || this.lineatt.color)
.style('stroke-width', !this.ticksWidth || (this.ticksWidth === 1) ? null : this.ticksWidth);
}
const gap0 = Math.round(0.25*this.ticksSize), gap = Math.round(1.25*this.ticksSize);
return { '-1': (side > 0) || ticks_plusminus ? gap : gap0,
1: (side < 0) || ticks_plusminus ? gap : gap0 };
}
/** @summary Performs labels drawing
* @return {Promise} with gaps in both direction */
async drawLabels(axis_g, side, gaps) {
const center_lbls = this.isCenteredLabels(),
rotate_lbls = this.labelsFont.angle !== 0,
label_g = axis_g.append('svg:g').attr('class', 'axis_labels').property('side', side),
lbl_pos = this.handle.lbl_pos || this.handle.major;
let textscale = 1, maxtextlen = 0, lbls_tilt = false,
max_lbl_width = 0, max_lbl_height = 0;
// function called when text is drawn to analyze width, required to correctly scale all labels
function process_drawtext_ready(painter) {
max_lbl_width = Math.max(max_lbl_width, this.result_width);
max_lbl_height = Math.max(max_lbl_height, this.result_height);
const textwidth = this.result_width;
if (textwidth && ((!painter.vertical && !rotate_lbls) || (painter.vertical && rotate_lbls)) && !painter.log) {
let maxwidth = this.gap_before*0.45 + this.gap_after*0.45;
if (!this.gap_before) maxwidth = 0.9*this.gap_after; else
if (!this.gap_after) maxwidth = 0.9*this.gap_before;
textscale = Math.min(textscale, maxwidth / textwidth);
}
if ((textscale > 0.0001) && (textscale < 0.8) && !painter.vertical && !rotate_lbls && (maxtextlen > 5) && (side > 0))
lbls_tilt = true;
const scale = textscale * (lbls_tilt ? 3 : 1);
if ((scale > 0.0001) && (scale < 1))
painter.scaleTextDrawing(1/scale, label_g);
}
const fix_offset = Math.round((this.vertical ? -side : side) * this.labelsOffset),
fix_coord = Math.round((this.vertical ? -side : side) * gaps[side]);
let lastpos = 0;
if (fix_offset)
makeTranslate(label_g, this.vertical ? fix_offset : 0, this.vertical ? 0 : fix_offset);
label_g.property('fix_offset', fix_offset);
return this.startTextDrawingAsync(this.labelsFont, 'font', label_g).then(() => {
for (let nmajor = 0; nmajor < lbl_pos.length; ++nmajor) {
const lbl = this.format(lbl_pos[nmajor], true);
if (lbl === null) continue;
const arg = { text: lbl, latex: 1, draw_g: label_g };
let pos = Math.round(this.func(lbl_pos[nmajor]));
arg.gap_before = (nmajor > 0) ? Math.abs(Math.round(pos - this.func(lbl_pos[nmajor-1]))) : 0;
arg.gap_after = (nmajor < lbl_pos.length - 1) ? Math.abs(Math.round(this.func(lbl_pos[nmajor+1])-pos)) : 0;
if (center_lbls) {
const gap = arg.gap_after || arg.gap_before;
pos = Math.round(pos - (this.vertical ? 0.5*gap : -0.5*gap));
if (!this.isInsideGrRange(pos, 5)) continue;
}
maxtextlen = Math.max(maxtextlen, lbl.length);
pos -= this.axis_shift;
if ((this.startingSize || this.endingSize) && !this.isInsideGrRange(pos, -Math.abs(this.startingSize), -Math.abs(this.endingSize))) continue;
if (this.vertical) {
arg.x = fix_coord;
arg.y = pos;
arg.align = rotate_lbls ? ((side < 0) ? 23 : 20) : ((side < 0) ? 12 : 32);
} else {
arg.x = pos;
arg.y = fix_coord;
arg.align = rotate_lbls ? ((side < 0) ? 12 : 32) : ((side < 0) ? 20 : 23);
if (this.log && !this.noexp && !this.vertical && arg.align === 23) {
arg.align = 21;
arg.y += this.labelsFont.size;
}
}
arg.post_process = process_drawtext_ready;
this.drawText(arg);
if (lastpos && (pos !== lastpos) && ((this.vertical && !rotate_lbls) || (!this.vertical && rotate_lbls))) {
const axis_step = Math.abs(pos-lastpos);
textscale = Math.min(textscale, 0.9*axis_step/this.labelsFont.size);
}
lastpos = pos;
}
if (this.order) {
this.drawText({ x: this.vertical ? side*5 : this.getGrRange(5),
y: this.has_obstacle ? fix_coord : (this.vertical ? this.getGrRange(3) : -3*side),
align: this.vertical ? ((side < 0) ? 30 : 10) : ((this.has_obstacle ^ (side < 0)) ? 13 : 10),
latex: 1,
text: '#times' + this.formatExp(10, this.order),
draw_g: label_g });
}
return this.finishTextDrawing(label_g);
}).then(() => {
if (lbls_tilt) {
label_g.selectAll('text').each(function() {
const txt = d3_select(this), tr = txt.attr('transform');
txt.attr('transform', tr + ' rotate(25)').style('text-anchor', 'start');
});
}
if (this.vertical)
gaps[side] += Math.round(rotate_lbls ? 1.2*max_lbl_height : max_lbl_width + 0.4*this.labelsFont.size) - side*fix_offset;
else {
const tilt_height = lbls_tilt ? max_lbl_width * Math.sin(25/180*Math.PI) + max_lbl_height * (Math.cos(25/180*Math.PI) + 0.2) : 0;
gaps[side] += Math.round(Math.max(rotate_lbls ? max_lbl_width + 0.4*this.labelsFont.size : 1.2*max_lbl_height, 1.2*this.labelsFont.size, tilt_height)) + fix_offset;
}
return gaps;
});
}
/** @summary Add zooming rect to axis drawing */
addZoomingRect(axis_g, side, lgaps) {
if (settings.Zooming && !this.disable_zooming && !this.isBatchMode()) {
const sz = Math.max(lgaps[side], 10),
d = this.vertical ? `v${this.gr_range}h${-side*sz}v${-this.gr_range}` : `h${this.gr_range}v${side*sz}h${-this.gr_range}`;
axis_g.append('svg:path')
.attr('d', `M0,0${d}z`)
.attr('class', 'axis_zoom')
.style('opacity', '0')
.style('cursor', 'crosshair');
}
}
/** @summary Returns true if axis title is rotated */
isTitleRotated() {
return this.titleFont && (this.titleFont.angle !== (this.vertical ? 270 : 0));
}
/** @summary Draw axis title */
async drawTitle(axis_g, side, lgaps) {
if (!this.fTitle)
return this;
const title_g = axis_g.append('svg:g').attr('class', 'axis_title'),
rotated = this.isTitleRotated();
return this.startTextDrawingAsync(this.titleFont, 'font', title_g).then(() => {
let title_shift_x, title_shift_y, title_basepos;
this.title_align = this.titleCenter ? 'middle' : (this.titleOpposite ^ (this.isReverseAxis() || rotated) ? 'begin' : 'end');
if (this.vertical) {
title_basepos = Math.round(-side*(lgaps[side]));
title_shift_x = title_basepos + Math.round(-side*this.titleOffset);
title_shift_y = Math.round(this.titleCenter ? this.gr_range/2 : (this.titleOpposite ? 0 : this.gr_range));
this.drawText({ align: [this.title_align, ((side < 0) ^ rotated ? 'top' : 'bottom')],
text: this.fTitle, draw_g: title_g });
} else {
title_shift_x = Math.round(this.titleCenter ? this.gr_range/2 : (this.titleOpposite ? 0 : this.gr_range));
title_basepos = Math.round(side*lgaps[side]);
title_shift_y = title_basepos + Math.round(side*this.titleOffset);
this.drawText({ align: [this.title_align, ((side > 0) ^ rotated ? 'top' : 'bottom')],
text: this.fTitle, draw_g: title_g });
}
makeTranslate(title_g, title_shift_x, title_shift_y)
.property('basepos', title_basepos)
.property('shift_x', title_shift_x)
.property('shift_y', title_shift_y);
this.addTitleDrag(title_g, side);
return this.finishTextDrawing(title_g);
});
}
/** @summary Extract major draw attributes, which are also used in interactive operations
* @private */
extractDrawAttributes(scalingSize) {
const pp = this.getPadPainter(),
rect = pp?.getPadRect() || { width: 10, height: 10 };
this.scalingSize = scalingSize || (this.vertical ? rect.width : rect.height);
this.createv7AttLine('line_');
this.optionUnlab = this.v7EvalAttr('labels_hide', false);
this.endingStyle = this.v7EvalAttr('ending_style', '');
this.endingSize = Math.round(this.v7EvalLength('ending_size', this.scalingSize, this.endingStyle ? 0.02 : 0));
this.startingSize = Math.round(this.v7EvalLength('starting_size', this.scalingSize, 0));
this.ticksSize = this.v7EvalLength('ticks_size', this.scalingSize, 0.02);
this.ticksSide = this.v7EvalAttr('ticks_side', 'normal');
this.ticksColor = this.v7EvalColor('ticks_color', '');
this.ticksWidth = this.v7EvalAttr('ticks_width', 1);
if (scalingSize && (this.ticksSize < 0))
this.ticksSize = -this.ticksSize;
this.fTitle = this.v7EvalAttr('title_value', '');
if (this.fTitle) {
this.titleFont = this.v7EvalFont('title', { size: 0.03 }, scalingSize || pp?.getPadHeight() || 10);
this.titleFont.roundAngle(180, this.vertical ? 270 : 0);
this.titleOffset = this.v7EvalLength('title_offset', this.scalingSize, 0);
this.titlePos = this.v7EvalAttr('title_position', 'right');
this.titleCenter = (this.titlePos === 'center');
this.titleOpposite = (this.titlePos === 'left');
} else {
delete this.titleFont;
delete this.titleOffset;
delete this.titlePos;
}
// TODO: remove old scaling factors for labels and ticks
this.labelsFont = this.v7EvalFont('labels', { size: scalingSize ? 0.05 : 0.03 });
this.labelsFont.roundAngle(180);
if (this.labelsFont.angle) this.labelsFont.angle = 270;
this.labelsOffset = this.v7EvalLength('labels_offset', this.scalingSize, 0);
if (scalingSize) this.ticksSize = this.labelsFont.size*0.5; // old lego scaling factor
if (this.maxTickSize && (this.ticksSize > this.maxTickSize))
this.ticksSize = this.maxTickSize;
}
/** @summary Performs axis drawing
* @return {Promise} which resolved when drawing is completed */
async drawAxis(layer, transform, side) {
let axis_g = layer;
if (side === undefined)
side = 1;
if (!this.standalone) {
axis_g = layer.selectChild(`.${this.name}_container`);
if (axis_g.empty())
axis_g = layer.append('svg:g').attr('class', `${this.name}_container`);
else
axis_g.selectAll('*').remove();
}
axis_g.attr('transform', transform);
this.extractDrawAttributes();
this.axis_g = axis_g;
this.side = side;
if (this.ticksSide === 'invert') side = -side;
if (this.standalone)
this.drawMainLine(axis_g);
const optionNoopt = false, // no ticks position optimization
optionInt = false, // integer labels
optionNoexp = false; // do not create exp
this.handle = this.createTicks(false, optionNoexp, optionNoopt, optionInt);
// first draw ticks
const tgaps = this.drawTicks(axis_g, side, true),
// draw labels
labelsPromise = this.optionUnlab ? Promise.resolve(tgaps) : this.drawLabels(axis_g, side, tgaps);
return labelsPromise.then(lgaps => {
// when drawing axis on frame, zoom rect should be always outside
this.addZoomingRect(axis_g, this.standalone ? side : this.side, lgaps);
return this.drawTitle(axis_g, side, lgaps);
});
}
/** @summary Assign handler, which is called when axis redraw by interactive changes
* @desc Used by palette painter to reassign interactive handlers
* @private */
setAfterDrawHandler(handler) {
this._afterDrawAgain = handler;
}
/** @summary Draw axis with the same settings, used by interactive changes */
drawAxisAgain() {
if (!this.axis_g || !this.side) return;
this.axis_g.selectAll('*').remove();
this.extractDrawAttributes();
let side = this.side;
if (this.ticksSide === 'invert') side = -side;
if (this.standalone)
this.drawMainLine(this.axis_g);
// first draw ticks
const tgaps = this.drawTicks(this.axis_g, side, false),
labelsPromise = this.optionUnlab ? Promise.resolve(tgaps) : this.drawLabels(this.axis_g, side, tgaps);
return labelsPromise.then(lgaps => {
// when drawing axis on frame, zoom rect should be always outside
this.addZoomingRect(this.axis_g, this.standalone ? side : this.side, lgaps);
return this.drawTitle(this.axis_g, side, lgaps);
}).then(() => {
if (isFunc(this._afterDrawAgain))
this._afterDrawAgain();
});
}
/** @summary Draw axis again on opposite frame size */
drawAxisOtherPlace(layer, transform, side, only_ticks) {
let axis_g = layer.selectChild(`.${this.name}_container2`);
if (axis_g.empty())
axis_g = layer.append('svg:g').attr('class', `${this.name}_container2`);
else
axis_g.selectAll('*').remove();
axis_g.attr('transform', transform);
if (this.ticksSide === 'invert')
side = -side;
// draw ticks and labels again
const tgaps = this.drawTicks(axis_g, side, false),
promise = this.optionUnlab || only_ticks ? Promise.resolve(tgaps) : this.drawLabels(axis_g, side, tgaps);
return promise.then(lgaps => {
this.addZoomingRect(axis_g, side, lgaps);
return true;
});
}
/** @summary Change zooming in standalone mode */
zoomStandalone(min, max) {
this.changeAxisAttr(1, 'zoomMin', min, 'zoomMax', max);
}
/** @summary Redraw axis, used in standalone mode for RAxisDrawable */
redraw() {
const drawable = this.getObject(),
pp = this.getPadPainter(),
pos = pp.getCoordinate(drawable.fPos),
reverse = this.v7EvalAttr('reverse', false),
labels_len = drawable.fLabels.length,
min = (labels_len > 0) ? 0 : this.v7EvalAttr('min', 0),
max = (labels_len > 0) ? labels_len : this.v7EvalAttr('max', 100);
let len = pp.getPadLength(drawable.fVertical, drawable.fLength);
// in vertical direction axis drawn in negative direction
if (drawable.fVertical) len -= pp.getPadHeight();
let smin = this.v7EvalAttr('zoomMin'),
smax = this.v7EvalAttr('zoomMax');
if (smin === smax) {
smin = min; smax = max;
}
this.configureAxis('axis', min, max, smin, smax, drawable.fVertical, undefined, len, { reverse, labels: labels_len > 0 });
this.createG();
this.standalone = true; // no need to clean axis container
const promise = this.drawAxis(this.draw_g, makeTranslate(pos.x, pos.y));
if (this.isBatchMode()) return promise;
return promise.then(() => {
if (settings.ContextMenu) {
this.draw_g.on('contextmenu', evnt => {
evnt.stopPropagation(); // disable main context menu
evnt.preventDefault(); // disable browser context menu
createMenu(evnt, this).then(menu => {
menu.header('RAxisDrawable', `${urlClassPrefix}ROOT_1_1Experimental_1_1RAxisBase.html`);
menu.add('Unzoom', () => this.zoomStandalone());
this.fillAxisContextMenu(menu, '');
menu.show();
});
});
}
addDragHandler(this, { x: pos.x, y: pos.y, width: this.vertical ? 10 : len, height: this.vertical ? len : 10,
only_move: true, redraw: d => this.positionChanged(d) });
this.draw_g.on('dblclick', () => this.zoomStandalone());
if (settings.ZoomWheel) {
this.draw_g.on('wheel', evnt => {
evnt.stopPropagation();
evnt.preventDefault();
const pos2 = d3_pointer(evnt, this.draw_g.node()),
coord = this.vertical ? (1 - pos2[1] / len) : pos2[0] / len,
item = this.analyzeWheelEvent(evnt, coord);
if (item.changed)
this.zoomStandalone(item.min, item.max);
});
}
});
}
/** @summary Process interactive moving of the axis drawing */
positionChanged(drag) {
const drawable = this.getObject(),
rect = this.getPadPainter().getPadRect(),
xn = drag.x / rect.width,
yn = 1 - drag.y / rect.height;
drawable.fPos.fHoriz.fArr = [xn];
drawable.fPos.fVert.fArr = [yn];
this.submitCanvExec(`SetPos({${xn.toFixed(4)},${yn.toFixed(4)}})`);
}
/** @summary Change axis attribute, submit changes to server and redraw axis when specified
* @desc Arguments as redraw_mode, name1, value1, name2, value2, ... */
changeAxisAttr(redraw_mode, ...args) {
const changes = {};
let indx = 0;
while (indx < args.length) {
this.v7AttrChange(changes, args[indx], args[indx + 1]);
this.v7SetAttr(args[indx], args[indx+1]);
indx += 2;
}
this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server
if (redraw_mode === 1) {
if (this.standalone)
this.redraw();
else
this.drawAxisAgain();
} else if (redraw_mode)
this.redrawPad();
}
/** @summary Change axis log scale kind */
changeAxisLog(arg) {
if ((this.kind === kAxisLabels) || (this.kind === kAxisTime)) return;
if (arg === 'toggle') arg = this.log ? 0 : 10;
arg = parseFloat(arg);
if (Number.isFinite(arg)) this.changeAxisAttr(2, 'log', arg, 'symlog', 0);
}
/** @summary Provide context menu for axis */
fillAxisContextMenu(menu, kind) {
if (kind) menu.add('Unzoom', () => this.getFramePainter().unzoom(kind));
menu.sub('Log scale', () => this.changeAxisLog('toggle'));
menu.addchk(!this.log && !this.symlog, 'linear', 0, arg => this.changeAxisLog(arg));
menu.addchk(this.log && !this.symlog && (this.logbase === 10), 'log10', () => this.changeAxisLog(10));
menu.addchk(this.log && !this.symlog && (this.logbase === 2), 'log2', () => this.changeAxisLog(2));
menu.addchk(this.log && !this.symlog && Math.abs(this.logbase - Math.exp(1)) < 0.1, 'ln', () => this.changeAxisLog(Math.exp(1)));
menu.addchk(!this.log && this.symlog, 'symlog', 0, () =>
menu.input('set symlog constant', this.symlog || 10, 'float').then(v => this.changeAxisAttr(2, 'symlog', v)));
menu.endsub();
menu.add('Divisions', () => menu.input('Set axis devisions', this.v7EvalAttr('ndiv', 508), 'int').then(val => this.changeAxisAttr(2, 'ndiv', val)));
menu.sub('Ticks');
menu.addRColorMenu('color', this.ticksColor, col => this.changeAxisAttr(1, 'ticks_color', col));
menu.addSizeMenu('size', 0, 0.05, 0.01, this.ticksSize/this.scalingSize, sz => this.changeAxisAttr(1, 'ticks_size', sz));
menu.addSelectMenu('side', ['normal', 'invert', 'both'], this.ticksSide, side => this.changeAxisAttr(1, 'ticks_side', side));
menu.endsub();
if (!this.optionUnlab && this.labelsFont) {
menu.sub('Labels');
menu.addSizeMenu('offset', -0.05, 0.05, 0.01, this.labelsOffset/this.scalingSize,
offset => this.changeAxisAttr(1, 'labels_offset', offset));
menu.addRAttrTextItems(this.labelsFont, { noangle: 1, noalign: 1 },
change => this.changeAxisAttr(1, 'labels_' + change.name, change.value));
menu.addchk(this.labelsFont.angle, 'rotate', res => this.changeAxisAttr(1, 'labels_angle', res ? 180 : 0));
menu.endsub();
}
menu.sub('Title', () => menu.input('Enter axis title', this.fTitle).then(t => this.changeAxisAttr(1, 'title_value', t)));
if (this.fTitle) {
menu.addSizeMenu('offset', -0.05, 0.05, 0.01, this.titleOffset/this.scalingSize,
offset => this.changeAxisAttr(1, 'title_offset', offset));
menu.addSelectMenu('position', ['left', 'center', 'right'], this.titlePos,
pos => this.changeAxisAttr(1, 'title_position', pos));
menu.addchk(this.isTitleRotated(), 'rotate', flag => this.changeAxisAttr(1, 'title_angle', flag ? 180 : 0));
menu.addRAttrTextItems(this.titleFont, { noangle: 1, noalign: 1 }, change => this.changeAxisAttr(1, 'title_' + change.name, change.value));
}
menu.endsub();
return true;
}
} // class RAxisPainter
export { RAxisPainter };