UNPKG

jsroot

Version:
783 lines (639 loc) 26.5 kB
import { settings, gStyle, create, BIT, clTPaveText, kTitle } from '../core.mjs'; import { scaleLinear, pointer as d3_pointer } from '../d3.mjs'; import { DrawOptions, buildSvgCurve, makeTranslate } from '../base/BasePainter.mjs'; import { ObjectPainter, getElementMainPainter } from '../base/ObjectPainter.mjs'; import { TPavePainter, kPosTitle } from '../hist/TPavePainter.mjs'; import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs'; import { TooltipHandler } from '../gpad/TFramePainter.mjs'; import { assignContextMenu, kNoReorder } from '../gui/menu.mjs'; const kNoTitle = BIT(17); /** * @summary Painter for TGraphPolargram objects. * * @private */ class TGraphPolargramPainter extends ObjectPainter { /** @summary Create painter * @param {object|string} dom - DOM element for drawing or element id * @param {object} polargram - object to draw */ constructor(dom, polargram, opt) { super(dom, polargram, opt); this.$polargram = true; // indicate that this is polargram this.zoom_rmin = this.zoom_rmax = 0; this.t0 = 0; this.mult = 1; this.decodeOptions(opt); } /** @summary Returns true if fixed coordinates are configured */ isNormalAngles() { const polar = this.getObject(); return polar?.fRadian || polar?.fGrad || polar?.fDegree; } /** @summary Decode draw options */ decodeOptions(opt) { const d = new DrawOptions(opt); if (!this.options) this.options = {}; Object.assign(this.options, { rdot: d.check('RDOT'), rangle: d.check('RANGLE', true) ? d.partAsInt() : 0, NoLabels: d.check('N'), OrthoLabels: d.check('O') }); this.storeDrawOpt(opt); } /** @summary Set angles range displayed by the polargram */ setAnglesRange(tmin, tmax, set_obj) { if (tmin >= tmax) tmax = tmin + 1; if (set_obj) { const polar = this.getObject(); polar.fRwtmin = tmin; polar.fRwtmax = tmax; } this.t0 = tmin; this.mult = 2*Math.PI/(tmax - tmin); } /** @summary Translate coordinates */ translate(input_angle, radius, keep_float) { // recalculate angle const angle = (input_angle - this.t0) * this.mult; let rx = this.r(radius), ry = rx/this.szx*this.szy, grx = rx * Math.cos(-angle), gry = ry * Math.sin(-angle); if (!keep_float) { grx = Math.round(grx); gry = Math.round(gry); rx = Math.round(rx); ry = Math.round(ry); } return { grx, gry, rx, ry }; } /** @summary format label for radius ticks */ format(radius) { if (radius === Math.round(radius)) return radius.toString(); if (this.ndig > 10) return radius.toExponential(4); return radius.toFixed((this.ndig > 0) ? this.ndig : 0); } /** @summary Convert axis values to text */ axisAsText(axis, value) { if (axis === 'r') { if (value === Math.round(value)) return value.toString(); if (this.ndig > 10) return value.toExponential(4); return value.toFixed(this.ndig+2); } value *= 180/Math.PI; return (value === Math.round(value)) ? value.toString() : value.toFixed(1); } /** @summary Returns coordinate of frame - without using frame itself */ getFrameRect() { const pp = this.getPadPainter(), pad = pp.getRootPad(true), w = pp.getPadWidth(), h = pp.getPadHeight(), rect = {}; if (pad) { rect.szx = Math.round(Math.max(0.1, 0.5 - Math.max(pad.fLeftMargin, pad.fRightMargin))*w); rect.szy = Math.round(Math.max(0.1, 0.5 - Math.max(pad.fBottomMargin, pad.fTopMargin))*h); } else { rect.szx = Math.round(0.5*w); rect.szy = Math.round(0.5*h); } rect.width = 2 * rect.szx; rect.height = 2 * rect.szy; rect.x = Math.round(w / 2 - rect.szx); rect.y = Math.round(h / 2 - rect.szy); rect.hint_delta_x = rect.szx; rect.hint_delta_y = rect.szy; rect.transform = makeTranslate(rect.x, rect.y) || ''; return rect; } /** @summary Process mouse event */ mouseEvent(kind, evnt) { // const layer = this.getLayerSvg('primitives_layer'), // interactive = layer.select('.interactive_ellipse'); // if (interactive.empty()) return; let pnt = null; if (kind !== 'leave') { const pos = d3_pointer(evnt, this.draw_g.node()); pnt = { x: pos[0], y: pos[1], touch: false }; } this.processFrameTooltipEvent(pnt); } /** @summary Process mouse wheel event */ mouseWheel(evnt) { evnt.stopPropagation(); evnt.preventDefault(); this.processFrameTooltipEvent(null); // remove all tooltips const polar = this.getObject(); if (!polar) return; let delta = evnt.wheelDelta ? -evnt.wheelDelta : (evnt.deltaY || evnt.detail); if (!delta) return; delta = (delta < 0) ? -0.2 : 0.2; let rmin = this.scale_rmin, rmax = this.scale_rmax; const range = rmax - rmin; // rmin -= delta*range; rmax += delta*range; if ((rmin < polar.fRwrmin) || (rmax > polar.fRwrmax)) rmin = rmax = 0; if ((this.zoom_rmin !== rmin) || (this.zoom_rmax !== rmax)) { this.zoom_rmin = rmin; this.zoom_rmax = rmax; this.redrawPad(); } } /** @summary Process mouse double click event */ mouseDoubleClick() { if (this.zoom_rmin || this.zoom_rmax) { this.zoom_rmin = this.zoom_rmax = 0; this.redrawPad(); } } /** @summary Draw polargram polar labels */ async drawPolarLabels(polar, nmajor) { const fontsize = Math.round(polar.fPolarTextSize * this.szy * 2); return this.startTextDrawingAsync(polar.fPolarLabelFont, fontsize) .then(() => { const lbls = (nmajor === 8) ? ['0', '#frac{#pi}{4}', '#frac{#pi}{2}', '#frac{3#pi}{4}', '#pi', '#frac{5#pi}{4}', '#frac{3#pi}{2}', '#frac{7#pi}{4}'] : ['0', '#frac{2#pi}{3}', '#frac{4#pi}{3}'], aligns = [12, 11, 21, 31, 32, 33, 23, 13]; for (let n = 0; n < nmajor; ++n) { const angle = -n*2*Math.PI/nmajor; this.draw_g.append('svg:path') .attr('d', `M0,0L${Math.round(this.szx*Math.cos(angle))},${Math.round(this.szy*Math.sin(angle))}`) .call(this.lineatt.func); let align = 12, rotate = 0; if (this.options.OrthoLabels) { rotate = -n/nmajor*360; if ((rotate > -271) && (rotate < -91)) { align = 32; rotate += 180; } } else { const aindx = Math.round(16 - angle/Math.PI*4) % 8; // index in align table, here absolute angle is important align = aligns[aindx]; } this.drawText({ align, rotate, x: Math.round((this.szx + fontsize)*Math.cos(angle)), y: Math.round((this.szy + fontsize/this.szx*this.szy)*(Math.sin(angle))), text: lbls[n], color: this.getColor(polar.fPolarLabelColor), latex: 1 }); } return this.finishTextDrawing(); }); } /** @summary Redraw polargram */ async redraw() { if (!this.isMainPainter()) return; const polar = this.getObject(), rect = this.getPadPainter().getFrameRect(); this.createG(); makeTranslate(this.draw_g, Math.round(rect.x + rect.width/2), Math.round(rect.y + rect.height/2)); this.szx = rect.szx; this.szy = rect.szy; this.scale_rmin = polar.fRwrmin; this.scale_rmax = polar.fRwrmax; if (this.zoom_rmin !== this.zoom_rmax) { this.scale_rmin = this.zoom_rmin; this.scale_rmax = this.zoom_rmax; } this.r = scaleLinear().domain([this.scale_rmin, this.scale_rmax]).range([0, this.szx]); if (polar.fRadian) { polar.fRwtmin = 0; polar.fRwtmax = 2*Math.PI; } else if (polar.fDegree) { polar.fRwtmin = 0; polar.fRwtmax = 360; } else if (polar.fGrad) { polar.fRwtmin = 0; polar.fRwtmax = 200; } this.setAnglesRange(polar.fRwtmin, polar.fRwtmax); const ticks = this.r.ticks(5); let nminor = Math.floor((polar.fNdivRad % 10000) / 100), nmajor = polar.fNdivPol % 100; if (nmajor !== 3) nmajor = 8; this.createAttLine({ attr: polar }); if (!this.gridatt) this.gridatt = this.createAttLine({ color: polar.fLineColor, style: 2, width: 1, std: false }); const range = Math.abs(polar.fRwrmax - polar.fRwrmin); this.ndig = (range <= 0) ? -3 : Math.round(Math.log10(ticks.length / range)); // verify that all radius labels are unique let lbls = [], indx = 0; while (indx<ticks.length) { const lbl = this.format(ticks[indx]); if (lbls.indexOf(lbl) >= 0) { if (++this.ndig>10) break; lbls = []; indx = 0; continue; } lbls.push(lbl); indx++; } let exclude_last = false; const pointer_events = this.isBatchMode() ? null : 'visibleFill'; if ((ticks[ticks.length - 1] < polar.fRwrmax) && (this.zoom_rmin === this.zoom_rmax)) { ticks.push(polar.fRwrmax); exclude_last = true; } return this.startTextDrawingAsync(polar.fRadialLabelFont, Math.round(polar.fRadialTextSize * this.szy * 2)).then(() => { const axis_angle = - (this.options.rangle || polar.fAxisAngle) / 180 * Math.PI, ca = Math.cos(axis_angle), sa = Math.sin(axis_angle); for (let n = 0; n < ticks.length; ++n) { let rx = this.r(ticks[n]), ry = rx / this.szx * this.szy; this.draw_g.append('ellipse') .attr('cx', 0) .attr('cy', 0) .attr('rx', Math.round(rx)) .attr('ry', Math.round(ry)) .style('fill', 'none') .style('pointer-events', pointer_events) .call(this.lineatt.func); if ((n < ticks.length - 1) || !exclude_last) { const halign = ca > 0.7 ? 1 : (ca > 0 ? 3 : (ca > -0.7 ? 1 : 3)), valign = Math.abs(ca) < 0.7 ? 1 : 3; this.drawText({ align: 10 * halign + valign, x: Math.round(rx*ca), y: Math.round(ry*sa), text: this.format(ticks[n]), color: this.getColor(polar.fRadialLabelColor), latex: 0 }); if (this.options.rdot) { this.draw_g.append('ellipse') .attr('cx', Math.round(rx * ca)) .attr('cy', Math.round(ry * sa)) .attr('rx', 3) .attr('ry', 3) .style('fill', 'red'); } } if ((nminor > 1) && ((n < ticks.length - 1) || !exclude_last)) { const dr = (ticks[1] - ticks[0]) / nminor; for (let nn = 1; nn < nminor; ++nn) { const gridr = ticks[n] + dr*nn; if (gridr > this.scale_rmax) break; rx = this.r(gridr); ry = rx / this.szx * this.szy; this.draw_g.append('ellipse') .attr('cx', 0) .attr('cy', 0) .attr('rx', Math.round(rx)) .attr('ry', Math.round(ry)) .style('fill', 'none') .style('pointer-events', pointer_events) .call(this.gridatt.func); } } } if (ca < 0.999) { this.draw_g.append('path') .attr('d', `M0,0L${Math.round(this.szx*ca)},${Math.round(this.szy*sa)}`) .style('pointer-events', pointer_events) .call(this.lineatt.func); } return this.finishTextDrawing(); }).then(() => { return this.options.NoLabels ? true : this.drawPolarLabels(polar, nmajor); }).then(() => { nminor = Math.floor((polar.fNdivPol % 10000) / 100); if (nminor > 1) { for (let n = 0; n < nmajor * nminor; ++n) { if (n % nminor === 0) continue; const angle = -n*2*Math.PI/nmajor/nminor; this.draw_g.append('svg:path') .attr('d', `M0,0L${Math.round(this.szx*Math.cos(angle))},${Math.round(this.szy*Math.sin(angle))}`) .call(this.gridatt.func); } } if (this.isBatchMode()) return; TooltipHandler.assign(this); assignContextMenu(this, kNoReorder); this.assignZoomHandler(this.draw_g); }); } /** @summary Fill TGraphPolargram context menu */ fillContextMenuItems(menu) { const pp = this.getObject(); menu.sub('Axis range'); menu.addchk(pp.fRadian, 'Radian', flag => { pp.fRadian = flag; pp.fDegree = pp.fGrad = false; this.interactiveRedraw('pad', flag ? 'exec:SetToRadian()' : 'exec:SetTwoPi()'); }, 'Handle data angles as radian range 0..2*Pi'); menu.addchk(pp.fDegree, 'Degree', flag => { pp.fDegree = flag; pp.fRadian = pp.fGrad = false; this.interactiveRedraw('pad', flag ? 'exec:SetToDegree()' : 'exec:SetTwoPi()'); }, 'Handle data angles as degree range 0..360'); menu.addchk(pp.fGrad, 'Grad', flag => { pp.fGrad = flag; pp.fRadian = pp.fDegree = false; this.interactiveRedraw('pad', flag ? 'exec:SetToGrad()' : 'exec:SetTwoPi()'); }, 'Handle data angles as grad range 0..200'); menu.endsub(); menu.addSizeMenu('Axis angle', 0, 315, 45, this.options.rangle || pp.fAxisAngle, v => { this.options.rangle = pp.fAxisAngle = v; this.interactiveRedraw('pad', `exec:SetAxisAngle(${v})`); }); } /** @summary Assign zoom handler to element * @private */ assignZoomHandler(elem) { elem.on('mouseenter', evnt => this.mouseEvent('enter', evnt)) .on('mousemove', evnt => this.mouseEvent('move', evnt)) .on('mouseleave', evnt => this.mouseEvent('leave', evnt)); if (settings.Zooming) elem.on('dblclick', evnt => this.mouseDoubleClick(evnt)); if (settings.Zooming && settings.ZoomWheel) elem.on('wheel', evnt => this.mouseWheel(evnt)); } /** @summary Draw TGraphPolargram */ static async draw(dom, polargram, opt) { const main = getElementMainPainter(dom); if (main) { if (main.getObject() === polargram) return main; throw Error('Cannot superimpose TGraphPolargram with any other drawings'); } const painter = new TGraphPolargramPainter(dom, polargram, opt); return ensureTCanvas(painter, false).then(() => { painter.setAsMainPainter(); return painter.redraw(); }).then(() => painter); } } // class TGraphPolargramPainter /** * @summary Painter for TGraphPolar objects. * * @private */ class TGraphPolarPainter extends ObjectPainter { /** @summary Decode options for drawing TGraphPolar */ decodeOptions(opt) { const d = new DrawOptions(opt || 'L'); if (!this.options) this.options = {}; const rdot = d.check('RDOT'), rangle = d.check('RANGLE', true) ? d.partAsInt() : 0; Object.assign(this.options, { mark: d.check('P'), err: d.check('E'), fill: d.check('F'), line: d.check('L'), curve: d.check('C'), radian: d.check('R'), degree: d.check('D'), grad: d.check('G'), Axis: d.check('N') ? 'N' : '' }); if (d.check('O')) this.options.Axis += 'O'; if (rdot) this.options.Axis += '_rdot'; if (rangle) this.options.Axis += `_rangle${rangle}`; this.storeDrawOpt(opt); } /** @summary Update TGraphPolar with polargram */ updateObject(obj, opt) { if (!this.matchObjectType(obj)) return false; if (opt && (opt !== this.options.original)) this.decodeOptions(opt); if (this._draw_axis && obj.fPolargram) this.getMainPainter().updateObject(obj.fPolargram); delete obj.fPolargram; // copy all properties but not polargram Object.assign(this.getObject(), obj); return true; } /** @summary Redraw TGraphPolar */ redraw() { return this.drawGraphPolar().then(() => this.updateTitle()); } /** @summary Drawing TGraphPolar */ async drawGraphPolar() { const graph = this.getObject(), main = this.getMainPainter(); if (!graph || !main?.$polargram) return; if (this.options.mark) this.createAttMarker({ attr: graph }); if (this.options.err || this.options.line || this.options.curve) this.createAttLine({ attr: graph }); if (this.options.fill) this.createAttFill({ attr: graph }); this.createG(); if (this._draw_axis && !main.isNormalAngles()) { const has_err = graph.fEX?.length; let rwtmin = graph.fX[0], rwtmax = graph.fX[0]; for (let n = 0; n < graph.fNpoints; ++n) { rwtmin = Math.min(rwtmin, graph.fX[n] - (has_err ? graph.fEX[n] : 0)); rwtmax = Math.max(rwtmax, graph.fX[n] + (has_err ? graph.fEX[n] : 0)); } rwtmax += (rwtmax - rwtmin) / graph.fNpoints; main.setAnglesRange(rwtmin, rwtmax, true); } this.draw_g.attr('transform', main.draw_g.attr('transform')); let mpath = '', epath = ''; const bins = [], pointer_events = this.isBatchMode() ? null : 'visibleFill'; for (let n = 0; n < graph.fNpoints; ++n) { if (graph.fY[n] > main.scale_rmax) continue; if (this.options.err) { const p1 = main.translate(graph.fX[n], graph.fY[n] - graph.fEY[n]), p2 = main.translate(graph.fX[n], graph.fY[n] + graph.fEY[n]), p3 = main.translate(graph.fX[n] + graph.fEX[n], graph.fY[n]), p4 = main.translate(graph.fX[n] - graph.fEX[n], graph.fY[n]); epath += `M${p1.grx},${p1.gry}L${p2.grx},${p2.gry}` + `M${p3.grx},${p3.gry}A${p4.rx},${p4.ry},0,0,1,${p4.grx},${p4.gry}`; } const pos = main.translate(graph.fX[n], graph.fY[n]); if (this.options.mark) mpath += this.markeratt.create(pos.grx, pos.gry); if (this.options.curve || this.options.line || this.options.fill) bins.push(pos); } if ((this.options.fill || this.options.line) && bins.length) { const lpath = buildSvgCurve(bins, { line: true }); if (this.options.fill) { this.draw_g.append('svg:path') .attr('d', lpath + 'Z') .style('pointer-events', pointer_events) .call(this.fillatt.func); } if (this.options.line) { this.draw_g.append('svg:path') .attr('d', lpath) .style('fill', 'none') .style('pointer-events', pointer_events) .call(this.lineatt.func); } } if (this.options.curve && bins.length) { this.draw_g.append('svg:path') .attr('d', buildSvgCurve(bins)) .style('fill', 'none') .style('pointer-events', pointer_events) .call(this.lineatt.func); } if (epath) { this.draw_g.append('svg:path') .attr('d', epath) .style('fill', 'none') .style('pointer-events', pointer_events) .call(this.lineatt.func); } if (mpath) { this.draw_g.append('svg:path') .attr('d', mpath) .style('pointer-events', pointer_events) .call(this.markeratt.func); } if (!this.isBatchMode()) { assignContextMenu(this, kNoReorder); main.assignZoomHandler(this.draw_g); } } /** @summary Create polargram object */ createPolargram(gr) { if (!gr.fPolargram) { gr.fPolargram = create('TGraphPolargram'); if (this.options.radian) gr.fPolargram.fRadian = true; else if (this.options.degree) gr.fPolargram.fDegree = true; else if (this.options.grad) gr.fPolargram.fGrad = true; } let rmin = gr.fY[0] || 0, rmax = rmin; const has_err = gr.fEY?.length; for (let n = 0; n < gr.fNpoints; ++n) { rmin = Math.min(rmin, gr.fY[n] - (has_err ? gr.fEY[n] : 0)); rmax = Math.max(rmax, gr.fY[n] + (has_err ? gr.fEY[n] : 0)); } gr.fPolargram.fRwrmin = rmin - (rmax-rmin)*0.1; gr.fPolargram.fRwrmax = rmax + (rmax-rmin)*0.1; return gr.fPolargram; } /** @summary Provide tooltip at specified point */ extractTooltip(pnt) { if (!pnt) return null; const graph = this.getObject(), main = this.getMainPainter(); let best_dist2 = 1e10, bestindx = -1, bestpos = null; for (let n = 0; n < graph.fNpoints; ++n) { const pos = main.translate(graph.fX[n], graph.fY[n]), dist2 = (pos.grx - pnt.x)**2 + (pos.gry - pnt.y)**2; if (dist2 < best_dist2) { best_dist2 = dist2; bestindx = n; bestpos = pos; } } let match_distance = 5; if (this.markeratt?.used) match_distance = this.markeratt.getFullSize(); if (Math.sqrt(best_dist2) > match_distance) return null; const res = { name: this.getObject().fName, title: this.getObject().fTitle, x: bestpos.grx, y: bestpos.gry, color1: (this.markeratt?.used ? this.markeratt.color : undefined) ?? (this.fillatt?.used ? this.fillatt.color : undefined) ?? this.lineatt?.color, exact: Math.sqrt(best_dist2) < 4, lines: [this.getObjectHint()], binindx: bestindx, menu_dist: match_distance, radius: match_distance }; res.lines.push(`r = ${main.axisAsText('r', graph.fY[bestindx])}`, `phi = ${main.axisAsText('phi', graph.fX[bestindx])}`); if (graph.fEY && graph.fEY[bestindx]) res.lines.push(`error r = ${main.axisAsText('r', graph.fEY[bestindx])}`); if (graph.fEX && graph.fEX[bestindx]) res.lines.push(`error phi = ${main.axisAsText('phi', graph.fEX[bestindx])}`); return res; } /** @summary Only redraw histogram title * @return {Promise} with painter */ async updateTitle() { // case when histogram drawn over other histogram (same option) if (!this._draw_axis) return this; const tpainter = this.getPadPainter()?.findPainterFor(null, kTitle, clTPaveText), pt = tpainter?.getObject(); if (!tpainter || !pt) return this; const gr = this.getObject(), draw_title = !gr.TestBit(kNoTitle) && (gStyle.fOptTitle > 0); pt.Clear(); if (draw_title) pt.AddText(gr.fTitle); return tpainter.redraw().then(() => this); } /** @summary Draw histogram title * @return {Promise} with painter */ async drawTitle() { // case when histogram drawn over other histogram (same option) if (!this._draw_axis) return this; const gr = this.getObject(), st = gStyle, draw_title = !gr.TestBit(kNoTitle) && (st.fOptTitle > 0), pp = this.getPadPainter(); let pt = pp.findInPrimitives(kTitle, clTPaveText); if (pt) { pt.Clear(); if (draw_title) pt.AddText(gr.fTitle); return this; } pt = create(clTPaveText); Object.assign(pt, { fName: kTitle, fFillColor: st.fTitleColor, fFillStyle: st.fTitleStyle, fBorderSize: st.fTitleBorderSize, fTextFont: st.fTitleFont, fTextSize: st.fTitleFontSize, fTextColor: st.fTitleTextColor, fTextAlign: 22 }); if (draw_title) pt.AddText(gr.fTitle); return TPavePainter.draw(pp, pt, kPosTitle) .then(p => { p?.setSecondaryId(this, kTitle); return this; }); } /** @summary Show tooltip */ showTooltip(hint) { let ttcircle = this.draw_g?.selectChild('.tooltip_bin'); if (!hint || !this.draw_g) { ttcircle?.remove(); return; } if (ttcircle.empty()) { ttcircle = this.draw_g.append('svg:ellipse') .attr('class', 'tooltip_bin') .style('pointer-events', 'none'); } hint.changed = ttcircle.property('current_bin') !== hint.binindx; if (hint.changed) { ttcircle.attr('cx', hint.x) .attr('cy', hint.y) .attr('rx', Math.round(hint.radius)) .attr('ry', Math.round(hint.radius)) .style('fill', 'none') .style('stroke', hint.color1) .property('current_bin', hint.binindx); } } /** @summary Process tooltip event */ processTooltipEvent(pnt) { const hint = this.extractTooltip(pnt); if (!pnt || !pnt.disabled) this.showTooltip(hint); return hint; } /** @summary Draw TGraphPolar */ static async draw(dom, graph, opt) { const painter = new TGraphPolarPainter(dom, graph, opt); painter.decodeOptions(opt); const main = painter.getMainPainter(); if (main && !main.$polargram) { console.error('Cannot superimpose TGraphPolar with plain histograms'); return null; } let pr = Promise.resolve(null); if (!main) { // indicate that axis defined by this graph painter._draw_axis = true; pr = TGraphPolargramPainter.draw(dom, painter.createPolargram(graph), painter.options.Axis); } return pr.then(gram_painter => { gram_painter?.setSecondaryId(painter, 'polargram'); painter.addToPadPrimitives(); return painter.drawGraphPolar(); }).then(() => painter.drawTitle()); } } // class TGraphPolarPainter export { TGraphPolargramPainter, TGraphPolarPainter };