draw/TGraphPolarPainter.mjs

  1. import { settings, gStyle, create, BIT, clTPaveText, kTitle } from '../core.mjs';
  2. import { scaleLinear, pointer as d3_pointer } from '../d3.mjs';
  3. import { DrawOptions, buildSvgCurve, makeTranslate } from '../base/BasePainter.mjs';
  4. import { ObjectPainter, getElementMainPainter } from '../base/ObjectPainter.mjs';
  5. import { TPavePainter, kPosTitle } from '../hist/TPavePainter.mjs';
  6. import { ensureTCanvas } from '../gpad/TCanvasPainter.mjs';
  7. import { TooltipHandler } from '../gpad/TFramePainter.mjs';
  8. import { assignContextMenu, kNoReorder } from '../gui/menu.mjs';
  9. const kNoTitle = BIT(17);
  10. /**
  11. * @summary Painter for TGraphPolargram objects.
  12. *
  13. * @private */
  14. class TGraphPolargramPainter extends ObjectPainter {
  15. /** @summary Create painter
  16. * @param {object|string} dom - DOM element for drawing or element id
  17. * @param {object} polargram - object to draw */
  18. constructor(dom, polargram, opt) {
  19. super(dom, polargram, opt);
  20. this.$polargram = true; // indicate that this is polargram
  21. this.zoom_rmin = this.zoom_rmax = 0;
  22. this.t0 = 0;
  23. this.mult = 1;
  24. this.decodeOptions(opt);
  25. }
  26. /** @summary Returns true if fixed coordinates are configured */
  27. isNormalAngles() {
  28. const polar = this.getObject();
  29. return polar?.fRadian || polar?.fGrad || polar?.fDegree;
  30. }
  31. /** @summary Decode draw options */
  32. decodeOptions(opt) {
  33. const d = new DrawOptions(opt);
  34. if (!this.options)
  35. this.options = {};
  36. Object.assign(this.options, {
  37. rdot: d.check('RDOT'),
  38. rangle: d.check('RANGLE', true) ? d.partAsInt() : 0,
  39. NoLabels: d.check('N'),
  40. OrthoLabels: d.check('O')
  41. });
  42. this.storeDrawOpt(opt);
  43. }
  44. /** @summary Set angles range displayed by the polargram */
  45. setAnglesRange(tmin, tmax, set_obj) {
  46. if (tmin >= tmax)
  47. tmax = tmin + 1;
  48. if (set_obj) {
  49. const polar = this.getObject();
  50. polar.fRwtmin = tmin;
  51. polar.fRwtmax = tmax;
  52. }
  53. this.t0 = tmin;
  54. this.mult = 2*Math.PI/(tmax - tmin);
  55. }
  56. /** @summary Translate coordinates */
  57. translate(input_angle, radius, keep_float) {
  58. // recalculate angle
  59. const angle = (input_angle - this.t0) * this.mult;
  60. let rx = this.r(radius),
  61. ry = rx/this.szx*this.szy,
  62. grx = rx * Math.cos(-angle),
  63. gry = ry * Math.sin(-angle);
  64. if (!keep_float) {
  65. grx = Math.round(grx);
  66. gry = Math.round(gry);
  67. rx = Math.round(rx);
  68. ry = Math.round(ry);
  69. }
  70. return { grx, gry, rx, ry };
  71. }
  72. /** @summary format label for radius ticks */
  73. format(radius) {
  74. if (radius === Math.round(radius)) return radius.toString();
  75. if (this.ndig > 10) return radius.toExponential(4);
  76. return radius.toFixed((this.ndig > 0) ? this.ndig : 0);
  77. }
  78. /** @summary Convert axis values to text */
  79. axisAsText(axis, value) {
  80. if (axis === 'r') {
  81. if (value === Math.round(value))
  82. return value.toString();
  83. if (this.ndig > 10)
  84. return value.toExponential(4);
  85. return value.toFixed(this.ndig+2);
  86. }
  87. value *= 180/Math.PI;
  88. return (value === Math.round(value)) ? value.toString() : value.toFixed(1);
  89. }
  90. /** @summary Returns coordinate of frame - without using frame itself */
  91. getFrameRect() {
  92. const pp = this.getPadPainter(),
  93. pad = pp.getRootPad(true),
  94. w = pp.getPadWidth(),
  95. h = pp.getPadHeight(),
  96. rect = {};
  97. if (pad) {
  98. rect.szx = Math.round(Math.max(0.1, 0.5 - Math.max(pad.fLeftMargin, pad.fRightMargin))*w);
  99. rect.szy = Math.round(Math.max(0.1, 0.5 - Math.max(pad.fBottomMargin, pad.fTopMargin))*h);
  100. } else {
  101. rect.szx = Math.round(0.5*w);
  102. rect.szy = Math.round(0.5*h);
  103. }
  104. rect.width = 2 * rect.szx;
  105. rect.height = 2 * rect.szy;
  106. rect.x = Math.round(w / 2 - rect.szx);
  107. rect.y = Math.round(h / 2 - rect.szy);
  108. rect.hint_delta_x = rect.szx;
  109. rect.hint_delta_y = rect.szy;
  110. rect.transform = makeTranslate(rect.x, rect.y) || '';
  111. return rect;
  112. }
  113. /** @summary Process mouse event */
  114. mouseEvent(kind, evnt) {
  115. // const layer = this.getLayerSvg('primitives_layer'),
  116. // interactive = layer.select('.interactive_ellipse');
  117. // if (interactive.empty()) return;
  118. let pnt = null;
  119. if (kind !== 'leave') {
  120. const pos = d3_pointer(evnt, this.draw_g.node());
  121. pnt = { x: pos[0], y: pos[1], touch: false };
  122. }
  123. this.processFrameTooltipEvent(pnt);
  124. }
  125. /** @summary Process mouse wheel event */
  126. mouseWheel(evnt) {
  127. evnt.stopPropagation();
  128. evnt.preventDefault();
  129. this.processFrameTooltipEvent(null); // remove all tooltips
  130. const polar = this.getObject();
  131. if (!polar) return;
  132. let delta = evnt.wheelDelta ? -evnt.wheelDelta : (evnt.deltaY || evnt.detail);
  133. if (!delta) return;
  134. delta = (delta < 0) ? -0.2 : 0.2;
  135. let rmin = this.scale_rmin, rmax = this.scale_rmax;
  136. const range = rmax - rmin;
  137. // rmin -= delta*range;
  138. rmax += delta*range;
  139. if ((rmin < polar.fRwrmin) || (rmax > polar.fRwrmax))
  140. rmin = rmax = 0;
  141. if ((this.zoom_rmin !== rmin) || (this.zoom_rmax !== rmax)) {
  142. this.zoom_rmin = rmin;
  143. this.zoom_rmax = rmax;
  144. this.redrawPad();
  145. }
  146. }
  147. /** @summary Process mouse double click event */
  148. mouseDoubleClick() {
  149. if (this.zoom_rmin || this.zoom_rmax) {
  150. this.zoom_rmin = this.zoom_rmax = 0;
  151. this.redrawPad();
  152. }
  153. }
  154. /** @summary Draw polargram polar labels */
  155. async drawPolarLabels(polar, nmajor) {
  156. const fontsize = Math.round(polar.fPolarTextSize * this.szy * 2);
  157. return this.startTextDrawingAsync(polar.fPolarLabelFont, fontsize)
  158. .then(() => {
  159. 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}'],
  160. aligns = [12, 11, 21, 31, 32, 33, 23, 13];
  161. for (let n = 0; n < nmajor; ++n) {
  162. const angle = -n*2*Math.PI/nmajor;
  163. this.draw_g.append('svg:path')
  164. .attr('d', `M0,0L${Math.round(this.szx*Math.cos(angle))},${Math.round(this.szy*Math.sin(angle))}`)
  165. .call(this.lineatt.func);
  166. let align = 12, rotate = 0;
  167. if (this.options.OrthoLabels) {
  168. rotate = -n/nmajor*360;
  169. if ((rotate > -271) && (rotate < -91)) {
  170. align = 32; rotate += 180;
  171. }
  172. } else {
  173. const aindx = Math.round(16 - angle/Math.PI*4) % 8; // index in align table, here absolute angle is important
  174. align = aligns[aindx];
  175. }
  176. this.drawText({ align, rotate,
  177. x: Math.round((this.szx + fontsize)*Math.cos(angle)),
  178. y: Math.round((this.szy + fontsize/this.szx*this.szy)*(Math.sin(angle))),
  179. text: lbls[n],
  180. color: this.getColor(polar.fPolarLabelColor), latex: 1 });
  181. }
  182. return this.finishTextDrawing();
  183. });
  184. }
  185. /** @summary Redraw polargram */
  186. async redraw() {
  187. if (!this.isMainPainter())
  188. return;
  189. const polar = this.getObject(),
  190. rect = this.getPadPainter().getFrameRect();
  191. this.createG();
  192. makeTranslate(this.draw_g, Math.round(rect.x + rect.width/2), Math.round(rect.y + rect.height/2));
  193. this.szx = rect.szx;
  194. this.szy = rect.szy;
  195. this.scale_rmin = polar.fRwrmin;
  196. this.scale_rmax = polar.fRwrmax;
  197. if (this.zoom_rmin !== this.zoom_rmax) {
  198. this.scale_rmin = this.zoom_rmin;
  199. this.scale_rmax = this.zoom_rmax;
  200. }
  201. this.r = scaleLinear().domain([this.scale_rmin, this.scale_rmax]).range([0, this.szx]);
  202. if (polar.fRadian) {
  203. polar.fRwtmin = 0; polar.fRwtmax = 2*Math.PI;
  204. } else if (polar.fDegree) {
  205. polar.fRwtmin = 0; polar.fRwtmax = 360;
  206. } else if (polar.fGrad) {
  207. polar.fRwtmin = 0; polar.fRwtmax = 200;
  208. }
  209. this.setAnglesRange(polar.fRwtmin, polar.fRwtmax);
  210. const ticks = this.r.ticks(5);
  211. let nminor = Math.floor((polar.fNdivRad % 10000) / 100),
  212. nmajor = polar.fNdivPol % 100;
  213. if (nmajor !== 3)
  214. nmajor = 8;
  215. this.createAttLine({ attr: polar });
  216. if (!this.gridatt)
  217. this.gridatt = this.createAttLine({ color: polar.fLineColor, style: 2, width: 1, std: false });
  218. const range = Math.abs(polar.fRwrmax - polar.fRwrmin);
  219. this.ndig = (range <= 0) ? -3 : Math.round(Math.log10(ticks.length / range));
  220. // verify that all radius labels are unique
  221. let lbls = [], indx = 0;
  222. while (indx<ticks.length) {
  223. const lbl = this.format(ticks[indx]);
  224. if (lbls.indexOf(lbl) >= 0) {
  225. if (++this.ndig>10) break;
  226. lbls = []; indx = 0; continue;
  227. }
  228. lbls.push(lbl);
  229. indx++;
  230. }
  231. let exclude_last = false;
  232. const pointer_events = this.isBatchMode() ? null : 'visibleFill';
  233. if ((ticks[ticks.length - 1] < polar.fRwrmax) && (this.zoom_rmin === this.zoom_rmax)) {
  234. ticks.push(polar.fRwrmax);
  235. exclude_last = true;
  236. }
  237. return this.startTextDrawingAsync(polar.fRadialLabelFont, Math.round(polar.fRadialTextSize * this.szy * 2)).then(() => {
  238. const axis_angle = - (this.options.rangle || polar.fAxisAngle) / 180 * Math.PI,
  239. ca = Math.cos(axis_angle),
  240. sa = Math.sin(axis_angle);
  241. for (let n = 0; n < ticks.length; ++n) {
  242. let rx = this.r(ticks[n]),
  243. ry = rx / this.szx * this.szy;
  244. this.draw_g.append('ellipse')
  245. .attr('cx', 0)
  246. .attr('cy', 0)
  247. .attr('rx', Math.round(rx))
  248. .attr('ry', Math.round(ry))
  249. .style('fill', 'none')
  250. .style('pointer-events', pointer_events)
  251. .call(this.lineatt.func);
  252. if ((n < ticks.length - 1) || !exclude_last) {
  253. const halign = ca > 0.7 ? 1 : (ca > 0 ? 3 : (ca > -0.7 ? 1 : 3)),
  254. valign = Math.abs(ca) < 0.7 ? 1 : 3;
  255. this.drawText({ align: 10 * halign + valign,
  256. x: Math.round(rx*ca),
  257. y: Math.round(ry*sa),
  258. text: this.format(ticks[n]),
  259. color: this.getColor(polar.fRadialLabelColor), latex: 0 });
  260. if (this.options.rdot) {
  261. this.draw_g.append('ellipse')
  262. .attr('cx', Math.round(rx * ca))
  263. .attr('cy', Math.round(ry * sa))
  264. .attr('rx', 3)
  265. .attr('ry', 3)
  266. .style('fill', 'red');
  267. }
  268. }
  269. if ((nminor > 1) && ((n < ticks.length - 1) || !exclude_last)) {
  270. const dr = (ticks[1] - ticks[0]) / nminor;
  271. for (let nn = 1; nn < nminor; ++nn) {
  272. const gridr = ticks[n] + dr*nn;
  273. if (gridr > this.scale_rmax) break;
  274. rx = this.r(gridr);
  275. ry = rx / this.szx * this.szy;
  276. this.draw_g.append('ellipse')
  277. .attr('cx', 0)
  278. .attr('cy', 0)
  279. .attr('rx', Math.round(rx))
  280. .attr('ry', Math.round(ry))
  281. .style('fill', 'none')
  282. .style('pointer-events', pointer_events)
  283. .call(this.gridatt.func);
  284. }
  285. }
  286. }
  287. if (ca < 0.999) {
  288. this.draw_g.append('path')
  289. .attr('d', `M0,0L${Math.round(this.szx*ca)},${Math.round(this.szy*sa)}`)
  290. .style('pointer-events', pointer_events)
  291. .call(this.lineatt.func);
  292. }
  293. return this.finishTextDrawing();
  294. }).then(() => {
  295. return this.options.NoLabels ? true : this.drawPolarLabels(polar, nmajor);
  296. }).then(() => {
  297. nminor = Math.floor((polar.fNdivPol % 10000) / 100);
  298. if (nminor > 1) {
  299. for (let n = 0; n < nmajor * nminor; ++n) {
  300. if (n % nminor === 0) continue;
  301. const angle = -n*2*Math.PI/nmajor/nminor;
  302. this.draw_g.append('svg:path')
  303. .attr('d', `M0,0L${Math.round(this.szx*Math.cos(angle))},${Math.round(this.szy*Math.sin(angle))}`)
  304. .call(this.gridatt.func);
  305. }
  306. }
  307. if (this.isBatchMode())
  308. return;
  309. TooltipHandler.assign(this);
  310. assignContextMenu(this, kNoReorder);
  311. this.assignZoomHandler(this.draw_g);
  312. });
  313. }
  314. /** @summary Fill TGraphPolargram context menu */
  315. fillContextMenuItems(menu) {
  316. const pp = this.getObject();
  317. menu.sub('Axis range');
  318. 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');
  319. 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');
  320. 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');
  321. menu.endsub();
  322. menu.addSizeMenu('Axis angle', 0, 315, 45, this.options.rangle || pp.fAxisAngle, v => {
  323. this.options.rangle = pp.fAxisAngle = v;
  324. this.interactiveRedraw('pad', `exec:SetAxisAngle(${v})`);
  325. });
  326. }
  327. /** @summary Assign zoom handler to element
  328. * @private */
  329. assignZoomHandler(elem) {
  330. elem.on('mouseenter', evnt => this.mouseEvent('enter', evnt))
  331. .on('mousemove', evnt => this.mouseEvent('move', evnt))
  332. .on('mouseleave', evnt => this.mouseEvent('leave', evnt));
  333. if (settings.Zooming)
  334. elem.on('dblclick', evnt => this.mouseDoubleClick(evnt));
  335. if (settings.Zooming && settings.ZoomWheel)
  336. elem.on('wheel', evnt => this.mouseWheel(evnt));
  337. }
  338. /** @summary Draw TGraphPolargram */
  339. static async draw(dom, polargram, opt) {
  340. const main = getElementMainPainter(dom);
  341. if (main) {
  342. if (main.getObject() === polargram)
  343. return main;
  344. throw Error('Cannot superimpose TGraphPolargram with any other drawings');
  345. }
  346. const painter = new TGraphPolargramPainter(dom, polargram, opt);
  347. return ensureTCanvas(painter, false).then(() => {
  348. painter.setAsMainPainter();
  349. return painter.redraw();
  350. }).then(() => painter);
  351. }
  352. } // class TGraphPolargramPainter
  353. /**
  354. * @summary Painter for TGraphPolar objects.
  355. *
  356. * @private
  357. */
  358. class TGraphPolarPainter extends ObjectPainter {
  359. /** @summary Decode options for drawing TGraphPolar */
  360. decodeOptions(opt) {
  361. const d = new DrawOptions(opt || 'L');
  362. if (!this.options)
  363. this.options = {};
  364. const rdot = d.check('RDOT'),
  365. rangle = d.check('RANGLE', true) ? d.partAsInt() : 0;
  366. Object.assign(this.options, {
  367. mark: d.check('P'),
  368. err: d.check('E'),
  369. fill: d.check('F'),
  370. line: d.check('L'),
  371. curve: d.check('C'),
  372. radian: d.check('R'),
  373. degree: d.check('D'),
  374. grad: d.check('G'),
  375. Axis: d.check('N') ? 'N' : ''
  376. });
  377. if (d.check('O'))
  378. this.options.Axis += 'O';
  379. if (rdot)
  380. this.options.Axis += '_rdot';
  381. if (rangle)
  382. this.options.Axis += `_rangle${rangle}`;
  383. this.storeDrawOpt(opt);
  384. }
  385. /** @summary Update TGraphPolar with polargram */
  386. updateObject(obj, opt) {
  387. if (!this.matchObjectType(obj))
  388. return false;
  389. if (opt && (opt !== this.options.original))
  390. this.decodeOptions(opt);
  391. if (this._draw_axis && obj.fPolargram)
  392. this.getMainPainter().updateObject(obj.fPolargram);
  393. delete obj.fPolargram;
  394. // copy all properties but not polargram
  395. Object.assign(this.getObject(), obj);
  396. return true;
  397. }
  398. /** @summary Redraw TGraphPolar */
  399. redraw() {
  400. return this.drawGraphPolar().then(() => this.updateTitle());
  401. }
  402. /** @summary Drawing TGraphPolar */
  403. async drawGraphPolar() {
  404. const graph = this.getObject(),
  405. main = this.getMainPainter();
  406. if (!graph || !main?.$polargram)
  407. return;
  408. if (this.options.mark)
  409. this.createAttMarker({ attr: graph });
  410. if (this.options.err || this.options.line || this.options.curve)
  411. this.createAttLine({ attr: graph });
  412. if (this.options.fill)
  413. this.createAttFill({ attr: graph });
  414. this.createG();
  415. if (this._draw_axis && !main.isNormalAngles()) {
  416. const has_err = graph.fEX?.length;
  417. let rwtmin = graph.fX[0],
  418. rwtmax = graph.fX[0];
  419. for (let n = 0; n < graph.fNpoints; ++n) {
  420. rwtmin = Math.min(rwtmin, graph.fX[n] - (has_err ? graph.fEX[n] : 0));
  421. rwtmax = Math.max(rwtmax, graph.fX[n] + (has_err ? graph.fEX[n] : 0));
  422. }
  423. rwtmax += (rwtmax - rwtmin) / graph.fNpoints;
  424. main.setAnglesRange(rwtmin, rwtmax, true);
  425. }
  426. this.draw_g.attr('transform', main.draw_g.attr('transform'));
  427. let mpath = '', epath = '';
  428. const bins = [], pointer_events = this.isBatchMode() ? null : 'visibleFill';
  429. for (let n = 0; n < graph.fNpoints; ++n) {
  430. if (graph.fY[n] > main.scale_rmax)
  431. continue;
  432. if (this.options.err) {
  433. const p1 = main.translate(graph.fX[n], graph.fY[n] - graph.fEY[n]),
  434. p2 = main.translate(graph.fX[n], graph.fY[n] + graph.fEY[n]),
  435. p3 = main.translate(graph.fX[n] + graph.fEX[n], graph.fY[n]),
  436. p4 = main.translate(graph.fX[n] - graph.fEX[n], graph.fY[n]);
  437. epath += `M${p1.grx},${p1.gry}L${p2.grx},${p2.gry}` +
  438. `M${p3.grx},${p3.gry}A${p4.rx},${p4.ry},0,0,1,${p4.grx},${p4.gry}`;
  439. }
  440. const pos = main.translate(graph.fX[n], graph.fY[n]);
  441. if (this.options.mark)
  442. mpath += this.markeratt.create(pos.grx, pos.gry);
  443. if (this.options.curve || this.options.line || this.options.fill)
  444. bins.push(pos);
  445. }
  446. if ((this.options.fill || this.options.line) && bins.length) {
  447. const lpath = buildSvgCurve(bins, { line: true });
  448. if (this.options.fill) {
  449. this.draw_g.append('svg:path')
  450. .attr('d', lpath + 'Z')
  451. .style('pointer-events', pointer_events)
  452. .call(this.fillatt.func);
  453. }
  454. if (this.options.line) {
  455. this.draw_g.append('svg:path')
  456. .attr('d', lpath)
  457. .style('fill', 'none')
  458. .style('pointer-events', pointer_events)
  459. .call(this.lineatt.func);
  460. }
  461. }
  462. if (this.options.curve && bins.length) {
  463. this.draw_g.append('svg:path')
  464. .attr('d', buildSvgCurve(bins))
  465. .style('fill', 'none')
  466. .style('pointer-events', pointer_events)
  467. .call(this.lineatt.func);
  468. }
  469. if (epath) {
  470. this.draw_g.append('svg:path')
  471. .attr('d', epath)
  472. .style('fill', 'none')
  473. .style('pointer-events', pointer_events)
  474. .call(this.lineatt.func);
  475. }
  476. if (mpath) {
  477. this.draw_g.append('svg:path')
  478. .attr('d', mpath)
  479. .style('pointer-events', pointer_events)
  480. .call(this.markeratt.func);
  481. }
  482. if (!this.isBatchMode()) {
  483. assignContextMenu(this, kNoReorder);
  484. main.assignZoomHandler(this.draw_g);
  485. }
  486. }
  487. /** @summary Create polargram object */
  488. createPolargram(gr) {
  489. if (!gr.fPolargram) {
  490. gr.fPolargram = create('TGraphPolargram');
  491. if (this.options.radian)
  492. gr.fPolargram.fRadian = true;
  493. else if (this.options.degree)
  494. gr.fPolargram.fDegree = true;
  495. else if (this.options.grad)
  496. gr.fPolargram.fGrad = true;
  497. }
  498. let rmin = gr.fY[0] || 0, rmax = rmin;
  499. const has_err = gr.fEY?.length;
  500. for (let n = 0; n < gr.fNpoints; ++n) {
  501. rmin = Math.min(rmin, gr.fY[n] - (has_err ? gr.fEY[n] : 0));
  502. rmax = Math.max(rmax, gr.fY[n] + (has_err ? gr.fEY[n] : 0));
  503. }
  504. gr.fPolargram.fRwrmin = rmin - (rmax-rmin)*0.1;
  505. gr.fPolargram.fRwrmax = rmax + (rmax-rmin)*0.1;
  506. return gr.fPolargram;
  507. }
  508. /** @summary Provide tooltip at specified point */
  509. extractTooltip(pnt) {
  510. if (!pnt) return null;
  511. const graph = this.getObject(),
  512. main = this.getMainPainter();
  513. let best_dist2 = 1e10, bestindx = -1, bestpos = null;
  514. for (let n = 0; n < graph.fNpoints; ++n) {
  515. const pos = main.translate(graph.fX[n], graph.fY[n]),
  516. dist2 = (pos.grx - pnt.x)**2 + (pos.gry - pnt.y)**2;
  517. if (dist2 < best_dist2) {
  518. best_dist2 = dist2;
  519. bestindx = n;
  520. bestpos = pos;
  521. }
  522. }
  523. let match_distance = 5;
  524. if (this.markeratt?.used)
  525. match_distance = this.markeratt.getFullSize();
  526. if (Math.sqrt(best_dist2) > match_distance)
  527. return null;
  528. const res = {
  529. name: this.getObject().fName, title: this.getObject().fTitle,
  530. x: bestpos.grx, y: bestpos.gry,
  531. color1: (this.markeratt?.used ? this.markeratt.color : undefined) ?? (this.fillatt?.used ? this.fillatt.color : undefined) ?? this.lineatt?.color,
  532. exact: Math.sqrt(best_dist2) < 4,
  533. lines: [this.getObjectHint()],
  534. binindx: bestindx,
  535. menu_dist: match_distance,
  536. radius: match_distance
  537. };
  538. res.lines.push(`r = ${main.axisAsText('r', graph.fY[bestindx])}`,
  539. `phi = ${main.axisAsText('phi', graph.fX[bestindx])}`);
  540. if (graph.fEY && graph.fEY[bestindx])
  541. res.lines.push(`error r = ${main.axisAsText('r', graph.fEY[bestindx])}`);
  542. if (graph.fEX && graph.fEX[bestindx])
  543. res.lines.push(`error phi = ${main.axisAsText('phi', graph.fEX[bestindx])}`);
  544. return res;
  545. }
  546. /** @summary Only redraw histogram title
  547. * @return {Promise} with painter */
  548. async updateTitle() {
  549. // case when histogram drawn over other histogram (same option)
  550. if (!this._draw_axis)
  551. return this;
  552. const tpainter = this.getPadPainter()?.findPainterFor(null, kTitle, clTPaveText),
  553. pt = tpainter?.getObject();
  554. if (!tpainter || !pt)
  555. return this;
  556. const gr = this.getObject(),
  557. draw_title = !gr.TestBit(kNoTitle) && (gStyle.fOptTitle > 0);
  558. pt.Clear();
  559. if (draw_title) pt.AddText(gr.fTitle);
  560. return tpainter.redraw().then(() => this);
  561. }
  562. /** @summary Draw histogram title
  563. * @return {Promise} with painter */
  564. async drawTitle() {
  565. // case when histogram drawn over other histogram (same option)
  566. if (!this._draw_axis)
  567. return this;
  568. const gr = this.getObject(),
  569. st = gStyle,
  570. draw_title = !gr.TestBit(kNoTitle) && (st.fOptTitle > 0),
  571. pp = this.getPadPainter();
  572. let pt = pp.findInPrimitives(kTitle, clTPaveText);
  573. if (pt) {
  574. pt.Clear();
  575. if (draw_title)
  576. pt.AddText(gr.fTitle);
  577. return this;
  578. }
  579. pt = create(clTPaveText);
  580. Object.assign(pt, { fName: kTitle, fFillColor: st.fTitleColor, fFillStyle: st.fTitleStyle, fBorderSize: st.fTitleBorderSize,
  581. fTextFont: st.fTitleFont, fTextSize: st.fTitleFontSize, fTextColor: st.fTitleTextColor, fTextAlign: 22 });
  582. if (draw_title)
  583. pt.AddText(gr.fTitle);
  584. return TPavePainter.draw(pp, pt, kPosTitle)
  585. .then(p => { p?.setSecondaryId(this, kTitle); return this; });
  586. }
  587. /** @summary Show tooltip */
  588. showTooltip(hint) {
  589. let ttcircle = this.draw_g?.selectChild('.tooltip_bin');
  590. if (!hint || !this.draw_g) {
  591. ttcircle?.remove();
  592. return;
  593. }
  594. if (ttcircle.empty()) {
  595. ttcircle = this.draw_g.append('svg:ellipse')
  596. .attr('class', 'tooltip_bin')
  597. .style('pointer-events', 'none');
  598. }
  599. hint.changed = ttcircle.property('current_bin') !== hint.binindx;
  600. if (hint.changed) {
  601. ttcircle.attr('cx', hint.x)
  602. .attr('cy', hint.y)
  603. .attr('rx', Math.round(hint.radius))
  604. .attr('ry', Math.round(hint.radius))
  605. .style('fill', 'none')
  606. .style('stroke', hint.color1)
  607. .property('current_bin', hint.binindx);
  608. }
  609. }
  610. /** @summary Process tooltip event */
  611. processTooltipEvent(pnt) {
  612. const hint = this.extractTooltip(pnt);
  613. if (!pnt || !pnt.disabled)
  614. this.showTooltip(hint);
  615. return hint;
  616. }
  617. /** @summary Draw TGraphPolar */
  618. static async draw(dom, graph, opt) {
  619. const painter = new TGraphPolarPainter(dom, graph, opt);
  620. painter.decodeOptions(opt);
  621. const main = painter.getMainPainter();
  622. if (main && !main.$polargram) {
  623. console.error('Cannot superimpose TGraphPolar with plain histograms');
  624. return null;
  625. }
  626. let pr = Promise.resolve(null);
  627. if (!main) {
  628. // indicate that axis defined by this graph
  629. painter._draw_axis = true;
  630. pr = TGraphPolargramPainter.draw(dom, painter.createPolargram(graph), painter.options.Axis);
  631. }
  632. return pr.then(gram_painter => {
  633. gram_painter?.setSecondaryId(painter, 'polargram');
  634. painter.addToPadPrimitives();
  635. return painter.drawGraphPolar();
  636. }).then(() => painter.drawTitle());
  637. }
  638. } // class TGraphPolarPainter
  639. export { TGraphPolargramPainter, TGraphPolarPainter };