UNPKG

jsroot

Version:
484 lines (386 loc) 16.6 kB
import { settings, isFunc, isStr, gStyle, nsREX } from '../core.mjs'; import { makeTranslate } from '../base/BasePainter.mjs'; import { RObjectPainter } from '../base/RObjectPainter.mjs'; import { ensureRCanvas } from '../gpad/RCanvasPainter.mjs'; import { addDragHandler } from '../gpad/TFramePainter.mjs'; import { createMenu } from '../gui/menu.mjs'; const ECorner = { kTopLeft: 1, kTopRight: 2, kBottomLeft: 3, kBottomRight: 4 }; /** * @summary Painter for RPave class * * @private */ class RPavePainter extends RObjectPainter { /** @summary Draw pave content * @desc assigned depending on pave class */ async drawContent() { return this; } /** @summary Draw pave */ async drawPave() { const rect = this.getPadPainter().getPadRect(), fp = this.getFramePainter(); this.onFrame = fp && this.v7EvalAttr('onFrame', true); this.corner = this.v7EvalAttr('corner', ECorner.kTopRight); const visible = this.v7EvalAttr('visible', true), offsetx = this.v7EvalLength('offsetX', rect.width, 0.02), offsety = this.v7EvalLength('offsetY', rect.height, 0.02), pave_width = this.v7EvalLength('width', rect.width, 0.3), pave_height = this.v7EvalLength('height', rect.height, 0.3); this.createG(); this.draw_g.classed('most_upper_primitives', true); // this primitive will remain on top of list if (!visible) return this; this.createv7AttLine('border_'); this.createv7AttFill(); const fr = this.onFrame ? fp.getFrameRect() : rect; let pave_x = 0, pave_y = 0; switch (this.corner) { case ECorner.kTopLeft: pave_x = fr.x + offsetx; pave_y = fr.y + offsety; break; case ECorner.kBottomLeft: pave_x = fr.x + offsetx; pave_y = fr.y + fr.height - offsety - pave_height; break; case ECorner.kBottomRight: pave_x = fr.x + fr.width - offsetx - pave_width; pave_y = fr.y + fr.height - offsety - pave_height; break; case ECorner.kTopRight: default: pave_x = fr.x + fr.width - offsetx - pave_width; pave_y = fr.y + offsety; } makeTranslate(this.draw_g, pave_x, pave_y); this.draw_g.append('svg:rect') .attr('x', 0) .attr('width', pave_width) .attr('y', 0) .attr('height', pave_height) .call(this.lineatt.func) .call(this.fillatt.func); this.pave_width = pave_width; this.pave_height = pave_height; // here should be fill and draw of text return this.drawContent().then(() => { if (!this.isBatchMode()) { // TODO: provide pave context menu as in v6 if (settings.ContextMenu && this.paveContextMenu) this.draw_g.on('contextmenu', evnt => this.paveContextMenu(evnt)); addDragHandler(this, { x: pave_x, y: pave_y, width: pave_width, height: pave_height, minwidth: 20, minheight: 20, redraw: d => this.sizeChanged(d) }); } return this; }); } /** @summary Process interactive moving of the stats box */ sizeChanged(drag) { this.pave_width = drag.width; this.pave_height = drag.height; const pave_x = drag.x, pave_y = drag.y, rect = this.getPadPainter().getPadRect(), fr = this.onFrame ? this.getFramePainter().getFrameRect() : rect, changes = {}; let offsetx, offsety; switch (this.corner) { case ECorner.kTopLeft: offsetx = pave_x - fr.x; offsety = pave_y - fr.y; break; case ECorner.kBottomLeft: offsetx = pave_x - fr.x; offsety = fr.y + fr.height - pave_y - this.pave_height; break; case ECorner.kBottomRight: offsetx = fr.x + fr.width - pave_x - this.pave_width; offsety = fr.y + fr.height - pave_y - this.pave_height; break; case ECorner.kTopRight: default: offsetx = fr.x + fr.width - pave_x - this.pave_width; offsety = pave_y - fr.y; } this.v7AttrChange(changes, 'offsetX', offsetx / rect.width); this.v7AttrChange(changes, 'offsetY', offsety / rect.height); this.v7AttrChange(changes, 'width', this.pave_width / rect.width); this.v7AttrChange(changes, 'height', this.pave_height / rect.height); this.v7SendAttrChanges(changes, false); // do not invoke canvas update on the server this.draw_g.selectChild('rect') .attr('width', this.pave_width) .attr('height', this.pave_height); this.drawContent(); } /** @summary Redraw RPave object */ async redraw(/* reason */) { return this.drawPave(); } /** @summary draw RPave object */ static async draw(dom, pave, opt) { const painter = new RPavePainter(dom, pave, opt, 'pave'); return ensureRCanvas(painter, false).then(() => painter.drawPave()); } } /** * @summary Painter for RLegend class * * @private */ class RLegendPainter extends RPavePainter { /** @summary draw RLegend content */ async drawContent() { const legend = this.getObject(), textFont = this.v7EvalFont('text', { size: 12, color: 'black', align: 22 }), width = this.pave_width, height = this.pave_height, pp = this.getPadPainter(); let nlines = legend.fEntries.length; if (legend.fTitle) nlines++; if (!nlines || !pp) return this; const stepy = height / nlines, margin_x = 0.02 * width; textFont.setSize(height/(nlines * 1.2)); return this.startTextDrawingAsync(textFont, 'font').then(() => { let posy = 0; if (legend.fTitle) { this.drawText({ latex: 1, width: width - 2*margin_x, height: stepy, x: margin_x, y: posy, text: legend.fTitle }); posy += stepy; } for (let i = 0; i < legend.fEntries.length; ++i) { const entry = legend.fEntries[i], w4 = Math.round(width/4); let objp = null; this.drawText({ latex: 1, width: 0.75*width - 3*margin_x, height: stepy, x: 2*margin_x + w4, y: posy, text: entry.fLabel }); if (entry.fDrawableId !== 'custom') objp = pp.findSnap(entry.fDrawableId, true); else if (entry.fDrawable.fIO) { objp = new RObjectPainter(this.getPadPainter(), entry.fDrawable.fIO); if (entry.fLine) objp.createv7AttLine(); if (entry.fFill) objp.createv7AttFill(); if (entry.fMarker) objp.createv7AttMarker(); } if (entry.fFill && objp?.fillatt) { this.draw_g .append('svg:path') .attr('d', `M${Math.round(margin_x)},${Math.round(posy + stepy*0.1)}h${w4}v${Math.round(stepy*0.8)}h${-w4}z`) .call(objp.fillatt.func); } if (entry.fLine && objp?.lineatt) { this.draw_g .append('svg:path') .attr('d', `M${Math.round(margin_x)},${Math.round(posy + stepy/2)}h${w4}`) .call(objp.lineatt.func); } if (entry.fError && objp?.lineatt) { this.draw_g .append('svg:path') .attr('d', `M${Math.round(margin_x + width/8)},${Math.round(posy + stepy*0.2)}v${Math.round(stepy*0.6)}`) .call(objp.lineatt.func); } if (entry.fMarker && objp?.markeratt) { this.draw_g.append('svg:path') .attr('d', objp.markeratt.create(margin_x + width/8, posy + stepy/2)) .call(objp.markeratt.func); } posy += stepy; } return this.finishTextDrawing(); }); } /** @summary draw RLegend object */ static async draw(dom, legend, opt) { const painter = new RLegendPainter(dom, legend, opt, 'legend'); return ensureRCanvas(painter, false).then(() => painter.drawPave()); } } // class RLegendPainter /** * @summary Painter for RPaveText class * * @private */ class RPaveTextPainter extends RPavePainter { /** @summary draw RPaveText content */ async drawContent() { const pavetext = this.getObject(), textFont = this.v7EvalFont('text', { size: 12, color: 'black', align: 22 }), width = this.pave_width, height = this.pave_height, nlines = pavetext.fText.length; if (!nlines) return; const stepy = height / nlines, margin_x = 0.02 * width; textFont.setSize(height/(nlines * 1.2)); return this.startTextDrawingAsync(textFont, 'font').then(() => { for (let i = 0, posy = 0; i < pavetext.fText.length; ++i, posy += stepy) this.drawText({ latex: 1, width: width - 2*margin_x, height: stepy, x: margin_x, y: posy, text: pavetext.fText[i] }); return this.finishTextDrawing(undefined, true); }); } /** @summary draw RPaveText object */ static async draw(dom, pave, opt) { const painter = new RPaveTextPainter(dom, pave, opt, 'pavetext'); return ensureRCanvas(painter, false).then(() => painter.drawPave()); } } // class RPaveTextPainter /** * @summary Painter for RHistStats class * * @private */ class RHistStatsPainter extends RPavePainter { /** @summary clear entries from stat box */ clearStat() { this.stats_lines = []; } /** @summary add text entry to stat box */ addText(line) { this.stats_lines.push(line); } /** @summary update statistic from the server */ updateStatistic(reply) { this.stats_lines = reply.lines; this.drawStatistic(this.stats_lines); } /** @summary fill statistic */ fillStatistic() { const pp = this.getPadPainter(); if (pp?._fast_drawing) return false; const obj = this.getObject(); if (obj.fLines !== undefined) { this.stats_lines = obj.fLines; delete obj.fLines; return true; } if (this.v7OfflineMode()) { const main = this.getMainPainter(); if (!isFunc(main?.fillStatistic)) return false; // we take statistic from main painter return main.fillStatistic(this, gStyle.fOptStat, gStyle.fOptFit); } // show lines which are exists, maybe server request will be received later return (this.stats_lines !== undefined); } /** @summary Draw content */ async drawContent() { if (this.fillStatistic()) return this.drawStatistic(this.stats_lines); return this; } /** @summary Change mask */ changeMask(nbit) { const obj = this.getObject(), mask = 1 << nbit; if (obj.fShowMask & mask) obj.fShowMask &= ~mask; else obj.fShowMask |= mask; if (this.fillStatistic()) this.drawStatistic(this.stats_lines); } /** @summary Context menu */ statsContextMenu(evnt) { evnt.preventDefault(); evnt.stopPropagation(); // disable main context menu createMenu(evnt, this).then(menu => { const obj = this.getObject(), action = this.changeMask.bind(this); menu.header('Stat Box'); for (let n = 0; n < obj.fEntries.length; ++n) menu.addchk((obj.fShowMask & (1<<n)), obj.fEntries[n], n, action); return this.fillObjectExecMenu(menu); }).then(menu => menu.show()); } /** @summary Draw statistic */ async drawStatistic(lines) { if (!lines) return this; const textFont = this.v7EvalFont('stats_text', { size: 12, color: 'black', align: 22 }), width = this.pave_width, height = this.pave_height, nlines = lines.length; let first_stat = 0, num_cols = 0, maxlen = 0; // adjust font size for (let j = 0; j < nlines; ++j) { const line = lines[j]; if (j > 0) maxlen = Math.max(maxlen, line.length); if ((j === 0) || (line.indexOf('|') < 0)) continue; if (first_stat === 0) first_stat = j; const parts = line.split('|'); if (parts.length > num_cols) num_cols = parts.length; } // for characters like 'p' or 'y' several more pixels required to stay in the box when drawn in last line const stepy = height / nlines, margin_x = 0.02 * width; let has_head = false, text_g = this.draw_g.selectChild('.statlines'); if (text_g.empty()) text_g = this.draw_g.append('svg:g').attr('class', 'statlines'); else text_g.selectAll('*').remove(); textFont.setSize(height/(nlines * 1.2)); return this.startTextDrawingAsync(textFont, 'font', text_g).then(() => { if (nlines === 1) this.drawText({ width, height, text: lines[0], latex: 1, draw_g: text_g }); else { for (let j = 0; j < nlines; ++j) { const posy = j*stepy; if (first_stat && (j >= first_stat)) { const parts = lines[j].split('|'); for (let n = 0; n < parts.length; ++n) { this.drawText({ align: 'middle', x: width * n / num_cols, y: posy, latex: 0, width: width/num_cols, height: stepy, text: parts[n], draw_g: text_g }); } } else if (lines[j].indexOf('=') < 0) { if (j === 0) { has_head = true; const max_hlen = Math.max(maxlen, Math.round((width-2*margin_x)/stepy/0.65)); if (lines[j].length > max_hlen + 5) lines[j] = lines[j].slice(0, max_hlen+2) + '...'; } this.drawText({ align: (j === 0) ? 'middle' : 'start', x: margin_x, y: posy, width: width - 2*margin_x, height: stepy, text: lines[j], draw_g: text_g }); } else { const parts = lines[j].split('='), args = []; for (let n = 0; n < 2; ++n) { const arg = { align: (n === 0) ? 'start' : 'end', x: margin_x, y: posy, width: width-2*margin_x, height: stepy, text: parts[n], draw_g: text_g, _expected_width: width-2*margin_x, _args: args, post_process(painter) { if (this._args[0].ready && this._args[1].ready) painter.scaleTextDrawing(1.05*(this._args[0].result_width && this._args[1].result_width)/this.__expected_width, this.draw_g); } }; args.push(arg); } for (let n = 0; n < 2; ++n) this.drawText(args[n]); } } } let lpath = ''; if (has_head) lpath += 'M0,' + Math.round(stepy) + 'h' + width; if ((first_stat > 0) && (num_cols > 1)) { for (let nrow = first_stat; nrow < nlines; ++nrow) lpath += 'M0,' + Math.round(nrow * stepy) + 'h' + width; for (let ncol = 0; ncol < num_cols - 1; ++ncol) lpath += 'M' + Math.round(width / num_cols * (ncol + 1)) + ',' + Math.round(first_stat * stepy) + 'V' + height; } if (lpath) this.draw_g.append('svg:path').attr('d', lpath); return this.finishTextDrawing(text_g); }); } /** @summary Redraw stats box */ async redraw(reason) { if (reason && isStr(reason) && (reason.indexOf('zoom') === 0) && this.v7NormalMode()) { const req = { _typename: `${nsREX}RHistStatBoxBase::RRequest`, mask: this.getObject().fShowMask // lines to show in stat box }; this.v7SubmitRequest('stat', req, reply => this.updateStatistic(reply)); } return this.drawPave(); } /** @summary draw RHistStats object */ static async draw(dom, stats, opt) { const painter = new RHistStatsPainter(dom, stats, opt, stats); return ensureRCanvas(painter, false).then(() => painter.drawPave()); } } // class RHistStatsPainter export { RPavePainter, RLegendPainter, RPaveTextPainter, RHistStatsPainter };