jquery.flot.tooltip.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /*
  2. * jquery.flot.tooltip
  3. *
  4. * description: easy-to-use tooltips for Flot charts
  5. * version: 0.7.1
  6. * author: Krzysztof Urbas @krzysu [myviews.pl]
  7. * website: https://github.com/krzysu/flot.tooltip
  8. *
  9. * build on 2014-06-22
  10. * released under MIT License, 2012
  11. */
  12. // IE8 polyfill for Array.indexOf
  13. if (!Array.prototype.indexOf) {
  14. Array.prototype.indexOf = function (searchElement, fromIndex) {
  15. if ( this === undefined || this === null ) {
  16. throw new TypeError( '"this" is null or not defined' );
  17. }
  18. var length = this.length >>> 0; // Hack to convert object.length to a UInt32
  19. fromIndex = +fromIndex || 0;
  20. if (Math.abs(fromIndex) === Infinity) {
  21. fromIndex = 0;
  22. }
  23. if (fromIndex < 0) {
  24. fromIndex += length;
  25. if (fromIndex < 0) {
  26. fromIndex = 0;
  27. }
  28. }
  29. for (;fromIndex < length; fromIndex++) {
  30. if (this[fromIndex] === searchElement) {
  31. return fromIndex;
  32. }
  33. }
  34. return -1;
  35. };
  36. }
  37. (function ($) {
  38. // plugin options, default values
  39. var defaultOptions = {
  40. tooltip: false,
  41. tooltipOpts: {
  42. content: "%s | X: %x | Y: %y",
  43. // allowed templates are:
  44. // %s -> series label,
  45. // %lx -> x axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels),
  46. // %ly -> y axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels),
  47. // %x -> X value,
  48. // %y -> Y value,
  49. // %x.2 -> precision of X value,
  50. // %p -> percent
  51. xDateFormat: null,
  52. yDateFormat: null,
  53. monthNames: null,
  54. dayNames: null,
  55. shifts: {
  56. x: 10,
  57. y: 20
  58. },
  59. defaultTheme: true,
  60. // callbacks
  61. onHover: function(flotItem, $tooltipEl) {}
  62. }
  63. };
  64. // object
  65. var FlotTooltip = function(plot) {
  66. // variables
  67. this.tipPosition = {x: 0, y: 0};
  68. this.init(plot);
  69. };
  70. // main plugin function
  71. FlotTooltip.prototype.init = function(plot) {
  72. var that = this;
  73. // detect other flot plugins
  74. var plotPluginsLength = $.plot.plugins.length;
  75. this.plotPlugins = [];
  76. if (plotPluginsLength) {
  77. for (var p = 0; p < plotPluginsLength; p++) {
  78. this.plotPlugins.push($.plot.plugins[p].name);
  79. }
  80. }
  81. plot.hooks.bindEvents.push(function (plot, eventHolder) {
  82. // get plot options
  83. that.plotOptions = plot.getOptions();
  84. // if not enabled return
  85. if (that.plotOptions.tooltip === false || typeof that.plotOptions.tooltip === 'undefined') return;
  86. // shortcut to access tooltip options
  87. that.tooltipOptions = that.plotOptions.tooltipOpts;
  88. // create tooltip DOM element
  89. var $tip = that.getDomElement();
  90. // bind event
  91. $( plot.getPlaceholder() ).bind("plothover", plothover);
  92. $(eventHolder).bind('mousemove', mouseMove);
  93. });
  94. plot.hooks.shutdown.push(function (plot, eventHolder){
  95. $(plot.getPlaceholder()).unbind("plothover", plothover);
  96. $(eventHolder).unbind("mousemove", mouseMove);
  97. });
  98. function mouseMove(e){
  99. var pos = {};
  100. pos.x = e.pageX;
  101. pos.y = e.pageY;
  102. that.updateTooltipPosition(pos);
  103. }
  104. function plothover(event, pos, item) {
  105. var $tip = that.getDomElement();
  106. if (item) {
  107. var tipText;
  108. // convert tooltip content template to real tipText
  109. tipText = that.stringFormat(that.tooltipOptions.content, item);
  110. $tip.html( tipText );
  111. that.updateTooltipPosition({ x: pos.pageX, y: pos.pageY });
  112. $tip.css({
  113. left: that.tipPosition.x + that.tooltipOptions.shifts.x,
  114. top: that.tipPosition.y + that.tooltipOptions.shifts.y
  115. })
  116. .show();
  117. // run callback
  118. if(typeof that.tooltipOptions.onHover === 'function') {
  119. that.tooltipOptions.onHover(item, $tip);
  120. }
  121. }
  122. else {
  123. $tip.hide().html('');
  124. }
  125. }
  126. };
  127. /**
  128. * get or create tooltip DOM element
  129. * @return jQuery object
  130. */
  131. FlotTooltip.prototype.getDomElement = function() {
  132. var $tip = $('#flotTip');
  133. if( $tip.length === 0 ){
  134. $tip = $('<div />').attr('id', 'flotTip');
  135. $tip.appendTo('body').hide().css({position: 'absolute'});
  136. if(this.tooltipOptions.defaultTheme) {
  137. $tip.css({
  138. 'background': '#fff',
  139. 'z-index': '1040',
  140. 'padding': '0.4em 0.6em',
  141. 'border-radius': '0.5em',
  142. 'font-size': '0.8em',
  143. 'border': '1px solid #111',
  144. 'display': 'none',
  145. 'white-space': 'nowrap'
  146. });
  147. }
  148. }
  149. return $tip;
  150. };
  151. // as the name says
  152. FlotTooltip.prototype.updateTooltipPosition = function(pos) {
  153. var $tip = $('#flotTip');
  154. var totalTipWidth = $tip.outerWidth() + this.tooltipOptions.shifts.x;
  155. var totalTipHeight = $tip.outerHeight() + this.tooltipOptions.shifts.y;
  156. if ((pos.x - $(window).scrollLeft()) > ($(window).innerWidth() - totalTipWidth)) {
  157. pos.x -= totalTipWidth;
  158. }
  159. if ((pos.y - $(window).scrollTop()) > ($(window).innerHeight() - totalTipHeight)) {
  160. pos.y -= totalTipHeight;
  161. }
  162. this.tipPosition.x = pos.x;
  163. this.tipPosition.y = pos.y;
  164. };
  165. /**
  166. * core function, create tooltip content
  167. * @param {string} content - template with tooltip content
  168. * @param {object} item - Flot item
  169. * @return {string} real tooltip content for current item
  170. */
  171. FlotTooltip.prototype.stringFormat = function(content, item) {
  172. var percentPattern = /%p\.{0,1}(\d{0,})/;
  173. var seriesPattern = /%s/;
  174. var xLabelPattern = /%lx/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded
  175. var yLabelPattern = /%ly/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded
  176. var xPattern = /%x\.{0,1}(\d{0,})/;
  177. var yPattern = /%y\.{0,1}(\d{0,})/;
  178. var xPatternWithoutPrecision = "%x";
  179. var yPatternWithoutPrecision = "%y";
  180. var customTextPattern = "%ct";
  181. var x, y, customText;
  182. // for threshold plugin we need to read data from different place
  183. if (typeof item.series.threshold !== "undefined") {
  184. x = item.datapoint[0];
  185. y = item.datapoint[1];
  186. customText = item.datapoint[2];
  187. } else if (typeof item.series.lines !== "undefined" && item.series.lines.steps) {
  188. x = item.series.datapoints.points[item.dataIndex * 2];
  189. y = item.series.datapoints.points[item.dataIndex * 2 + 1];
  190. // TODO: where to find custom text in this variant?
  191. customText = "";
  192. } else {
  193. x = item.series.data[item.dataIndex][0];
  194. y = item.series.data[item.dataIndex][1];
  195. customText = item.series.data[item.dataIndex][2];
  196. }
  197. // I think this is only in case of threshold plugin
  198. if (item.series.label === null && item.series.originSeries) {
  199. item.series.label = item.series.originSeries.label;
  200. }
  201. // if it is a function callback get the content string
  202. if( typeof(content) === 'function' ) {
  203. content = content(item.series.label, x, y, item);
  204. }
  205. // percent match for pie charts
  206. if( typeof (item.series.percent) !== 'undefined' ) {
  207. content = this.adjustValPrecision(percentPattern, content, item.series.percent);
  208. }
  209. // series match
  210. if( typeof(item.series.label) !== 'undefined' ) {
  211. content = content.replace(seriesPattern, item.series.label);
  212. }
  213. else {
  214. //remove %s if label is undefined
  215. content = content.replace(seriesPattern, "");
  216. }
  217. // x axis label match
  218. if( this.hasAxisLabel('xaxis', item) ) {
  219. content = content.replace(xLabelPattern, item.series.xaxis.options.axisLabel);
  220. }
  221. else {
  222. //remove %lx if axis label is undefined or axislabels plugin not present
  223. content = content.replace(xLabelPattern, "");
  224. }
  225. // y axis label match
  226. if( this.hasAxisLabel('yaxis', item) ) {
  227. content = content.replace(yLabelPattern, item.series.yaxis.options.axisLabel);
  228. }
  229. else {
  230. //remove %ly if axis label is undefined or axislabels plugin not present
  231. content = content.replace(yLabelPattern, "");
  232. }
  233. // time mode axes with custom dateFormat
  234. if(this.isTimeMode('xaxis', item) && this.isXDateFormat(item)) {
  235. content = content.replace(xPattern, this.timestampToDate(x, this.tooltipOptions.xDateFormat, item.series.xaxis.options));
  236. }
  237. if(this.isTimeMode('yaxis', item) && this.isYDateFormat(item)) {
  238. content = content.replace(yPattern, this.timestampToDate(y, this.tooltipOptions.yDateFormat, item.series.yaxis.options));
  239. }
  240. // set precision if defined
  241. if(typeof x === 'number') {
  242. content = this.adjustValPrecision(xPattern, content, x);
  243. }
  244. if(typeof y === 'number') {
  245. content = this.adjustValPrecision(yPattern, content, y);
  246. }
  247. // change x from number to given label, if given
  248. if(typeof item.series.xaxis.ticks !== 'undefined') {
  249. var ticks;
  250. if(this.hasRotatedXAxisTicks(item)) {
  251. // xaxis.ticks will be an empty array if tickRotor is being used, but the values are available in rotatedTicks
  252. ticks = 'rotatedTicks';
  253. }
  254. else {
  255. ticks = 'ticks';
  256. }
  257. // see https://github.com/krzysu/flot.tooltip/issues/65
  258. var tickIndex = item.dataIndex + item.seriesIndex;
  259. if(item.series.xaxis[ticks].length > tickIndex && !this.isTimeMode('xaxis', item)) {
  260. var valueX = (this.isCategoriesMode('xaxis', item)) ? item.series.xaxis[ticks][tickIndex].label : item.series.xaxis[ticks][tickIndex].v;
  261. if (valueX === x) {
  262. content = content.replace(xPattern, item.series.xaxis[ticks][tickIndex].label);
  263. }
  264. }
  265. }
  266. // change y from number to given label, if given
  267. if(typeof item.series.yaxis.ticks !== 'undefined') {
  268. for (var index in item.series.yaxis.ticks) {
  269. if (item.series.yaxis.ticks.hasOwnProperty(index)) {
  270. var valueY = (this.isCategoriesMode('yaxis', item)) ? item.series.yaxis.ticks[index].label : item.series.yaxis.ticks[index].v;
  271. if (valueY === y) {
  272. content = content.replace(yPattern, item.series.yaxis.ticks[index].label);
  273. }
  274. }
  275. }
  276. }
  277. // if no value customization, use tickFormatter by default
  278. if(typeof item.series.xaxis.tickFormatter !== 'undefined') {
  279. //escape dollar
  280. content = content.replace(xPatternWithoutPrecision, item.series.xaxis.tickFormatter(x, item.series.xaxis).replace(/\$/g, '$$'));
  281. }
  282. if(typeof item.series.yaxis.tickFormatter !== 'undefined') {
  283. //escape dollar
  284. content = content.replace(yPatternWithoutPrecision, item.series.yaxis.tickFormatter(y, item.series.yaxis).replace(/\$/g, '$$'));
  285. }
  286. if(customText) {
  287. content = content.replace(customTextPattern, customText);
  288. }
  289. return content;
  290. };
  291. // helpers just for readability
  292. FlotTooltip.prototype.isTimeMode = function(axisName, item) {
  293. return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'time');
  294. };
  295. FlotTooltip.prototype.isXDateFormat = function(item) {
  296. return (typeof this.tooltipOptions.xDateFormat !== 'undefined' && this.tooltipOptions.xDateFormat !== null);
  297. };
  298. FlotTooltip.prototype.isYDateFormat = function(item) {
  299. return (typeof this.tooltipOptions.yDateFormat !== 'undefined' && this.tooltipOptions.yDateFormat !== null);
  300. };
  301. FlotTooltip.prototype.isCategoriesMode = function(axisName, item) {
  302. return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'categories');
  303. };
  304. //
  305. FlotTooltip.prototype.timestampToDate = function(tmst, dateFormat, options) {
  306. var theDate = $.plot.dateGenerator(tmst, options);
  307. return $.plot.formatDate(theDate, dateFormat, this.tooltipOptions.monthNames, this.tooltipOptions.dayNames);
  308. };
  309. //
  310. FlotTooltip.prototype.adjustValPrecision = function(pattern, content, value) {
  311. var precision;
  312. var matchResult = content.match(pattern);
  313. if( matchResult !== null ) {
  314. if(RegExp.$1 !== '') {
  315. precision = RegExp.$1;
  316. value = value.toFixed(precision);
  317. // only replace content if precision exists, in other case use thickformater
  318. content = content.replace(pattern, value);
  319. }
  320. }
  321. return content;
  322. };
  323. // other plugins detection below
  324. // check if flot-axislabels plugin (https://github.com/markrcote/flot-axislabels) is used and that an axis label is given
  325. FlotTooltip.prototype.hasAxisLabel = function(axisName, item) {
  326. return (this.plotPlugins.indexOf('axisLabels') !== -1 && typeof item.series[axisName].options.axisLabel !== 'undefined' && item.series[axisName].options.axisLabel.length > 0);
  327. };
  328. // check whether flot-tickRotor, a plugin which allows rotation of X-axis ticks, is being used
  329. FlotTooltip.prototype.hasRotatedXAxisTicks = function(item) {
  330. return ($.grep($.plot.plugins, function(p){ return p.name === "tickRotor"; }).length === 1 && typeof item.series.xaxis.rotatedTicks !== 'undefined');
  331. };
  332. //
  333. var init = function(plot) {
  334. new FlotTooltip(plot);
  335. };
  336. // define Flot plugin
  337. $.plot.plugins.push({
  338. init: init,
  339. options: defaultOptions,
  340. name: 'tooltip',
  341. version: '0.6.7'
  342. });
  343. })(jQuery);