jquery.flot.events.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. import angular from 'angular';
  2. import $ from 'jquery';
  3. import _ from 'lodash';
  4. //@ts-ignore
  5. import Drop from 'tether-drop';
  6. import { CreatePlotOverlay } from '@grafana/ui';
  7. /** @ngInject */
  8. const createAnnotationToolip: CreatePlotOverlay = (element, event, plot) => {
  9. const injector = angular.element(document).injector();
  10. const content = document.createElement('div');
  11. content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
  12. injector.invoke([
  13. '$compile',
  14. '$rootScope',
  15. ($compile, $rootScope) => {
  16. const eventManager = plot.getOptions().events.manager;
  17. const tmpScope = $rootScope.$new(true);
  18. tmpScope.event = event;
  19. tmpScope.onEdit = () => {
  20. eventManager.editEvent(event);
  21. };
  22. $compile(content)(tmpScope);
  23. tmpScope.$digest();
  24. tmpScope.$destroy();
  25. const drop = new Drop({
  26. target: element[0],
  27. content: content,
  28. position: 'bottom center',
  29. classes: 'drop-popover drop-popover--annotation',
  30. openOn: 'hover',
  31. hoverCloseDelay: 200,
  32. tetherOptions: {
  33. constraints: [{ to: 'window', pin: true, attachment: 'both' }],
  34. },
  35. });
  36. drop.open();
  37. drop.on('close', () => {
  38. setTimeout(() => {
  39. drop.destroy();
  40. });
  41. });
  42. },
  43. ]);
  44. };
  45. let markerElementToAttachTo: any = null;
  46. /** @ngInject */
  47. const createEditPopover: CreatePlotOverlay = (element, event, plot) => {
  48. const eventManager = plot.getOptions().events.manager;
  49. if (eventManager.editorOpen) {
  50. // update marker element to attach to (needed in case of legend on the right
  51. // when there is a double render pass and the initial marker element is removed)
  52. markerElementToAttachTo = element;
  53. return;
  54. }
  55. // mark as openend
  56. eventManager.editorOpened();
  57. // set marker elment to attache to
  58. markerElementToAttachTo = element;
  59. // wait for element to be attached and positioned
  60. setTimeout(() => {
  61. const injector = angular.element(document).injector();
  62. const content = document.createElement('div');
  63. content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
  64. injector.invoke([
  65. '$compile',
  66. '$rootScope',
  67. ($compile, $rootScope) => {
  68. const scope = $rootScope.$new(true);
  69. let drop: any;
  70. scope.event = event;
  71. scope.panelCtrl = eventManager.panelCtrl;
  72. scope.close = () => {
  73. drop.close();
  74. };
  75. $compile(content)(scope);
  76. scope.$digest();
  77. drop = new Drop({
  78. target: markerElementToAttachTo[0],
  79. content: content,
  80. position: 'bottom center',
  81. classes: 'drop-popover drop-popover--form',
  82. openOn: 'click',
  83. tetherOptions: {
  84. constraints: [{ to: 'window', pin: true, attachment: 'both' }],
  85. },
  86. });
  87. drop.open();
  88. eventManager.editorOpened();
  89. drop.on('close', () => {
  90. // need timeout here in order call drop.destroy
  91. setTimeout(() => {
  92. eventManager.editorClosed();
  93. scope.$destroy();
  94. drop.destroy();
  95. });
  96. });
  97. },
  98. ]);
  99. }, 100);
  100. };
  101. export { createEditPopover, createAnnotationToolip };
  102. /*
  103. * jquery.flot.events
  104. *
  105. * description: Flot plugin for adding events/markers to the plot
  106. * version: 0.2.5
  107. * authors:
  108. * Alexander Wunschik <alex@wunschik.net>
  109. * Joel Oughton <joeloughton@gmail.com>
  110. * Nicolas Joseph <www.nicolasjoseph.com>
  111. *
  112. * website: https://github.com/mojoaxel/flot-events
  113. *
  114. * released under MIT License and GPLv2+
  115. */
  116. /**
  117. * A class that allows for the drawing an remove of some object
  118. */
  119. export class DrawableEvent {
  120. _object: any;
  121. _drawFunc: any;
  122. _clearFunc: any;
  123. _moveFunc: any;
  124. _position: any;
  125. _width: any;
  126. _height: any;
  127. /** @ngInject */
  128. constructor(
  129. object: JQuery,
  130. drawFunc: any,
  131. clearFunc: any,
  132. moveFunc: any,
  133. left: number,
  134. top: number,
  135. width: number,
  136. height: number
  137. ) {
  138. this._object = object;
  139. this._drawFunc = drawFunc;
  140. this._clearFunc = clearFunc;
  141. this._moveFunc = moveFunc;
  142. this._position = { left, top };
  143. this._width = width;
  144. this._height = height;
  145. }
  146. width() {
  147. return this._width;
  148. }
  149. height() {
  150. return this._height;
  151. }
  152. position() {
  153. return this._position;
  154. }
  155. draw() {
  156. this._drawFunc(this._object);
  157. }
  158. clear() {
  159. this._clearFunc(this._object);
  160. }
  161. getObject() {
  162. return this._object;
  163. }
  164. moveTo(position: { left: number; top: number }) {
  165. this._position = position;
  166. this._moveFunc(this._object, this._position);
  167. }
  168. }
  169. /**
  170. * Event class that stores options (eventType, min, max, title, description) and the object to draw.
  171. */
  172. export class VisualEvent {
  173. _parent: any;
  174. _options: any;
  175. _drawableEvent: any;
  176. _hidden: any;
  177. /** @ngInject */
  178. constructor(options: any, drawableEvent: DrawableEvent) {
  179. this._options = options;
  180. this._drawableEvent = drawableEvent;
  181. this._hidden = false;
  182. }
  183. visual() {
  184. return this._drawableEvent;
  185. }
  186. getOptions() {
  187. return this._options;
  188. }
  189. getParent() {
  190. return this._parent;
  191. }
  192. isHidden() {
  193. return this._hidden;
  194. }
  195. hide() {
  196. this._hidden = true;
  197. }
  198. unhide() {
  199. this._hidden = false;
  200. }
  201. }
  202. /**
  203. * A Class that handles the event-markers inside the given plot
  204. */
  205. export class EventMarkers {
  206. _events: any;
  207. _types: any;
  208. _plot: any;
  209. eventsEnabled: any;
  210. /** @ngInject */
  211. constructor(plot: any) {
  212. this._events = [];
  213. this._types = [];
  214. this._plot = plot;
  215. this.eventsEnabled = false;
  216. }
  217. getEvents() {
  218. return this._events;
  219. }
  220. setTypes(types: any) {
  221. return (this._types = types);
  222. }
  223. /**
  224. * create internal objects for the given events
  225. */
  226. setupEvents(events: any[]) {
  227. const parts = _.partition(events, 'isRegion');
  228. const regions = parts[0];
  229. events = parts[1];
  230. $.each(events, (index, event) => {
  231. const ve = new VisualEvent(event, this._buildDiv(event));
  232. this._events.push(ve);
  233. });
  234. $.each(regions, (index, event) => {
  235. const vre = new VisualEvent(event, this._buildRegDiv(event));
  236. this._events.push(vre);
  237. });
  238. this._events.sort((a: any, b: any) => {
  239. const ao = a.getOptions(),
  240. bo = b.getOptions();
  241. if (ao.min > bo.min) {
  242. return 1;
  243. }
  244. if (ao.min < bo.min) {
  245. return -1;
  246. }
  247. return 0;
  248. });
  249. }
  250. /**
  251. * draw the events to the plot
  252. */
  253. drawEvents() {
  254. // var o = this._plot.getPlotOffset();
  255. $.each(this._events, (index, event) => {
  256. // check event is inside the graph range
  257. if (this._insidePlot(event.getOptions().min) && !event.isHidden()) {
  258. event.visual().draw();
  259. } else {
  260. event
  261. .visual()
  262. .getObject()
  263. .hide();
  264. }
  265. });
  266. }
  267. /**
  268. * update the position of the event-markers (e.g. after scrolling or zooming)
  269. */
  270. updateEvents() {
  271. const o = this._plot.getPlotOffset();
  272. let left;
  273. let top;
  274. const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
  275. $.each(this._events, (index, event) => {
  276. top = o.top + this._plot.height() - event.visual().height();
  277. left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
  278. event.visual().moveTo({ top: top, left: left });
  279. });
  280. }
  281. /**
  282. * remove all events from the plot
  283. */
  284. _clearEvents() {
  285. $.each(this._events, (index, val) => {
  286. val.visual().clear();
  287. });
  288. this._events = [];
  289. }
  290. /**
  291. * create a DOM element for the given event
  292. */
  293. _buildDiv(event: { eventType: any; min: any; editModel: any }) {
  294. const that = this;
  295. const container = this._plot.getPlaceholder();
  296. const o = this._plot.getPlotOffset();
  297. const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
  298. let top, left, color, markerSize, markerShow, lineStyle, lineWidth;
  299. let markerTooltip;
  300. // map the eventType to a types object
  301. const eventTypeId = event.eventType;
  302. if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
  303. color = '#666';
  304. } else {
  305. color = this._types[eventTypeId].color;
  306. }
  307. if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].markerSize) {
  308. markerSize = 8; //default marker size
  309. } else {
  310. markerSize = this._types[eventTypeId].markerSize;
  311. }
  312. if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerShow === undefined) {
  313. markerShow = true;
  314. } else {
  315. markerShow = this._types[eventTypeId].markerShow;
  316. }
  317. if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
  318. markerTooltip = true;
  319. } else {
  320. markerTooltip = this._types[eventTypeId].markerTooltip;
  321. }
  322. if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
  323. lineStyle = 'dashed'; //default line style
  324. } else {
  325. lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
  326. }
  327. if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
  328. lineWidth = 1; //default line width
  329. } else {
  330. lineWidth = this._types[eventTypeId].lineWidth;
  331. }
  332. let topOffset = xaxis.options.eventSectionHeight || 0;
  333. topOffset = topOffset / 3;
  334. top = o.top + this._plot.height() + topOffset;
  335. left = xaxis.p2c(event.min) + o.left;
  336. const line = $('<div class="events_line flot-temp-elem"></div>')
  337. .css({
  338. position: 'absolute',
  339. opacity: 0.8,
  340. left: left + 'px',
  341. top: 8,
  342. width: lineWidth + 'px',
  343. height: this._plot.height() + topOffset * 0.8,
  344. 'border-left-width': lineWidth + 'px',
  345. 'border-left-style': lineStyle,
  346. 'border-left-color': color,
  347. color: color,
  348. })
  349. .appendTo(container);
  350. if (markerShow) {
  351. const marker = $('<div class="events_marker"></div>').css({
  352. position: 'absolute',
  353. left: -markerSize - Math.round(lineWidth / 2) + 'px',
  354. 'font-size': 0,
  355. 'line-height': 0,
  356. width: 0,
  357. height: 0,
  358. 'border-left': markerSize + 'px solid transparent',
  359. 'border-right': markerSize + 'px solid transparent',
  360. });
  361. marker.appendTo(line);
  362. if (
  363. this._types[eventTypeId] &&
  364. this._types[eventTypeId].position &&
  365. this._types[eventTypeId].position.toUpperCase() === 'BOTTOM'
  366. ) {
  367. marker.css({
  368. top: top - markerSize - 8 + 'px',
  369. 'border-top': 'none',
  370. 'border-bottom': markerSize + 'px solid ' + color,
  371. });
  372. } else {
  373. marker.css({
  374. top: '0px',
  375. 'border-top': markerSize + 'px solid ' + color,
  376. 'border-bottom': 'none',
  377. });
  378. }
  379. marker.data({
  380. event: event,
  381. });
  382. const mouseenter = function(this: any) {
  383. createAnnotationToolip(marker, $(this).data('event'), that._plot);
  384. };
  385. if (event.editModel) {
  386. createEditPopover(marker, event.editModel, that._plot);
  387. }
  388. const mouseleave = () => {
  389. that._plot.clearSelection();
  390. };
  391. if (markerTooltip) {
  392. marker.css({ cursor: 'help' });
  393. marker.hover(mouseenter, mouseleave);
  394. }
  395. }
  396. const drawableEvent = new DrawableEvent(
  397. line,
  398. function drawFunc(obj: { show: () => void }) {
  399. obj.show();
  400. },
  401. (obj: { remove: () => void }) => {
  402. obj.remove();
  403. },
  404. (obj: any, position: { top: any; left: any }) => {
  405. obj.css({
  406. top: position.top,
  407. left: position.left,
  408. });
  409. },
  410. left,
  411. top,
  412. line.width(),
  413. line.height()
  414. );
  415. return drawableEvent;
  416. }
  417. /**
  418. * create a DOM element for the given region
  419. */
  420. _buildRegDiv(event: { eventType: any; min: number; timeEnd: number; editModel: any }) {
  421. const that = this;
  422. const container = this._plot.getPlaceholder();
  423. const o = this._plot.getPlotOffset();
  424. const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
  425. let top,
  426. left,
  427. lineWidth: number,
  428. regionWidth,
  429. lineStyle: string | number | cssPropertySetter,
  430. color: string,
  431. markerTooltip;
  432. // map the eventType to a types object
  433. const eventTypeId = event.eventType;
  434. if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
  435. color = '#666';
  436. } else {
  437. color = this._types[eventTypeId].color;
  438. }
  439. if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
  440. markerTooltip = true;
  441. } else {
  442. markerTooltip = this._types[eventTypeId].markerTooltip;
  443. }
  444. if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
  445. lineWidth = 1; //default line width
  446. } else {
  447. lineWidth = this._types[eventTypeId].lineWidth;
  448. }
  449. if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
  450. lineStyle = 'dashed'; //default line style
  451. } else {
  452. lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
  453. }
  454. const topOffset = 2;
  455. top = o.top + this._plot.height() + topOffset;
  456. const timeFrom = Math.min(event.min, event.timeEnd);
  457. const timeTo = Math.max(event.min, event.timeEnd);
  458. left = xaxis.p2c(timeFrom) + o.left;
  459. const right = xaxis.p2c(timeTo) + o.left;
  460. regionWidth = right - left;
  461. _.each([left, right], position => {
  462. const line = $('<div class="events_line flot-temp-elem"></div>').css({
  463. position: 'absolute',
  464. opacity: 0.8,
  465. left: position + 'px',
  466. top: 8,
  467. width: lineWidth + 'px',
  468. height: this._plot.height() + topOffset,
  469. 'border-left-width': lineWidth + 'px',
  470. 'border-left-style': lineStyle,
  471. 'border-left-color': color,
  472. color: color,
  473. });
  474. line.appendTo(container);
  475. });
  476. const region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
  477. position: 'absolute',
  478. opacity: 0.5,
  479. left: left + 'px',
  480. top: top,
  481. width: Math.round(regionWidth + lineWidth) + 'px',
  482. height: '0.5rem',
  483. 'border-left-color': color,
  484. color: color,
  485. 'background-color': color,
  486. });
  487. region.appendTo(container);
  488. region.data({
  489. event: event,
  490. });
  491. const mouseenter = function(this: any) {
  492. createAnnotationToolip(region, $(this).data('event'), that._plot);
  493. };
  494. if (event.editModel) {
  495. createEditPopover(region, event.editModel, that._plot);
  496. }
  497. const mouseleave = () => {
  498. that._plot.clearSelection();
  499. };
  500. if (markerTooltip) {
  501. region.css({ cursor: 'help' });
  502. region.hover(mouseenter, mouseleave);
  503. }
  504. const drawableEvent = new DrawableEvent(
  505. region,
  506. function drawFunc(obj: { show: () => void }) {
  507. obj.show();
  508. },
  509. (obj: { remove: () => void }) => {
  510. obj.remove();
  511. },
  512. (obj: { css: (arg0: { top: any; left: any }) => void }, position: { top: any; left: any }) => {
  513. obj.css({
  514. top: position.top,
  515. left: position.left,
  516. });
  517. },
  518. left,
  519. top,
  520. region.width(),
  521. region.height()
  522. );
  523. return drawableEvent;
  524. }
  525. /**
  526. * check if the event is inside visible range
  527. */
  528. _insidePlot(x: any) {
  529. const xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
  530. const xc = xaxis.p2c(x);
  531. return xc > 0 && xc < xaxis.p2c(xaxis.max);
  532. }
  533. }
  534. /**
  535. * initialize the plugin for the given plot
  536. */
  537. /** @ngInject */
  538. export function init(this: any, plot: any) {
  539. /*jshint validthis:true */
  540. const that = this;
  541. const eventMarkers = new EventMarkers(plot);
  542. plot.getEvents = () => {
  543. return eventMarkers._events;
  544. };
  545. plot.hideEvents = () => {
  546. $.each(eventMarkers._events, (index, event) => {
  547. event
  548. .visual()
  549. .getObject()
  550. .hide();
  551. });
  552. };
  553. plot.showEvents = () => {
  554. plot.hideEvents();
  555. $.each(eventMarkers._events, (index, event) => {
  556. event.hide();
  557. });
  558. that.eventMarkers.drawEvents();
  559. };
  560. // change events on an existing plot
  561. plot.setEvents = (events: any[]) => {
  562. if (eventMarkers.eventsEnabled) {
  563. eventMarkers.setupEvents(events);
  564. }
  565. };
  566. plot.hooks.processOptions.push((plot: any, options: any) => {
  567. // enable the plugin
  568. if (options.events.data != null) {
  569. eventMarkers.eventsEnabled = true;
  570. }
  571. });
  572. plot.hooks.draw.push((plot: any) => {
  573. const options = plot.getOptions();
  574. if (eventMarkers.eventsEnabled) {
  575. // check for first run
  576. if (eventMarkers.getEvents().length < 1) {
  577. eventMarkers.setTypes(options.events.types);
  578. eventMarkers.setupEvents(options.events.data);
  579. } else {
  580. eventMarkers.updateEvents();
  581. }
  582. }
  583. eventMarkers.drawEvents();
  584. });
  585. }
  586. const defaultOptions: any = {
  587. events: {
  588. data: null,
  589. types: null,
  590. xaxis: 1,
  591. position: 'BOTTOM',
  592. },
  593. };
  594. $.plot.plugins.push({
  595. init: init,
  596. options: defaultOptions,
  597. name: 'events',
  598. version: '0.2.5',
  599. });