UNPKG

jsroot

Version:
1,296 lines (1,079 loc) 56.4 kB
import { gStyle, settings, constants, clTAxis, clTGaxis, isFunc, isStr } from '../core.mjs'; import { select as d3_select, drag as d3_drag, timeFormat as d3_timeFormat, utcFormat as d3_utcFormat, scaleTime as d3_scaleTime, scaleSymlog as d3_scaleSymlog, scaleLog as d3_scaleLog, scaleLinear as d3_scaleLinear } from '../d3.mjs'; import { floatToString, makeTranslate, addHighlightStyle } from '../base/BasePainter.mjs'; import { ObjectPainter, EAxisBits, kAxisLabels, kAxisNormal, kAxisFunc, kAxisTime } from '../base/ObjectPainter.mjs'; import { FontHandler } from '../base/FontHandler.mjs'; /** @summary Return time offset value for given TAxis object * @private */ function getTimeOffset(axis) { const dflt_time_offset = 788918400000; if (!axis) return dflt_time_offset; const idF = axis.fTimeFormat.indexOf('%F'); if (idF < 0) return gStyle.fTimeOffset * 1000; let sof = axis.fTimeFormat.slice(idF + 2); // default string in axis offset if (sof.indexOf('1995-01-01 00:00:00s0') === 0) return dflt_time_offset; // another default string with unix time if (sof.indexOf('1970-01-01 00:00:00s0') === 0) return 0; // special case, used from DABC painters if ((sof === '0') || (sof === '')) return 0; // decode time from ROOT string const next = (separ, min, max) => { const pos = sof.indexOf(separ); if (pos < 0) return min; const val = parseInt(sof.slice(0, pos)); sof = sof.slice(pos + 1); if (!Number.isInteger(val) || (val < min) || (val > max)) return min; return val; }, year = next('-', 1900, 2900), month = next('-', 1, 12) - 1, day = next(' ', 1, 31), hour = next(':', 0, 23), min = next(':', 0, 59), sec = next('s', 0, 59), msec = next(' ', 0, 999); let offset = Date.UTC(year, month, day, hour, min, sec, msec); // now also handle suffix like GMT or GMT -0600 sof = sof.toUpperCase(); if (sof.indexOf('GMT') === 0) { sof = sof.slice(4).trim(); if (sof.length > 3) { let p = 0, sign = 1000; if (sof[0] === '-') { p = 1; sign = -1000; } offset -= sign * (parseInt(sof.slice(p, p + 2)) * 3600 + parseInt(sof.slice(p + 2, p + 4)) * 60); } } return offset; } /** @summary Return true when GMT option configured in time format * @private */ function getTimeGMT(axis) { const fmt = axis?.fTimeFormat ?? ''; return (fmt.indexOf('gmt') > 0) || (fmt.indexOf('GMT') > 0); } /** @summary Tries to choose time format for provided time interval * @private */ function chooseTimeFormat(awidth, ticks) { if (awidth < 0.5) return ticks ? '%S.%L' : '%H:%M:%S.%L'; if (awidth < 30) return ticks ? '%Mm%S' : '%H:%M:%S'; awidth /= 60; if (awidth < 30) return ticks ? '%Hh%M' : '%d/%m %H:%M'; awidth /= 60; if (awidth < 12) return ticks ? '%d-%Hh' : '%d/%m/%y %Hh'; awidth /= 24; if (awidth < 15.218425) return ticks ? '%d/%m' : '%d/%m/%y'; awidth /= 30.43685; if (awidth < 6) return '%d/%m/%y'; awidth /= 12; if (awidth < 2) return ticks ? '%m/%y' : '%d/%m/%y'; return '%Y'; } /** * @summary Base axis painter methods * * @private */ const AxisPainterMethods = { initAxisPainter() { this.name = 'yaxis'; this.kind = kAxisNormal; this.func = null; this.order = 0; // scaling order for axis labels this.full_min = 0; this.full_max = 1; this.scale_min = 0; this.scale_max = 1; this.ticks = []; // list of major ticks }, /** @summary Cleanup axis painter */ cleanupAxisPainter() { this.ticks = []; delete this.format; delete this.func; delete this.tfunc1; delete this.tfunc2; delete this.gr; }, /** @summary Assign often used members of frame painter */ assignFrameMembers(fp, axis) { fp[`gr${axis}`] = this.gr; // fp.grx fp[`log${axis}`] = this.log; // fp.logx fp[`scale_${axis}min`] = this.scale_min; // fp.scale_xmin fp[`scale_${axis}max`] = this.scale_max; // fp.scale_xmax }, /** @summary Convert axis value into the Date object */ convertDate(v) { const dt = new Date(this.timeoffset + v*1000); let res = dt; if (!this.timegmt && settings.TimeZone) { try { const ms = dt.getMilliseconds(); res = new Date(dt.toLocaleString('en-US', { timeZone: settings.TimeZone })); res.setMilliseconds(ms); } catch { res = dt; } } return res; }, /** @summary Convert graphical point back into axis value */ revertPoint(pnt) { const value = this.func.invert(pnt); return this.kind === kAxisTime ? (value - this.timeoffset) / 1000 : value; }, /** @summary Provide label for time axis */ formatTime(dt, asticks) { return asticks ? this.tfunc1(dt) : this.tfunc2(dt); }, /** @summary Provide label for log axis */ formatLog(d, asticks, fmt) { const val = parseFloat(d), rnd = Math.round(val); if (!asticks) return ((rnd === val) && (Math.abs(rnd) < 1e9)) ? rnd.toString() : floatToString(val, fmt || gStyle.fStatFormat); if (val <= 0) return null; let vlog = Math.log10(val); const base = this.logbase; if (base !== 10) vlog /= Math.log10(base); if (this.moreloglabels || (Math.abs(vlog - Math.round(vlog)) < 0.001)) { if (!this.noexp && (asticks !== 2)) return this.formatExp(base, Math.floor(vlog + 0.01), val); if (Math.abs(base - Math.E) < 0.001) return floatToString(val, fmt || gStyle.fStatFormat); return (vlog < 0) ? val.toFixed(Math.round(-vlog + 0.5)) : val.toFixed(0); } return null; }, /** @summary Provide label for normal axis */ formatNormal(d, asticks, fmt) { let val = parseFloat(d); if (asticks && this.order) val /= Math.pow(10, this.order); if (gStyle.fStripDecimals && (val === Math.round(val))) return Math.abs(val) < 1e9 ? val.toFixed(0) : val.toExponential(4); if (asticks) { if (this.ndig > 10) return val.toExponential(this.ndig - 11); let res = val.toFixed(this.ndig); const p = res.indexOf('.'); if ((p > 0) && settings.StripAxisLabels) { while ((res.length >= p) && ((res.at(-1) === '0') || (res.at(-1) === '.'))) res = res.slice(0, res.length - 1); } return res; } return floatToString(val, fmt || '8.6g'); }, /** @summary Provide label for exponential form */ formatExp(base, order, value) { let res = ''; const sbase = Math.abs(base - Math.E) < 0.001 ? 'e' : base.toString(); if (value) { value = Math.round(value/Math.pow(base, order)); if (settings.StripAxisLabels) { if (order === 0) return value.toString(); else if ((order === 1) && (value === 1)) return sbase; } if (value !== 1) res = value.toString() + (settings.Latex ? '#times' : 'x'); } res += sbase; if (settings.Latex > constants.Latex.Symbols) return res + `^{${order}}`; const superscript_symbols = { 0: '\u2070', 1: '\xB9', 2: '\xB2', 3: '\xB3', 4: '\u2074', 5: '\u2075', 6: '\u2076', 7: '\u2077', 8: '\u2078', 9: '\u2079', '-': '\u207B' }, str = order.toString(); for (let n = 0; n < str.length; ++n) res += superscript_symbols[str[n]]; return res; }, /** @summary Convert 'raw' axis value into text */ axisAsText(value, fmt) { if (this.kind === kAxisTime) value = this.convertDate(value); if (this.format) return this.format(value, false, fmt); return value.toPrecision(4); }, /** @summary Produce ticks for d3.scaleLog * @desc Fixing following problem, described [here]{@link https://stackoverflow.com/questions/64649793} */ poduceLogTicks(func, number) { const linearArray = arr => { let sum1 = 0, sum2 = 0; for (let k = 1; k < arr.length; ++k) { const diff = (arr[k] - arr[k-1]); sum1 += diff; sum2 += diff**2; } const mean = sum1/(arr.length - 1), dev = sum2/(arr.length - 1) - mean**2; if (dev <= 0) return true; if (Math.abs(mean) < 1e-100) return false; return Math.sqrt(dev)/mean < 1e-6; }; let arr = func.ticks(number); while ((number > 4) && linearArray(arr)) { number = Math.round(number*0.8); arr = func.ticks(number); } // if still linear array, try to sort out 'bad' ticks if ((number < 5) && linearArray(arr) && this.logbase && (this.logbase !== 10)) { const arr2 = []; arr.forEach(val => { const pow = Math.log10(val) / Math.log10(this.logbase); if (Math.abs(Math.round(pow) - pow) < 0.01) arr2.push(val); }); if (arr2.length > 0) arr = arr2; } return arr; }, /** @summary Produce axis ticks */ produceTicks(ndiv, ndiv2) { if (!this.noticksopt) { const total = ndiv * (ndiv2 || 1); if (this.log) return this.poduceLogTicks(this.func, total); const dom = this.func.domain(), check = ticks => { if (ticks.length <= total) return true; if (ticks.length > total + 1) return false; return (ticks[0] === dom[0]) || (ticks[total] === dom[1]); // special case of N+1 ticks, but match any range }, res1 = this.func.ticks(total); if (ndiv2 || check(res1)) return res1; const res2 = this.func.ticks(Math.round(total * 0.7)); return (res2.length > 2) && check(res2) ? res2 : res1; } const dom = this.func.domain(), ticks = []; if (ndiv2) ndiv = (ndiv-1) * ndiv2; for (let n = 0; n <= ndiv; ++n) ticks.push((dom[0]*(ndiv-n) + dom[1]*n)/ndiv); return ticks; }, /** @summary Method analyze mouse wheel event and returns item with suggested zooming range */ analyzeWheelEvent(evnt, dmin, item, test_ignore) { if (!item) item = {}; let delta = 0, delta_left = 1, delta_right = 1; if ('dleft' in item) { delta_left = item.dleft; delta = 1; } if ('dright' in item) { delta_right = item.dright; delta = 1; } if (item.delta) delta = item.delta; else if (evnt) delta = evnt.wheelDelta ? -evnt.wheelDelta : (evnt.deltaY || evnt.detail); if (!delta || (test_ignore && item.ignore)) return; delta = (delta < 0) ? -0.2 : 0.2; delta_left *= delta; delta_right *= delta; const lmin = item.min = this.scale_min, lmax = item.max = this.scale_max, gmin = this.full_min, gmax = this.full_max; if ((item.min === item.max) && (delta < 0)) { item.min = gmin; item.max = gmax; } if (item.min >= item.max) return; if (item.reverse) dmin = 1 - dmin; if ((dmin > 0) && (dmin < 1)) { if (this.log) { let factor = (item.min > 0) ? Math.log10(item.max/item.min) : 2; if (factor > 10) factor = 10; else if (factor < 0.01) factor = 0.01; item.min /= Math.pow(10, factor * delta_left * dmin); item.max *= Math.pow(10, factor * delta_right * (1 - dmin)); // special handling for Z scale - limit zooming of color scale if (this.minposbin && this.name === 'zaxis') item.min = Math.max(item.min, 0.3*this.minposbin); } else if ((delta_left === -delta_right) && !item.reverse) { // shift left/right, try to keep range constant let delta_shift = (item.max - item.min) * delta_right * dmin; if ((Math.round(item.max) === item.max) && (Math.round(item.min) === item.min) && (Math.abs(delta_shift) > 1)) delta_shift = Math.round(delta_shift); if (item.min + delta_shift < gmin) delta_shift = gmin - item.min; else if (item.max + delta_shift > gmax) delta_shift = gmax - item.max; if (delta_shift !== 0) { item.min += delta_shift; item.max += delta_shift; } else { delete item.min; delete item.max; } } else { let rx_left = (item.max - item.min), rx_right = rx_left; if (delta_left > 0) rx_left = 1.001 * rx_left / (1-delta_left); item.min += -delta_left*dmin*rx_left; if (delta_right > 0) rx_right = 1.001 * rx_right / (1-delta_right); item.max -= -delta_right*(1-dmin)*rx_right; } if (item.min >= item.max) item.min = item.max = undefined; else if (delta_left !== delta_right) { // extra check case when moving left or right if (((item.min < gmin) && (lmin === gmin)) || ((item.max > gmax) && (lmax === gmax))) item.min = item.max = undefined; } else { item.min = Math.max(item.min, gmin); item.max = Math.min(item.max, gmax); } } else item.min = item.max = undefined; item.changed = ((item.min !== undefined) && (item.max !== undefined)); return item; } }; // AxisPainterMethods /** * @summary Painter for TAxis object * * @private */ class TAxisPainter extends ObjectPainter { /** @summary constructor * @param {object|string} dom - identifier or dom element * @param {object} axis - object to draw * @param {boolean} embedded - if true, painter used in other objects painters */ constructor(dom, axis, embedded) { super(dom, axis); this.is_gaxis = axis?._typename === clTGaxis; Object.assign(this, AxisPainterMethods); this.initAxisPainter(); this.embedded = embedded; // indicate that painter embedded into the histogram painter this.invert_side = false; this.lbls_both_sides = false; // draw labels on both sides } /** @summary cleanup painter */ cleanup() { this.cleanupAxisPainter(); delete this.hist_painter; delete this.hist_axis; delete this.is_gaxis; super.cleanup(); } /** @summary Use in GED to identify kind of axis */ getAxisType() { return clTAxis; } /** @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, range, opts) { const axis = this.getObject(); this.name = name; this.full_min = min; this.full_max = max; this.kind = kAxisNormal; this.vertical = vertical; this.log = opts.log || 0; this.minposbin = opts.minposbin; this.ignore_labels = opts.ignore_labels; this.noexp_changed = opts.noexp_changed; this.symlog = opts.symlog || false; this.reverse = opts.reverse || false; // special flag to change align of labels on vertical axis // it is workaround shown in TGaxis docu this.reverseAlign = this.vertical && this.reverse && this.is_gaxis && (axis.fX1 !== axis.fX2); this.swap_side = opts.swap_side || false; this.fixed_ticks = opts.fixed_ticks || null; this.maxTickSize = opts.maxTickSize || 0; this.value_axis = opts.value_axis ?? false; // use fMinimum/fMaximum from source object if (opts.time_scale || axis.fTimeDisplay) { this.kind = kAxisTime; this.timeoffset = getTimeOffset(axis); this.timegmt = getTimeGMT(axis); } else if (opts.axis_func) this.kind = kAxisFunc; else this.kind = !axis.fLabels || this.ignore_labels ? kAxisNormal : kAxisLabels; if (this.kind === kAxisTime) this.func = d3_scaleTime().domain([this.convertDate(smin), this.convertDate(smax)]); else if (this.log) { if ((this.log === 1) || (this.log === 10)) this.logbase = 10; else if (this.log === 3) this.logbase = Math.E; else this.logbase = Math.round(this.log); if (smax <= 0) smax = 1; if (opts.log_min_nz) this.log_min_nz = opts.log_min_nz; else if (axis && opts.logcheckmin) { let v = 0; for (let i = 0; i < axis.fNbins; ++i) { v = axis.GetBinLowEdge(i+1); if (v > 0) break; v = axis.GetBinCenter(i+1); if (v > 0) break; } if (v > 0) this.log_min_nz = v; } if ((smin <= 0) && this.log_min_nz) smin = this.log_min_nz; if ((smin <= 0) || (smin >= smax)) smin = smax * (opts.logminfactor || 1e-4); if (this.kind === kAxisFunc) this.func = this.createFuncHandle(opts.axis_func, this.logbase, smin, smax); else this.func = d3_scaleLog().base(this.logbase).domain([smin, smax]); } else if (this.symlog) { let v = Math.max(Math.abs(smin), Math.abs(smax)); if (Number.isInteger(this.symlog) && (this.symlog > 0)) v *= Math.pow(10, -1*this.symlog); else v *= 0.01; this.func = d3_scaleSymlog().constant(v).domain([smin, smax]); } else if (this.kind === kAxisFunc) this.func = this.createFuncHandle(opts.axis_func, 0, smin, smax); else this.func = d3_scaleLinear().domain([smin, smax]); if (this.vertical ^ this.reverse) { const d = range[0]; range[0] = range[1]; range[1] = d; } this.func.range(range); this.scale_min = smin; this.scale_max = smax; 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 let ndiv = 508; if (this.is_gaxis) ndiv = axis.fNdiv; else if (axis) { if (!axis.fNdivisions) ndiv = 0; else ndiv = Math.max(axis.fNdivisions, 4); } this.nticks = ndiv % 100; this.nticks2 = (ndiv % 10000 - this.nticks) / 100; this.nticks3 = Math.floor(ndiv/10000); if (axis && !this.is_gaxis && (this.nticks > 20)) this.nticks = 20; let gr_range = Math.abs(this.func.range()[1] - this.func.range()[0]); if (gr_range <= 0) gr_range = 100; if (this.kind === kAxisTime) { if (this.nticks > 8) this.nticks = 8; const scale_range = this.scale_max - this.scale_min, idF = axis.fTimeFormat.indexOf('%F'), tf2 = chooseTimeFormat(scale_range / gr_range, false); let tf1 = (idF >= 0) ? axis.fTimeFormat.slice(0, idF) : axis.fTimeFormat; if (!tf1 || (scale_range < 0.1 * (this.full_max - this.full_min))) tf1 = chooseTimeFormat(scale_range / this.nticks, true); this.tfunc1 = this.tfunc2 = this.timegmt ? d3_utcFormat(tf1) : d3_timeFormat(tf1); if (tf2 !== tf1) this.tfunc2 = this.timegmt ? d3_utcFormat(tf2) : 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 = axis?.TestBit(EAxisBits.kNoExponent); if ((this.scale_max < 300) && (this.scale_min > 0.3) && !this.noexp_changed && (this.log === 1)) this.noexp = true; this.moreloglabels = axis?.TestBit(EAxisBits.kMoreLogLabels); 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.regular_labels = true; if (axis?.fNbins && axis?.fLabels) { if ((axis.fNbins !== Math.round(axis.fXmax - axis.fXmin)) || (axis.fXmin !== 0) || (axis.fXmax !== axis.fNbins)) this.regular_labels = false; } this.nticks2 = 1; this.format = this.formatLabels; } else { this.order = 0; this.ndig = 0; this.format = this.formatNormal; } } /** @summary Check zooming value for log scale * @private */ checkZoomMin(value) { return this.log && this.log_min_nz ? Math.max(value, this.log_min_nz) : value; } /** @summary Return scale min */ getScaleMin() { return this.func?.domain()[0] ?? 0; } /** @summary Return scale max */ getScaleMax() { return this.func?.domain()[1] ?? 0; } /** @summary Return true if labels may be removed while they are not fit to graphical range */ cutLabels() { if (!settings.CutAxisLabels) return false; if (isStr(settings.CutAxisLabels)) return settings.CutAxisLabels.indexOf(this.name) >= 0; return this.vertical; // cut vertical axis by default } /** @summary Provide label for axis value */ formatLabels(d) { const a = this.getObject(); let indx = parseFloat(d); if (!this.regular_labels) indx = Math.round((indx - a.fXmin)/(a.fXmax - a.fXmin) * a.fNbins); else indx = Math.floor(indx); if ((indx < 0) || (indx >= a.fNbins)) return null; const arr = a.fLabels.arr; for (let i = 0; i < arr.length; ++i) { if (arr[i].fUniqueID === indx+1) return arr[i].fString; } 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 handle = { painter: this, nminor: 0, nmiddle: 0, nmajor: 0, func: this.func, minor: [], middle: [], major: [] }; let ticks = []; if (this.fixed_ticks) { this.fixed_ticks.forEach(v => { if ((v >= this.scale_min) && (v <= this.scale_max)) ticks.push(v); }); } else if (this.kind === kAxisLabels) { handle.lbl_pos = []; const axis = this.getObject(); for (let n = 0; n <= axis.fNbins; ++n) { const x = this.regular_labels ? n : axis.fXmin + n / axis.fNbins * (axis.fXmax - axis.fXmin); if ((x >= this.scale_min) && (x <= this.scale_max)) { handle.lbl_pos.push(x); ticks.push(x); } } } else ticks = this.produceTicks(this.nticks); handle.minor = handle.middle = handle.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)) && !this.fixed_ticks) { 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 this.painter.findLabelModifier(this.painter.getObject(), this.nmajor-1, this.major); }; 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.kind === kAxisFunc)) && !this.log && (handle.major.length > 0)) { let maxorder = 0, minorder = 0, exclorder3 = false; if (!optionNoexp && !this.cutLabels()) { 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); const p = lbl.indexOf('.'); if (!order && !optionNoexp && ((p > gStyle.fAxisMaxDigits) || ((p < 0) && (lbl.length > gStyle.fAxisMaxDigits)))) { totallen += 1e10; // do not use order = 0 when too many digits are there exclorder3 = false; } totallen += lbl.length; indx++; continue; } if (++this.ndig > 15) 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.log) return false; return this.getObject()?.TestBit(EAxisBits.kCenterLabels); } /** @summary Is labels should be rotated */ isRotateLabels() { return this.getObject()?.TestBit(EAxisBits.kLabelsVert); } /** @summary Is title should be rotated */ isRotateTitle() { return this.getObject()?.TestBit(EAxisBits.kRotateTitle); } /** @summary Add interactive elements to draw axes title */ addTitleDrag(title_g, vertical, offset_k, reverse, axis_length) { if (!settings.MoveResize || this.isBatchMode()) return; let drag_rect = null, x_0, y_0, i_0, acc_x, acc_y, new_x, new_y, sign_0, alt_pos, curr_indx, can_indx0 = true; 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 = vertical ? box.height : box.width; x_0 = new_x = acc_x = title_g.property('shift_x'); y_0 = new_y = acc_y = title_g.property('shift_y'); sign_0 = vertical ? (acc_x > 0) : (acc_y > 0); // sign should remain can_indx0 = !this.hist_painter?.snapid; // online canvas does not allow alternate position alt_pos = vertical ? [axis_length, axis_length/2, 0] : [0, axis_length/2, axis_length]; // possible positions const off = vertical ? -title_length/2 : title_length/2; if (this.title_align === 'middle') { alt_pos[0] += off; alt_pos[2] -= off; } else if (this.title_align === 'begin') { alt_pos[1] -= off; alt_pos[2] -= 2*off; } else { // end alt_pos[0] += 2*off; alt_pos[1] += off; } if (this.titleCenter) curr_indx = 1; else if ((reverse ^ this.titleOpposite) && can_indx0) curr_indx = 0; else curr_indx = 2; alt_pos[curr_indx] = vertical ? acc_y : acc_x; i_0 = curr_indx; 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; let set_x, set_y, besti = can_indx0 ? 0 : 1; const p = vertical ? acc_y : acc_x; for (let i = 1; i < 3; ++i) { if (Math.abs(p - alt_pos[i]) < Math.abs(p - alt_pos[besti])) besti = i; } if (vertical) { set_x = acc_x; set_y = alt_pos[besti]; } else { set_y = acc_y; set_x = alt_pos[besti]; } if (sign_0 === (vertical ? (set_x > 0) : (set_y > 0))) { 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(); title_g.property('shift_x', new_x) .property('shift_y', new_y); const axis = this.getObject(), axis2 = this.source_axis, setBit = (bit, on) => { axis?.SetBit(bit, on); axis2?.SetBit(bit, on); }; this.titleOffset = (vertical ? new_x : new_y) / offset_k; const offset = this.titleOffset / this.offsetScaling / this.titleSize; if (axis) axis.fTitleOffset = offset; if (axis2) axis2.fTitleOffset = offset; if (curr_indx === 1) { setBit(EAxisBits.kCenterTitle, true); this.titleCenter = true; setBit(EAxisBits.kOppositeTitle, false); this.titleOpposite = false; } else if (curr_indx === 0) { setBit(EAxisBits.kCenterTitle, false); this.titleCenter = false; setBit(EAxisBits.kOppositeTitle, true); this.titleOpposite = true; } else { setBit(EAxisBits.kCenterTitle, false); this.titleCenter = false; setBit(EAxisBits.kOppositeTitle, false); this.titleOpposite = false; } drag_rect.remove(); drag_rect = null; if ((x_0 !== new_x) || (y_0 !== new_y) || (i_0 !== curr_indx)) this.submitAxisExec(`SetTitleOffset(${offset});;SetBit(${EAxisBits.kCenterTitle},${this.titleCenter?1:0})`); if (this.hist_painter && this.hist_axis) this.hist_painter.getCanvPainter()?.producePadEvent('select', this.hist_painter.getPadPainter(), this); }); title_g.style('cursor', 'move').call(drag_move); } /** @summary Configure hist painter which creates axis - to be able submit execs * @private */ setHistPainter(hist_painter, axis_name) { this.hist_painter = hist_painter; this.hist_axis = axis_name; } /** @summary Submit exec for the axis - if possible * @private */ submitAxisExec(exec, only_gaxis) { const snapid = this.hist_painter?.snapid; if (snapid && this.hist_axis && !only_gaxis) this.submitCanvExec(exec, `${snapid}#${this.hist_axis}`); else if (this.is_gaxis) this.submitCanvExec(exec); } /** @summary Produce svg path for axis ticks */ produceTicksPath(handle, side, tickSize, ticksPlusMinus, secondShift, real_draw) { let path1 = '', path2 = ''; this.ticks = []; while (handle.next(true)) { let h1 = Math.round(tickSize/4), h2 = 0; if (handle.kind < 3) h1 = Math.round(tickSize/2); if (handle.kind === 1) { // if not showing labels, not show large tick // FIXME: for labels last tick is smaller, if (/* (this.kind === kAxisLabels) || */ (this.format(handle.tick, true) !== null)) h1 = tickSize; this.ticks.push(handle.grpos); // keep graphical positions of major ticks } if (ticksPlusMinus > 0) h2 = -h1; else if (side < 0) { h2 = -h1; h1 = 0; } path1 += this.vertical ? `M${h1},${handle.grpos}H${h2}` : `M${handle.grpos},${-h1}V${-h2}`; if (secondShift) path2 += this.vertical ? `M${secondShift-h1},${handle.grpos}H${secondShift-h2}` : `M${handle.grpos},${secondShift+h1}V${secondShift+h2}`; } return real_draw ? path1 + path2 : ''; } /** @summary Returns modifier for axis label */ findLabelModifier(axis, nlabel, positions) { if (!axis.fModLabs) return null; for (let n = 0; n < axis.fModLabs.arr.length; ++n) { const mod = axis.fModLabs.arr[n]; if ((mod.fLabValue !== undefined) && (mod.fLabNum === 0)) { const eps = this.log ? positions[nlabel]*1e-6 : (this.scale_max - this.scale_min)*1e-6; if (Math.abs(mod.fLabValue - positions[nlabel]) < eps) return mod; } if ((mod.fLabNum === nlabel + 1) || ((mod.fLabNum < 0) && (nlabel === positions.length + mod.fLabNum))) return mod; } return null; } /** @summary Draw axis labels * @return {Promise} with array label size and max width */ async drawLabels(axis_g, axis, w, h, handle, side, labelsFont, labeloffset, tickSize, ticksPlusMinus, max_text_width, frame_ygap) { const center_lbls = this.isCenteredLabels(), label_g = [axis_g.append('svg:g').attr('class', 'axis_labels')], lbl_pos = handle.lbl_pos || handle.major, tilt_angle = gStyle.AxisTiltAngle ?? 25; let rotate_lbls = this.isRotateLabels(), textscale = 1, flipscale = 1, maxtextlen = 0, applied_scale = 0, lbl_tilt = false, any_modified = false, max_textwidth = 0, max_tiltsize = 0; if (this.lbls_both_sides) label_g.push(axis_g.append('svg:g').attr('class', 'axis_labels').attr('transform', this.vertical ? `translate(${w})` : `translate(0,${-h})`)); if (frame_ygap > 0) max_tiltsize = frame_ygap / Math.sin(tilt_angle/180*Math.PI) - Math.tan(tilt_angle/180*Math.PI); // function called when text is drawn to analyze width, required to correctly scale all labels // must be function to correctly handle 'this' argument function process_drawtext_ready(painter) { const textwidth = this.result_width; max_textwidth = Math.max(max_textwidth, textwidth); const maxwidth = !this.gap_before ? 0.9*this.gap_after : (!this.gap_after ? 0.9*this.gap_before : this.gap_before*0.45 + this.gap_after*0.45); if (!painter.vertical && !rotate_lbls && this.result_height && maxwidth) flipscale = Math.min(flipscale, maxwidth/this.result_height); if (textwidth && ((!painter.vertical && !rotate_lbls) || (painter.vertical && rotate_lbls)) && !painter.log) textscale = Math.min(textscale, maxwidth / textwidth); else if (painter.vertical && max_text_width && this.normal_side && (max_text_width - labeloffset > 20) && (textwidth > max_text_width - labeloffset)) textscale = Math.min(textscale, (max_text_width - labeloffset) / textwidth); if ((textscale > 0.0001) && (textscale < 0.7) && !any_modified && !painter.vertical && !rotate_lbls && (label_g.length === 1) && (lbl_tilt === false)) { if (maxtextlen > 5) lbl_tilt = true; } let scale = textscale; if (lbl_tilt) { if (max_tiltsize && max_textwidth) { scale = Math.min(1, 0.8*max_tiltsize/max_textwidth); if (scale < textscale) { // if due to tilt scale is even smaller - ignore tilting lbl_tilt = 0; scale = textscale; } } else scale *= 3; } if (((scale > 0.0001) && (scale < 1)) || (lbl_tilt !== false)) { applied_scale = 1/scale; painter.scaleTextDrawing(applied_scale, label_g[0]); } } // check if short labels can be rotated if (!this.vertical && this.regular_labels && !rotate_lbls) { let tlen = 0; for (let nmajor = 0; nmajor < lbl_pos.length; ++nmajor) { const text = this.format(lbl_pos[nmajor], true); if (text) tlen = Math.max(tlen, text.length); } if ((tlen > 2) && (tlen <= 5) && (lbl_pos.length * labelsFont.size > w / 2)) { rotate_lbls = true; lbl_tilt = 0; } } let pr = Promise.resolve(); for (let lcnt = 0; lcnt < label_g.length; ++lcnt) { if (lcnt > 0) side = -side; pr = pr.then(() => this.startTextDrawingAsync(labelsFont, 'font', label_g[lcnt])).then(() => { let lastpos = 0; const fix_coord = this.vertical ? -labeloffset * side : labeloffset * side + ticksPlusMinus * tickSize; for (let nmajor = 0; nmajor < lbl_pos.length; ++nmajor) { let text = this.format(lbl_pos[nmajor], true); if (text === null) continue; const mod = this.findLabelModifier(axis, nmajor, lbl_pos); if (mod?.fTextSize === 0) continue; if (mod) any_modified = true; if (mod?.fLabText) text = mod.fLabText; const arg = { text, color: labelsFont.color, latex: 1, draw_g: label_g[lcnt], normal_side: (lcnt === 0) }; let pos = Math.round(this.func(lbl_pos[nmajor])); if (mod?.fTextColor > 0) arg.color = this.getColor(mod.fTextColor); 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 !== this.reverse) ? 0.5 * gap : -0.5 * gap)); if ((pos < -5) || (pos > (this.vertical ? h : w) + 5)) continue; } maxtextlen = Math.max(maxtextlen, text.length); if (this.vertical) { arg.x = fix_coord; arg.y = pos; arg.align = rotate_lbls ? (this.optionLeft || this.reverseAlign ? 23 : 21) : (this.optionLeft || this.reverseAlign ? 12 : 32); if (this.cutLabels()) { const gap = labelsFont.size * (rotate_lbls ? 1.5 : 0.6); if ((pos < gap) || (pos > h - gap)) continue; } } else { arg.x = pos; arg.y = fix_coord; arg.align = rotate_lbls ? ((side < 0) ? 12 : 32) : ((side < 0) ? 21 : 23); if (this.log && !this.noexp && !this.vertical && arg.align === 23) { arg.align = 21; arg.y += labelsFont.size; } else if (arg.align % 10 === 3) arg.y -= labelsFont.size*0.1; // font takes 10% more by top align if (this.cutLabels()) { const gap = labelsFont.size * (rotate_lbls ? 0.4 : 1.5); if ((pos < gap) || (pos > w - gap)) continue; } } if (rotate_lbls) arg.rotate = 270; else if (mod && mod.fTextAngle !== -1) arg.rotate = -mod.fTextAngle; // only for major text drawing scale factor need to be checked // for modified labels ignore scaling if ((lcnt === 0) && !mod?.fLabText) arg.post_process = process_drawtext_ready; this.drawText(arg); // workaround for symlog where labels can be compressed to close if (this.symlog && lastpos && (pos !== lastpos) && ((this.vertical && !rotate_lbls) || (!this.vertical && rotate_lbls))) { const axis_step = Math.abs(pos - lastpos); textscale = Math.min(textscale, 1.1*axis_step/labelsFont.size); } lastpos = pos; } if (this.order) { let xoff = 0, yoff = 0; if (this.name === 'xaxis') { xoff = gStyle.fXAxisExpXOffset || 0; yoff = gStyle.fXAxisExpYOffset || 0; } else if (this.name === 'yaxis') { xoff = gStyle.fYAxisExpXOffset || 0; yoff = gStyle.fYAxisExpYOffset || 0; } if (xoff) xoff = Math.round(xoff * (this.getPadPainter()?.getPadWidth() ?? 0)); if (yoff) yoff = Math.round(yoff * (this.getPadPainter()?.getPadHeight() ?? 0)); this.drawText({ color: labelsFont.color, x: xoff + (this.vertical ? side*5 : w+5), y: yoff + (this.has_obstacle ? fix_coord : (this.vertical ? -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[lcnt] }); } if ((lcnt > 1) && applied_scale) this.scaleTextDrawing(applied_scale, label_g[lcnt]); return this.finishTextDrawing(label_g[lcnt], true); }); } return pr.then(() => { this._maxlbllen = maxtextlen; // for internal use in palette painter if (lbl_tilt) { label_g[0].selectAll('text').each(function() { const txt = d3_select(this), tr = txt.attr('transform'); if (lbl_tilt) txt.attr('transform', `${tr} rotate(${tilt_angle})`).style('text-anchor', 'start'); }); } return max_textwidth; }); } /** @summary Extract major draw attributes, which are also used in interactive operations * @private */ extractDrawAttributes(scalingSize, w, h) { const axis = this.getObject(); let pp = this.getPadPainter(); if (axis.$use_top_pad) pp = pp?.getPadPainter(); // workaround for ratio plot const pad_w = pp?.getPadWidth() || scalingSize || w/0.8, // use factor 0.8 as ratio between frame and pad size pad_h = pp?.getPadHeight() || scalingSize || h/0.8, // if no external scaling size use scaling as in TGaxis.cxx:1448 - NDC axis length is in the scaling factor tickScalingSize = scalingSize || (this.vertical ? h/pad_h*pad_w : w/pad_w*pad_h), bit_plus = axis.TestBit(EAxisBits.kTickPlus), bit_minus = axis.TestBit(EAxisBits.kTickMinus); let tickSize, titleColor, titleFontId, offset; this.scalingSize = scalingSize || Math.max(Math.min(pad_w, pad_h), 10); if (this.is_gaxis) { const optionSize = axis.fChopt.indexOf('S') >= 0; this.optionUnlab = axis.fChopt.indexOf('U') >= 0; this.optionMinus = (axis.fChopt.indexOf('-') >= 0) || bit_minus; this.optionPlus = (axis.fChopt.indexOf('+') >= 0) || bit_plus; this.optionNoopt = (axis.fChopt.indexOf('N') >= 0); // no ticks position optimization this.optionInt = (axis.fChopt.indexOf('I') >= 0); // integer labels this.optionText = (axis.fChopt.indexOf('T') >= 0); // text scaling? this.optionLeft = (axis.fChopt.indexOf('L') >= 0); // left text align this.optionRight = (axis.fChopt.indexOf('R') >= 0); // right text align this.optionCenter = (axis.fChopt.indexOf('C') >= 0); // center text align this.createAttLine({ attr: axis }); tickSize = optionSize ? axis.fTickSize : 0.03; titleColor = this.getColor(axis.fTextColor); titleFontId = axis.fTextFont; offset = axis.fLabelOffset; // workaround for old reverse axes where offset is not properly working if (this.reverse && (!this.vertical || (!this.optionMinus && (axis.fX1 !== axis.fX2)))) offset = -offset; } else { this.optionUnlab = false; if (!bit_plus && !bit_minus) { this.optionMinus = this.vertical ^ this.invert_side; this.optionPlus = !this.optionMinus; } else { this.optionPlus = bit_plus; this.optionMinus = bit_minus; } this.optionNoopt = false; // no ticks position optimization this.optionInt = false; // integer labels this.optionText = false; this.createAttLine({ color: axis.fAxisColor, width: 1, style: 1 }); tickSize = axis.fTickLength; titleColor = this.getColor(axis.fTitleColor); titleFontId = axis.fTitleFont; offset = axis.fLabelOffset; } offset += (this.vertical ? 0.002 : 0.005); if (this.kind === kAxisLabels) this.optionText = true; this.optionNoexp = axis.TestBit(EAxisBits.kNoExponent); this.ticksSize = Math.round(tickSize * tickScalingSize); if (scalingSize && (this.ticksSize < 0)) this.ticksSize = -this.ticksSize; if (this.maxTickSize && (this.ticksSize > this.maxTickSize)) this.ticksSize = this.maxTickSize; // now used only in 3D drawing this.ticksColor = this.lineatt.color; this.ticksWidth = this.lineatt.width; const k = this.optionText ? 0.66666 : 1; // set TGaxis.cxx, line 1504 this.labelSize = Math.round((axis.fLabelSize < 1) ? k * axis.fLabelSize * this.scalingSize : k * axis.fLabelSize); this.labelsOffset = Math.round(offset * this.scalingSize); this.labelsFont = new FontHandler(axis.fLabelFont, this.labelSize, scalingSize); if ((this.labelSize <= 0) || (Math.abs(axis.fLabelOffset) > 1.1)) this.optionUnlab = true; // disable labels when size not specified this.labelsFont.setColor(this.getColor(axis.fLabelColor)); this.fTitle = axis.fTitle; if (this.fTitle) { this.titleSize = (axis.fTitleSize >= 1) ? axis.fTitleSize : Math.round(axis.fTitleSize * this.scalingSize); this.titleFont = new FontHandler(titleFontId, this.titleSize, scalingSize); this.titleFont.setColor(titleColor); this.offsetScaling = (axis.fTitleSize >= 1) ? 1 : (this.vertical ? pad_w : pad_h) / this.scalingSize; this.titleOffset = axis.fTitleOffset; if (!this.titleOffset && this.name[0] === 'x') this.titleOffset = gStyle.fXaxis.fTitleO