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