jquery.flot.tooltip.js 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. /*
  2. * jquery.flot.tooltip
  3. *
  4. * description: easy-to-use tooltips for Flot charts
  5. * version: 0.8.7
  6. * authors: Krzysztof Urbas @krzysu [myviews.pl],Evan Steinkerchner @Roundaround
  7. * website: https://github.com/krzysu/flot.tooltip
  8. *
  9. * build on 2016-03-15
  10. * released under MIT License, 2012
  11. */
  12. (function ($) {
  13. // plugin options, default values
  14. var defaultOptions = {
  15. tooltip: {
  16. show: false,
  17. cssClass: "flotTip",
  18. content: "%s | X: %x | Y: %y",
  19. // allowed templates are:
  20. // %s -> series label,
  21. // %c -> series color,
  22. // %lx -> x axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels),
  23. // %ly -> y axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels),
  24. // %x -> X value,
  25. // %y -> Y value,
  26. // %x.2 -> precision of X value,
  27. // %p -> percent
  28. // %n -> value (not percent) of pie chart
  29. xDateFormat: null,
  30. yDateFormat: null,
  31. monthNames: null,
  32. dayNames: null,
  33. shifts: {
  34. x: 10,
  35. y: 20
  36. },
  37. defaultTheme: true,
  38. snap: true,
  39. lines: false,
  40. clickTips: false,
  41. // callbacks
  42. onHover: function (flotItem, $tooltipEl) {},
  43. $compat: false
  44. }
  45. };
  46. // dummy default options object for legacy code (<0.8.5) - is deleted later
  47. defaultOptions.tooltipOpts = defaultOptions.tooltip;
  48. // object
  49. var FlotTooltip = function (plot) {
  50. // variables
  51. this.tipPosition = {x: 0, y: 0};
  52. this.init(plot);
  53. };
  54. // main plugin function
  55. FlotTooltip.prototype.init = function (plot) {
  56. var that = this;
  57. // detect other flot plugins
  58. var plotPluginsLength = $.plot.plugins.length;
  59. this.plotPlugins = [];
  60. if (plotPluginsLength) {
  61. for (var p = 0; p < plotPluginsLength; p++) {
  62. this.plotPlugins.push($.plot.plugins[p].name);
  63. }
  64. }
  65. plot.hooks.bindEvents.push(function (plot, eventHolder) {
  66. // get plot options
  67. that.plotOptions = plot.getOptions();
  68. // for legacy (<0.8.5) implementations
  69. if (typeof(that.plotOptions.tooltip) === 'boolean') {
  70. that.plotOptions.tooltipOpts.show = that.plotOptions.tooltip;
  71. that.plotOptions.tooltip = that.plotOptions.tooltipOpts;
  72. delete that.plotOptions.tooltipOpts;
  73. }
  74. // if not enabled return
  75. if (that.plotOptions.tooltip.show === false || typeof that.plotOptions.tooltip.show === 'undefined') return;
  76. // shortcut to access tooltip options
  77. that.tooltipOptions = that.plotOptions.tooltip;
  78. if (that.tooltipOptions.$compat) {
  79. that.wfunc = 'width';
  80. that.hfunc = 'height';
  81. } else {
  82. that.wfunc = 'innerWidth';
  83. that.hfunc = 'innerHeight';
  84. }
  85. // create tooltip DOM element
  86. var $tip = that.getDomElement();
  87. // bind event
  88. $( plot.getPlaceholder() ).bind("plothover", plothover);
  89. if (that.tooltipOptions.clickTips) {
  90. $( plot.getPlaceholder() ).bind("plotclick", plotclick);
  91. }
  92. that.clickmode = false;
  93. $(eventHolder).bind('mousemove', mouseMove);
  94. });
  95. plot.hooks.shutdown.push(function (plot, eventHolder){
  96. $(plot.getPlaceholder()).unbind("plothover", plothover);
  97. $(plot.getPlaceholder()).unbind("plotclick", plotclick);
  98. plot.removeTooltip();
  99. $(eventHolder).unbind("mousemove", mouseMove);
  100. });
  101. function mouseMove(e){
  102. var pos = {};
  103. pos.x = e.pageX;
  104. pos.y = e.pageY;
  105. plot.setTooltipPosition(pos);
  106. }
  107. /**
  108. * open the tooltip (if not already open) and freeze it on the current position till the next click
  109. */
  110. function plotclick(event, pos, item) {
  111. if (! that.clickmode) {
  112. // it is the click activating the clicktip
  113. plothover(event, pos, item);
  114. if (that.getDomElement().is(":visible")) {
  115. $(plot.getPlaceholder()).unbind("plothover", plothover);
  116. that.clickmode = true;
  117. }
  118. } else {
  119. // it is the click deactivating the clicktip
  120. $( plot.getPlaceholder() ).bind("plothover", plothover);
  121. plot.hideTooltip();
  122. that.clickmode = false;
  123. }
  124. }
  125. function plothover(event, pos, item) {
  126. // Simple distance formula.
  127. var lineDistance = function (p1x, p1y, p2x, p2y) {
  128. return Math.sqrt((p2x - p1x) * (p2x - p1x) + (p2y - p1y) * (p2y - p1y));
  129. };
  130. // Here is some voodoo magic for determining the distance to a line form a given point {x, y}.
  131. var dotLineLength = function (x, y, x0, y0, x1, y1, o) {
  132. if (o && !(o =
  133. function (x, y, x0, y0, x1, y1) {
  134. if (typeof x0 !== 'undefined') return { x: x0, y: y };
  135. else if (typeof y0 !== 'undefined') return { x: x, y: y0 };
  136. var left,
  137. tg = -1 / ((y1 - y0) / (x1 - x0));
  138. return {
  139. x: left = (x1 * (x * tg - y + y0) + x0 * (x * -tg + y - y1)) / (tg * (x1 - x0) + y0 - y1),
  140. y: tg * left - tg * x + y
  141. };
  142. } (x, y, x0, y0, x1, y1),
  143. o.x >= Math.min(x0, x1) && o.x <= Math.max(x0, x1) && o.y >= Math.min(y0, y1) && o.y <= Math.max(y0, y1))
  144. ) {
  145. var l1 = lineDistance(x, y, x0, y0), l2 = lineDistance(x, y, x1, y1);
  146. return l1 > l2 ? l2 : l1;
  147. } else {
  148. var a = y0 - y1, b = x1 - x0, c = x0 * y1 - y0 * x1;
  149. return Math.abs(a * x + b * y + c) / Math.sqrt(a * a + b * b);
  150. }
  151. };
  152. if (item) {
  153. plot.showTooltip(item, that.tooltipOptions.snap ? item : pos);
  154. } else if (that.plotOptions.series.lines.show && that.tooltipOptions.lines === true) {
  155. var maxDistance = that.plotOptions.grid.mouseActiveRadius;
  156. var closestTrace = {
  157. distance: maxDistance + 1
  158. };
  159. var ttPos = pos;
  160. $.each(plot.getData(), function (i, series) {
  161. var xBeforeIndex = 0,
  162. xAfterIndex = -1;
  163. // Our search here assumes our data is sorted via the x-axis.
  164. // TODO: Improve efficiency somehow - search smaller sets of data.
  165. for (var j = 1; j < series.data.length; j++) {
  166. if (series.data[j - 1][0] <= pos.x && series.data[j][0] >= pos.x) {
  167. xBeforeIndex = j - 1;
  168. xAfterIndex = j;
  169. }
  170. }
  171. if (xAfterIndex === -1) {
  172. plot.hideTooltip();
  173. return;
  174. }
  175. var pointPrev = { x: series.data[xBeforeIndex][0], y: series.data[xBeforeIndex][1] },
  176. pointNext = { x: series.data[xAfterIndex][0], y: series.data[xAfterIndex][1] };
  177. var distToLine = dotLineLength(series.xaxis.p2c(pos.x), series.yaxis.p2c(pos.y), series.xaxis.p2c(pointPrev.x),
  178. series.yaxis.p2c(pointPrev.y), series.xaxis.p2c(pointNext.x), series.yaxis.p2c(pointNext.y), false);
  179. if (distToLine < closestTrace.distance) {
  180. var closestIndex = lineDistance(pointPrev.x, pointPrev.y, pos.x, pos.y) <
  181. lineDistance(pos.x, pos.y, pointNext.x, pointNext.y) ? xBeforeIndex : xAfterIndex;
  182. var pointSize = series.datapoints.pointsize;
  183. // Calculate the point on the line vertically closest to our cursor.
  184. var pointOnLine = [
  185. pos.x,
  186. pointPrev.y + ((pointNext.y - pointPrev.y) * ((pos.x - pointPrev.x) / (pointNext.x - pointPrev.x)))
  187. ];
  188. var item = {
  189. datapoint: pointOnLine,
  190. dataIndex: closestIndex,
  191. series: series,
  192. seriesIndex: i
  193. };
  194. closestTrace = {
  195. distance: distToLine,
  196. item: item
  197. };
  198. if (that.tooltipOptions.snap) {
  199. ttPos = {
  200. pageX: series.xaxis.p2c(pointOnLine[0]),
  201. pageY: series.yaxis.p2c(pointOnLine[1])
  202. };
  203. }
  204. }
  205. });
  206. if (closestTrace.distance < maxDistance + 1)
  207. plot.showTooltip(closestTrace.item, ttPos);
  208. else
  209. plot.hideTooltip();
  210. } else {
  211. plot.hideTooltip();
  212. }
  213. }
  214. // Quick little function for setting the tooltip position.
  215. plot.setTooltipPosition = function (pos) {
  216. var $tip = that.getDomElement();
  217. var totalTipWidth = $tip.outerWidth() + that.tooltipOptions.shifts.x;
  218. var totalTipHeight = $tip.outerHeight() + that.tooltipOptions.shifts.y;
  219. if ((pos.x - $(window).scrollLeft()) > ($(window)[that.wfunc]() - totalTipWidth)) {
  220. pos.x -= totalTipWidth;
  221. }
  222. if ((pos.y - $(window).scrollTop()) > ($(window)[that.hfunc]() - totalTipHeight)) {
  223. pos.y -= totalTipHeight;
  224. }
  225. /*
  226. The section applies the new positioning ONLY if pos.x and pos.y
  227. are numbers. If they are undefined or not a number, use the last
  228. known numerical position. This hack fixes a bug that kept pie
  229. charts from keeping their tooltip positioning.
  230. */
  231. if (isNaN(pos.x)) {
  232. that.tipPosition.x = that.tipPosition.xPrev;
  233. }
  234. else {
  235. that.tipPosition.x = pos.x;
  236. that.tipPosition.xPrev = pos.x;
  237. }
  238. if (isNaN(pos.y)) {
  239. that.tipPosition.y = that.tipPosition.yPrev;
  240. }
  241. else {
  242. that.tipPosition.y = pos.y;
  243. that.tipPosition.yPrev = pos.y;
  244. }
  245. };
  246. // Quick little function for showing the tooltip.
  247. plot.showTooltip = function (target, position, targetPosition) {
  248. var $tip = that.getDomElement();
  249. // convert tooltip content template to real tipText
  250. var tipText = that.stringFormat(that.tooltipOptions.content, target);
  251. if (tipText === '')
  252. return;
  253. $tip.html(tipText);
  254. plot.setTooltipPosition({ x: position.pageX, y: position.pageY });
  255. $tip.css({
  256. left: that.tipPosition.x + that.tooltipOptions.shifts.x,
  257. top: that.tipPosition.y + that.tooltipOptions.shifts.y
  258. }).show();
  259. // run callback
  260. if (typeof that.tooltipOptions.onHover === 'function') {
  261. that.tooltipOptions.onHover(target, $tip);
  262. }
  263. };
  264. // Quick little function for hiding the tooltip.
  265. plot.hideTooltip = function () {
  266. that.getDomElement().hide().html('');
  267. };
  268. plot.removeTooltip = function() {
  269. that.getDomElement().remove();
  270. };
  271. };
  272. /**
  273. * get or create tooltip DOM element
  274. * @return jQuery object
  275. */
  276. FlotTooltip.prototype.getDomElement = function () {
  277. var $tip = $('<div>');
  278. if (this.tooltipOptions && this.tooltipOptions.cssClass) {
  279. $tip = $('.' + this.tooltipOptions.cssClass);
  280. if( $tip.length === 0 ){
  281. $tip = $('<div />').addClass(this.tooltipOptions.cssClass);
  282. $tip.appendTo('body').hide().css({position: 'absolute'});
  283. if(this.tooltipOptions.defaultTheme) {
  284. $tip.css({
  285. 'background': '#fff',
  286. 'z-index': '1040',
  287. 'padding': '0.4em 0.6em',
  288. 'border-radius': '0.5em',
  289. 'font-size': '0.8em',
  290. 'border': '1px solid #111',
  291. 'display': 'none',
  292. 'white-space': 'nowrap'
  293. });
  294. }
  295. }
  296. }
  297. return $tip;
  298. };
  299. /**
  300. * core function, create tooltip content
  301. * @param {string} content - template with tooltip content
  302. * @param {object} item - Flot item
  303. * @return {string} real tooltip content for current item
  304. */
  305. FlotTooltip.prototype.stringFormat = function (content, item) {
  306. var percentPattern = /%p\.{0,1}(\d{0,})/;
  307. var seriesPattern = /%s/;
  308. var colorPattern = /%c/;
  309. var xLabelPattern = /%lx/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded
  310. var yLabelPattern = /%ly/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded
  311. var xPattern = /%x\.{0,1}(\d{0,})/;
  312. var yPattern = /%y\.{0,1}(\d{0,})/;
  313. var xPatternWithoutPrecision = "%x";
  314. var yPatternWithoutPrecision = "%y";
  315. var customTextPattern = "%ct";
  316. var nPiePattern = "%n";
  317. var x, y, customText, p, n;
  318. // for threshold plugin we need to read data from different place
  319. if (typeof item.series.threshold !== "undefined") {
  320. x = item.datapoint[0];
  321. y = item.datapoint[1];
  322. customText = item.datapoint[2];
  323. }
  324. // for CurvedLines plugin we need to read data from different place
  325. else if (typeof item.series.curvedLines !== "undefined") {
  326. x = item.datapoint[0];
  327. y = item.datapoint[1];
  328. }
  329. else if (typeof item.series.lines !== "undefined" && item.series.lines.steps) {
  330. x = item.series.datapoints.points[item.dataIndex * 2];
  331. y = item.series.datapoints.points[item.dataIndex * 2 + 1];
  332. // TODO: where to find custom text in this variant?
  333. customText = "";
  334. } else {
  335. x = item.series.data[item.dataIndex][0];
  336. y = item.series.data[item.dataIndex][1];
  337. customText = item.series.data[item.dataIndex][2];
  338. }
  339. // I think this is only in case of threshold plugin
  340. if (item.series.label === null && item.series.originSeries) {
  341. item.series.label = item.series.originSeries.label;
  342. }
  343. // if it is a function callback get the content string
  344. if (typeof(content) === 'function') {
  345. content = content(item.series.label, x, y, item);
  346. }
  347. // the case where the passed content is equal to false
  348. if (typeof(content) === 'boolean' && !content) {
  349. return '';
  350. }
  351. /* replacement of %ct and other multi-character templates must
  352. precede the replacement of single-character templates
  353. to avoid conflict between '%c' and '%ct' and similar substrings
  354. */
  355. if (customText)
  356. content = content.replace(customTextPattern, customText);
  357. // percent match for pie charts and stacked percent
  358. if (typeof (item.series.percent) !== 'undefined') {
  359. p = item.series.percent;
  360. } else if (typeof (item.series.percents) !== 'undefined') {
  361. p = item.series.percents[item.dataIndex];
  362. }
  363. if (typeof p === 'number') {
  364. content = this.adjustValPrecision(percentPattern, content, p);
  365. }
  366. // replace %n with number of items represented by slice in pie charts
  367. if (item.series.hasOwnProperty('pie')) {
  368. if (typeof (item.series.data[0][1] !== 'undefined')) {
  369. n = item.series.data[0][1];
  370. }
  371. }
  372. if (typeof n === 'number') {
  373. content = content.replace(nPiePattern, n);
  374. }
  375. // series match
  376. if (typeof(item.series.label) !== 'undefined') {
  377. content = content.replace(seriesPattern, item.series.label);
  378. } else {
  379. //remove %s if label is undefined
  380. content = content.replace(seriesPattern, "");
  381. }
  382. // color match
  383. if (typeof(item.series.color) !== 'undefined') {
  384. content = content.replace(colorPattern, item.series.color);
  385. } else {
  386. //remove %s if color is undefined
  387. content = content.replace(colorPattern, "");
  388. }
  389. // x axis label match
  390. if (this.hasAxisLabel('xaxis', item)) {
  391. content = content.replace(xLabelPattern, item.series.xaxis.options.axisLabel);
  392. } else {
  393. //remove %lx if axis label is undefined or axislabels plugin not present
  394. content = content.replace(xLabelPattern, "");
  395. }
  396. // y axis label match
  397. if (this.hasAxisLabel('yaxis', item)) {
  398. content = content.replace(yLabelPattern, item.series.yaxis.options.axisLabel);
  399. } else {
  400. //remove %ly if axis label is undefined or axislabels plugin not present
  401. content = content.replace(yLabelPattern, "");
  402. }
  403. // time mode axes with custom dateFormat
  404. if (this.isTimeMode('xaxis', item) && this.isXDateFormat(item)) {
  405. content = content.replace(xPattern, this.timestampToDate(x, this.tooltipOptions.xDateFormat, item.series.xaxis.options));
  406. }
  407. if (this.isTimeMode('yaxis', item) && this.isYDateFormat(item)) {
  408. content = content.replace(yPattern, this.timestampToDate(y, this.tooltipOptions.yDateFormat, item.series.yaxis.options));
  409. }
  410. // set precision if defined
  411. if (typeof x === 'number') {
  412. content = this.adjustValPrecision(xPattern, content, x);
  413. }
  414. if (typeof y === 'number') {
  415. content = this.adjustValPrecision(yPattern, content, y);
  416. }
  417. // change x from number to given label, if given
  418. if (typeof item.series.xaxis.ticks !== 'undefined') {
  419. var ticks;
  420. if (this.hasRotatedXAxisTicks(item)) {
  421. // xaxis.ticks will be an empty array if tickRotor is being used, but the values are available in rotatedTicks
  422. ticks = 'rotatedTicks';
  423. } else {
  424. ticks = 'ticks';
  425. }
  426. // see https://github.com/krzysu/flot.tooltip/issues/65
  427. var tickIndex = item.dataIndex + item.seriesIndex;
  428. for (var xIndex in item.series.xaxis[ticks]) {
  429. if (item.series.xaxis[ticks].hasOwnProperty(tickIndex) && !this.isTimeMode('xaxis', item)) {
  430. var valueX = (this.isCategoriesMode('xaxis', item)) ? item.series.xaxis[ticks][tickIndex].label : item.series.xaxis[ticks][tickIndex].v;
  431. if (valueX === x) {
  432. content = content.replace(xPattern, item.series.xaxis[ticks][tickIndex].label.replace(/\$/g, '$$$$'));
  433. }
  434. }
  435. }
  436. }
  437. // change y from number to given label, if given
  438. if (typeof item.series.yaxis.ticks !== 'undefined') {
  439. for (var yIndex in item.series.yaxis.ticks) {
  440. if (item.series.yaxis.ticks.hasOwnProperty(yIndex)) {
  441. var valueY = (this.isCategoriesMode('yaxis', item)) ? item.series.yaxis.ticks[yIndex].label : item.series.yaxis.ticks[yIndex].v;
  442. if (valueY === y) {
  443. content = content.replace(yPattern, item.series.yaxis.ticks[yIndex].label.replace(/\$/g, '$$$$'));
  444. }
  445. }
  446. }
  447. }
  448. // if no value customization, use tickFormatter by default
  449. if (typeof item.series.xaxis.tickFormatter !== 'undefined') {
  450. //escape dollar
  451. content = content.replace(xPatternWithoutPrecision, item.series.xaxis.tickFormatter(x, item.series.xaxis).replace(/\$/g, '$$'));
  452. }
  453. if (typeof item.series.yaxis.tickFormatter !== 'undefined') {
  454. //escape dollar
  455. content = content.replace(yPatternWithoutPrecision, item.series.yaxis.tickFormatter(y, item.series.yaxis).replace(/\$/g, '$$'));
  456. }
  457. return content;
  458. };
  459. // helpers just for readability
  460. FlotTooltip.prototype.isTimeMode = function (axisName, item) {
  461. return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'time');
  462. };
  463. FlotTooltip.prototype.isXDateFormat = function (item) {
  464. return (typeof this.tooltipOptions.xDateFormat !== 'undefined' && this.tooltipOptions.xDateFormat !== null);
  465. };
  466. FlotTooltip.prototype.isYDateFormat = function (item) {
  467. return (typeof this.tooltipOptions.yDateFormat !== 'undefined' && this.tooltipOptions.yDateFormat !== null);
  468. };
  469. FlotTooltip.prototype.isCategoriesMode = function (axisName, item) {
  470. return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'categories');
  471. };
  472. //
  473. FlotTooltip.prototype.timestampToDate = function (tmst, dateFormat, options) {
  474. var theDate = $.plot.dateGenerator(tmst, options);
  475. return $.plot.formatDate(theDate, dateFormat, this.tooltipOptions.monthNames, this.tooltipOptions.dayNames);
  476. };
  477. //
  478. FlotTooltip.prototype.adjustValPrecision = function (pattern, content, value) {
  479. var precision;
  480. var matchResult = content.match(pattern);
  481. if( matchResult !== null ) {
  482. if(RegExp.$1 !== '') {
  483. precision = RegExp.$1;
  484. value = value.toFixed(precision);
  485. // only replace content if precision exists, in other case use thickformater
  486. content = content.replace(pattern, value);
  487. }
  488. }
  489. return content;
  490. };
  491. // other plugins detection below
  492. // check if flot-axislabels plugin (https://github.com/markrcote/flot-axislabels) is used and that an axis label is given
  493. FlotTooltip.prototype.hasAxisLabel = function (axisName, item) {
  494. return ($.inArray('axisLabels', this.plotPlugins) !== -1 && typeof item.series[axisName].options.axisLabel !== 'undefined' && item.series[axisName].options.axisLabel.length > 0);
  495. };
  496. // check whether flot-tickRotor, a plugin which allows rotation of X-axis ticks, is being used
  497. FlotTooltip.prototype.hasRotatedXAxisTicks = function (item) {
  498. return ($.inArray('tickRotor',this.plotPlugins) !== -1 && typeof item.series.xaxis.rotatedTicks !== 'undefined');
  499. };
  500. //
  501. var init = function (plot) {
  502. new FlotTooltip(plot);
  503. };
  504. // define Flot plugin
  505. $.plot.plugins.push({
  506. init: init,
  507. options: defaultOptions,
  508. name: 'tooltip',
  509. version: '0.8.5'
  510. });
  511. })(jQuery);