jquery.flot.events.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. /**
  2. * Flot plugin for adding 'events' to the plot.
  3. *
  4. * Events are small icons drawn onto the graph that represent something happening at that time.
  5. *
  6. * This plugin adds the following options to flot:
  7. *
  8. * options = {
  9. * events: {
  10. * levels: int // number of hierarchy levels
  11. * data: [], // array of event objects
  12. * types: [] // array of icons
  13. * xaxis: int // the x axis to attach events to
  14. * }
  15. * };
  16. *
  17. *
  18. * An event is a javascript object in the following form:
  19. *
  20. * {
  21. * min: startTime,
  22. * max: endTime,
  23. * eventType: "type",
  24. * title: "event title",
  25. * description: "event description"
  26. * }
  27. *
  28. * Types is an array of javascript objects in the following form:
  29. *
  30. * types: [
  31. * {
  32. * eventType: "eventType",
  33. * level: hierarchicalLevel,
  34. * icon: {
  35. image: "eventImage1.png",
  36. * width: 10,
  37. * height: 10
  38. * }
  39. * }
  40. * ]
  41. *
  42. * @author Joel Oughton
  43. */
  44. (function($){
  45. function init(plot){
  46. var DEFAULT_ICON = {
  47. icon: "icon-caret-up",
  48. size: 20,
  49. width: 19,
  50. height: 10
  51. };
  52. var _events = [], _types, _eventsEnabled = false, lastRange;
  53. plot.getEvents = function(){
  54. return _events;
  55. };
  56. plot.hideEvents = function(levelRange){
  57. $.each(_events, function(index, event){
  58. if (_withinHierarchy(event.level(), levelRange)) {
  59. event.visual().getObject().hide();
  60. }
  61. });
  62. };
  63. plot.showEvents = function(levelRange){
  64. plot.hideEvents();
  65. $.each(_events, function(index, event){
  66. if (!_withinHierarchy(event.level(), levelRange)) {
  67. event.hide();
  68. }
  69. });
  70. _drawEvents();
  71. };
  72. plot.hooks.processOptions.push(function(plot, options){
  73. // enable the plugin
  74. if (options.events.data != null) {
  75. _eventsEnabled = true;
  76. }
  77. });
  78. plot.hooks.draw.push(function(plot, canvascontext){
  79. var options = plot.getOptions();
  80. var xaxis = plot.getXAxes()[options.events.xaxis - 1];
  81. if (_eventsEnabled) {
  82. // check for first run
  83. if (_events.length < 1) {
  84. _lastRange = xaxis.max - xaxis.min;
  85. // check for clustering
  86. if (options.events.clustering) {
  87. var ed = _clusterEvents(options.events.types, options.events.data, xaxis.max - xaxis.min);
  88. _types = ed.types;
  89. _setupEvents(ed.data);
  90. } else {
  91. _types = options.events.types;
  92. _setupEvents(options.events.data);
  93. }
  94. } else {
  95. /*if (options.events.clustering) {
  96. _clearEvents();
  97. var ed = _clusterEvents(options.events.types, options.events.data, xaxis.max - xaxis.min);
  98. _types = ed.types;
  99. _setupEvents(ed.data);
  100. }*/
  101. _updateEvents();
  102. }
  103. }
  104. _drawEvents();
  105. });
  106. var _drawEvents = function() {
  107. var o = plot.getPlotOffset();
  108. var pleft = o.left, pright = plot.width() - o.right;
  109. $.each(_events, function(index, event){
  110. // check event is inside the graph range and inside the hierarchy level
  111. if (_insidePlot(event.getOptions().min) &&
  112. !event.isHidden()) {
  113. event.visual().draw();
  114. } else {
  115. event.visual().getObject().hide();
  116. }
  117. });
  118. _identicalStarts();
  119. _overlaps();
  120. };
  121. var _withinHierarchy = function(level, levelRange){
  122. var range = {};
  123. if (!levelRange) {
  124. range.start = 0;
  125. range.end = _events.length - 1;
  126. } else {
  127. range.start = (levelRange.min == undefined) ? 0 : levelRange.min;
  128. range.end = (levelRange.max == undefined) ? _events.length - 1 : levelRange.max;
  129. }
  130. if (level >= range.start && level <= range.end) {
  131. return true;
  132. }
  133. return false;
  134. };
  135. var _clearEvents = function(){
  136. $.each(_events, function(index, val) {
  137. val.visual().clear();
  138. });
  139. _events = [];
  140. };
  141. var _updateEvents = function() {
  142. var o = plot.getPlotOffset(), left, top;
  143. var xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1];
  144. $.each(_events, function(index, event) {
  145. top = o.top + plot.height() - event.visual().height();
  146. left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
  147. event.visual().moveTo({ top: top, left: left });
  148. });
  149. };
  150. var _showTooltip = function(x, y, event){
  151. /*
  152. var tooltip = $('<div id="tooltip" class=""></div>').appendTo('body').fadeIn(200);
  153. $('<div id="title">' + event.title + '</div>').appendTo(tooltip);
  154. $('<div id="type">Type: ' + event.eventType + '</div>').appendTo(tooltip);
  155. $('<div id="description">' + event.description + '</div>').appendTo(tooltip);
  156. tooltip.css({
  157. top: y - tooltip.height() - 5,
  158. left: x
  159. });
  160. console.log(tooltip);
  161. */
  162. // @rashidkpc - hack to work with our normal tooltip placer
  163. var $tooltip = $('<div id="tooltip">');
  164. if (event) {
  165. $tooltip
  166. .html(event.description)
  167. .place_tt(x, y, {
  168. offset: 10
  169. });
  170. } else {
  171. $tooltip.remove();
  172. }
  173. };
  174. var _setupEvents = function(events){
  175. $.each(events, function(index, event){
  176. var level = (plot.getOptions().events.levels == null || !_types || !_types[event.eventType]) ? 0 : _types[event.eventType].level;
  177. if (level > plot.getOptions().events.levels) {
  178. throw "A type's level has exceeded the maximum. Level=" +
  179. level +
  180. ", Max levels:" +
  181. (plot.getOptions().events.levels);
  182. }
  183. _events.push(new VisualEvent(event, _buildDiv(event), level));
  184. });
  185. _events.sort(compareEvents);
  186. };
  187. var _identicalStarts = function() {
  188. var ranges = [], range = {}, event, prev, offset = 0;
  189. $.each(_events, function(index, val) {
  190. if (prev) {
  191. if (val.getOptions().min == prev.getOptions().min) {
  192. if (!range.min) {
  193. range.min = index;
  194. }
  195. range.max = index;
  196. } else {
  197. if (range.min) {
  198. ranges.push(range);
  199. range = {};
  200. }
  201. }
  202. }
  203. prev = val;
  204. });
  205. if (range.min) {
  206. ranges.push(range);
  207. }
  208. $.each(ranges, function(index, val) {
  209. var removed = _events.splice(val.min - offset, val.max - val.min + 1);
  210. $.each(removed, function(index, val) {
  211. val.visual().clear();
  212. });
  213. offset += val.max - val.min + 1;
  214. });
  215. };
  216. var _overlaps = function() {
  217. var xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1];
  218. var range, diff, cmid, pmid, left = 0, right = -1;
  219. pright = plot.width() - plot.getPlotOffset().right;
  220. // coverts a clump of events into a single vertical line
  221. var processClump = function() {
  222. // find the middle x value
  223. pmid = _events[right].getOptions().min -
  224. (_events[right].getOptions().min - _events[left].getOptions().min) / 2;
  225. cmid = xaxis.p2c(pmid);
  226. // hide the events between the discovered range
  227. while (left <= right) {
  228. _events[left++].visual().getObject().hide();
  229. }
  230. // draw a vertical line in the middle of where they are
  231. if (_insidePlot(pmid)) {
  232. _drawLine('#000', 1, { x: cmid, y: 0 }, { x: cmid, y: plot.height() });
  233. }
  234. };
  235. if (xaxis.min && xaxis.max) {
  236. range = xaxis.max - xaxis.min;
  237. for (var i = 1; i < _events.length; i++) {
  238. diff = _events[i].getOptions().min - _events[i - 1].getOptions().min;
  239. if (diff / range > 0.007) { //enough variance
  240. // has a clump has been found
  241. if (right != -1) {
  242. //processClump();
  243. }
  244. right = -1;
  245. left = i;
  246. } else { // not enough variance
  247. right = i;
  248. // handle to final case
  249. if (i == _events.length - 1) {
  250. //processClump();
  251. }
  252. }
  253. }
  254. }
  255. };
  256. var _buildDiv = function(event){
  257. //var po = plot.pointOffset({ x: 450, y: 1});
  258. var container = plot.getPlaceholder(), o = plot.getPlotOffset(), yaxis,
  259. xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1], axes = plot.getAxes();
  260. var top, left, div, icon, level, drawableEvent;
  261. // determine the y axis used
  262. if (axes.yaxis && axes.yaxis.used) yaxis = axes.yaxis;
  263. if (axes.yaxis2 && axes.yaxis2.used) yaxis = axes.yaxis2;
  264. // use the default icon and level
  265. if (_types == null || !_types[event.eventType] || !_types[event.eventType].icon) {
  266. icon = DEFAULT_ICON;
  267. level = 0;
  268. } else {
  269. icon = _types[event.eventType].icon;
  270. level = _types[event.eventType].level;
  271. }
  272. div = $('<i style="position:absolute" class="'+icon.icon+'"></i>').appendTo(container);
  273. top = o.top + plot.height() - icon.size + 1;
  274. left = xaxis.p2c(event.min) + o.left - icon.size / 2;
  275. div.css({
  276. left: left + 'px',
  277. top: top,
  278. color: icon.color,
  279. "text-shadow" : "1px 1px "+icon.outline+", -1px -1px "+icon.outline+", -1px 1px "+icon.outline+", 1px -1px "+icon.outline,
  280. 'font-size': icon['size']+'px',
  281. });
  282. div.hide();
  283. div.data({
  284. "event": event
  285. });
  286. div.hover(
  287. // mouseenter
  288. function(){
  289. var pos = $(this).offset();
  290. _showTooltip(pos.left + $(this).width() / 2, pos.top, $(this).data("event"));
  291. },
  292. // mouseleave
  293. function(){
  294. //$(this).data("bouncing", false);
  295. $('#tooltip').remove();
  296. plot.clearSelection();
  297. });
  298. drawableEvent = new DrawableEvent(
  299. div,
  300. function(obj){
  301. obj.show();
  302. },
  303. function(obj){
  304. obj.remove();
  305. },
  306. function(obj, position){
  307. obj.css({
  308. top: position.top,
  309. left: position.left
  310. });
  311. },
  312. left, top, div.width(), div.height());
  313. return drawableEvent;
  314. };
  315. var _getEventsAtPos = function(x, y){
  316. var found = [], left, top, width, height;
  317. $.each(_events, function(index, val){
  318. left = val.div.offset().left;
  319. top = val.div.offset().top;
  320. width = val.div.width();
  321. height = val.div.height();
  322. if (x >= left && x <= left + width && y >= top && y <= top + height) {
  323. found.push(val);
  324. }
  325. return found;
  326. });
  327. };
  328. var _insidePlot = function(x) {
  329. var xaxis = plot.getXAxes()[plot.getOptions().events.xaxis - 1];
  330. var xc = xaxis.p2c(x);
  331. return xc > 0 && xc < xaxis.p2c(xaxis.max);
  332. };
  333. var _drawLine = function(color, lineWidth, from, to) {
  334. var ctx = plot.getCanvas().getContext("2d");
  335. var plotOffset = plot.getPlotOffset();
  336. ctx.save();
  337. ctx.translate(plotOffset.left, plotOffset.top);
  338. ctx.beginPath();
  339. ctx.strokeStyle = color;
  340. ctx.lineWidth = lineWidth;
  341. ctx.moveTo(from.x, from.y);
  342. ctx.lineTo(to.x, to.y);
  343. ctx.stroke();
  344. ctx.restore();
  345. };
  346. /**
  347. * Runs over the given 2d array of event objects and returns an object
  348. * containing:
  349. *
  350. * {
  351. * types {}, // An array containing all the different event types
  352. * data [], // An array of the clustered events
  353. * }
  354. *
  355. * @param {Object} types
  356. * an object containing event types
  357. * @param {Object} events
  358. * an array of event to cluster
  359. * @param {Object} range
  360. * the current graph range
  361. */
  362. var _clusterEvents = function(types, events, range) {
  363. //TODO: support custom types
  364. var groups, clusters = [], newEvents = [];
  365. // split into same evenType groups
  366. groups = _groupEvents(events);
  367. $.each(groups.eventTypes, function(index, val) {
  368. clusters.push(_varianceAlgorithm(groups.groupedEvents[val], 1, range));
  369. });
  370. // summarise clusters
  371. $.each(clusters, function(index, eventType) {
  372. // each cluser of each event type
  373. $.each(eventType, function(index, cluster) {
  374. var newEvent = {
  375. min: cluster[0].min,
  376. max: cluster[cluster.length - 1].min, //TODO: needs to be max of end event if it exists
  377. eventType: cluster[0].eventType + ",cluster",
  378. title: "Cluster of: " + cluster[0].title,
  379. description: cluster[0].description + ", Number of events in the cluster: " + cluster.length
  380. };
  381. newEvents.push(newEvent);
  382. });
  383. });
  384. return { types: types, data: newEvents };
  385. };
  386. /**
  387. * Runs over the given 2d array of event objects and returns an object
  388. * containing:
  389. *
  390. * {
  391. * eventTypes [], // An array containing all the different event types
  392. * groupedEvents {}, // An object containing all the grouped events
  393. * }
  394. *
  395. * @param {Object} events
  396. * an array of event objects
  397. */
  398. var _groupEvents = function(events) {
  399. var eventTypes = [], groupedEvents = {};
  400. $.each(events, function(index, val) {
  401. if (!groupedEvents[val.eventType]) {
  402. groupedEvents[val.eventType] = [];
  403. eventTypes.push(val.eventType);
  404. }
  405. groupedEvents[val.eventType].push(val);
  406. });
  407. return { eventTypes: eventTypes, groupedEvents: groupedEvents };
  408. };
  409. /**
  410. * Runs over the given 2d array of event objects and returns a 3d array of
  411. * the same events,but clustered into groups with similar x deltas.
  412. *
  413. * This function assumes that the events are related. So it must be run on
  414. * each set of related events.
  415. *
  416. * @param {Object} events
  417. * an array of event objects
  418. * @param {Object} sens
  419. * a measure of the level of grouping tolerance
  420. * @param {Object} space
  421. * the size of the space we have to place clusters within
  422. */
  423. var _varianceAlgorithm = function(events, sens, space) {
  424. var cluster, clusters = [], sum = 0, avg, density;
  425. // find the average x delta
  426. for (var i = 1; i < events.length - 1; i++) {
  427. sum += events[i].min - events[i - 1].min;
  428. }
  429. avg = sum / (events.length - 2);
  430. // first point
  431. cluster = [ events[0] ];
  432. // middle points
  433. for (var i = 1; i < events.length; i++) {
  434. var leftDiff = events[i].min - events[i - 1].min;
  435. density = leftDiff / space;
  436. if (leftDiff > avg * sens && density > 0.05) {
  437. clusters.push(cluster);
  438. cluster = [ events[i] ];
  439. } else {
  440. cluster.push(events[i]);
  441. }
  442. }
  443. clusters.push(cluster);
  444. return clusters;
  445. };
  446. }
  447. var options = {
  448. events: {
  449. levels: null,
  450. data: null,
  451. types: null,
  452. xaxis: 1,
  453. clustering: false
  454. }
  455. };
  456. $.plot.plugins.push({
  457. init: init,
  458. options: options,
  459. name: "events",
  460. version: "0.20"
  461. });
  462. /**
  463. * A class that allows for the drawing an remove of some object
  464. *
  465. * @param {Object} object
  466. * the drawable object
  467. * @param {Object} drawFunc
  468. * the draw function
  469. * @param {Object} clearFunc
  470. * the clear function
  471. */
  472. function DrawableEvent(object, drawFunc, clearFunc, moveFunc, left, top, width, height){
  473. var _object = object, _drawFunc = drawFunc, _clearFunc = clearFunc, _moveFunc = moveFunc,
  474. _position = { left: left, top: top }, _width = width, _height = height;
  475. this.width = function() { return _width; };
  476. this.height = function() { return _height };
  477. this.position = function() { return _position; };
  478. this.draw = function() { _drawFunc(_object); };
  479. this.clear = function() { _clearFunc(_object); };
  480. this.getObject = function() { return _object; };
  481. this.moveTo = function(position) {
  482. _position = position;
  483. _moveFunc(_object, _position);
  484. };
  485. }
  486. /**
  487. * Event class that stores options (eventType, min, max, title, description) and the object to draw.
  488. *
  489. * @param {Object} options
  490. * @param {Object} drawableEvent
  491. */
  492. function VisualEvent(options, drawableEvent, level){
  493. var _parent, _options = options, _drawableEvent = drawableEvent,
  494. _level = level, _hidden = false;
  495. this.visual = function() { return _drawableEvent; }
  496. this.level = function() { return _level; };
  497. this.getOptions = function() { return _options; };
  498. this.getParent = function() { return _parent; };
  499. this.isHidden = function() { return _hidden; };
  500. this.hide = function() { _hidden = true; };
  501. this.unhide = function() { _hidden = false; };
  502. }
  503. function compareEvents(a, b) {
  504. var ao = a.getOptions(), bo = b.getOptions();
  505. if (ao.min > bo.min) return 1;
  506. if (ao.min < bo.min) return -1;
  507. return 0;
  508. };
  509. })(jQuery);