You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
604 lines
23 KiB
604 lines
23 KiB
/* |
|
* jquery.flot.tooltip |
|
* |
|
* description: easy-to-use tooltips for Flot charts |
|
* version: 0.8.7 |
|
* authors: Krzysztof Urbas @krzysu [myviews.pl],Evan Steinkerchner @Roundaround |
|
* website: https://github.com/krzysu/flot.tooltip |
|
* |
|
* build on 2016-03-15 |
|
* released under MIT License, 2012 |
|
*/ |
|
(function ($) { |
|
// plugin options, default values |
|
var defaultOptions = { |
|
tooltip: { |
|
show: false, |
|
cssClass: "flotTip", |
|
content: "%s | X: %x | Y: %y", |
|
// allowed templates are: |
|
// %s -> series label, |
|
// %c -> series color, |
|
// %lx -> x axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels), |
|
// %ly -> y axis label (requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels), |
|
// %x -> X value, |
|
// %y -> Y value, |
|
// %x.2 -> precision of X value, |
|
// %p -> percent |
|
// %n -> value (not percent) of pie chart |
|
xDateFormat: null, |
|
yDateFormat: null, |
|
monthNames: null, |
|
dayNames: null, |
|
shifts: { |
|
x: 10, |
|
y: 20 |
|
}, |
|
defaultTheme: true, |
|
snap: true, |
|
lines: false, |
|
clickTips: false, |
|
|
|
// callbacks |
|
onHover: function (flotItem, $tooltipEl) {}, |
|
|
|
$compat: false |
|
} |
|
}; |
|
|
|
// dummy default options object for legacy code (<0.8.5) - is deleted later |
|
defaultOptions.tooltipOpts = defaultOptions.tooltip; |
|
|
|
// object |
|
var FlotTooltip = function (plot) { |
|
// variables |
|
this.tipPosition = {x: 0, y: 0}; |
|
|
|
this.init(plot); |
|
}; |
|
|
|
// main plugin function |
|
FlotTooltip.prototype.init = function (plot) { |
|
var that = this; |
|
|
|
// detect other flot plugins |
|
var plotPluginsLength = $.plot.plugins.length; |
|
this.plotPlugins = []; |
|
|
|
if (plotPluginsLength) { |
|
for (var p = 0; p < plotPluginsLength; p++) { |
|
this.plotPlugins.push($.plot.plugins[p].name); |
|
} |
|
} |
|
|
|
plot.hooks.bindEvents.push(function (plot, eventHolder) { |
|
|
|
// get plot options |
|
that.plotOptions = plot.getOptions(); |
|
|
|
// for legacy (<0.8.5) implementations |
|
if (typeof(that.plotOptions.tooltip) === 'boolean') { |
|
that.plotOptions.tooltipOpts.show = that.plotOptions.tooltip; |
|
that.plotOptions.tooltip = that.plotOptions.tooltipOpts; |
|
delete that.plotOptions.tooltipOpts; |
|
} |
|
|
|
// if not enabled return |
|
if (that.plotOptions.tooltip.show === false || typeof that.plotOptions.tooltip.show === 'undefined') return; |
|
|
|
// shortcut to access tooltip options |
|
that.tooltipOptions = that.plotOptions.tooltip; |
|
|
|
if (that.tooltipOptions.$compat) { |
|
that.wfunc = 'width'; |
|
that.hfunc = 'height'; |
|
} else { |
|
that.wfunc = 'innerWidth'; |
|
that.hfunc = 'innerHeight'; |
|
} |
|
|
|
// create tooltip DOM element |
|
var $tip = that.getDomElement(); |
|
|
|
// bind event |
|
$( plot.getPlaceholder() ).bind("plothover", plothover); |
|
if (that.tooltipOptions.clickTips) { |
|
$( plot.getPlaceholder() ).bind("plotclick", plotclick); |
|
} |
|
that.clickmode = false; |
|
|
|
$(eventHolder).bind('mousemove', mouseMove); |
|
}); |
|
|
|
plot.hooks.shutdown.push(function (plot, eventHolder){ |
|
$(plot.getPlaceholder()).unbind("plothover", plothover); |
|
$(plot.getPlaceholder()).unbind("plotclick", plotclick); |
|
plot.removeTooltip(); |
|
$(eventHolder).unbind("mousemove", mouseMove); |
|
}); |
|
|
|
function mouseMove(e){ |
|
var pos = {}; |
|
pos.x = e.pageX; |
|
pos.y = e.pageY; |
|
plot.setTooltipPosition(pos); |
|
} |
|
|
|
/** |
|
* open the tooltip (if not already open) and freeze it on the current position till the next click |
|
*/ |
|
function plotclick(event, pos, item) { |
|
if (! that.clickmode) { |
|
// it is the click activating the clicktip |
|
plothover(event, pos, item); |
|
if (that.getDomElement().is(":visible")) { |
|
$(plot.getPlaceholder()).unbind("plothover", plothover); |
|
that.clickmode = true; |
|
} |
|
} else { |
|
// it is the click deactivating the clicktip |
|
$( plot.getPlaceholder() ).bind("plothover", plothover); |
|
plot.hideTooltip(); |
|
that.clickmode = false; |
|
} |
|
} |
|
|
|
function plothover(event, pos, item) { |
|
// Simple distance formula. |
|
var lineDistance = function (p1x, p1y, p2x, p2y) { |
|
return Math.sqrt((p2x - p1x) * (p2x - p1x) + (p2y - p1y) * (p2y - p1y)); |
|
}; |
|
|
|
// Here is some voodoo magic for determining the distance to a line form a given point {x, y}. |
|
var dotLineLength = function (x, y, x0, y0, x1, y1, o) { |
|
if (o && !(o = |
|
function (x, y, x0, y0, x1, y1) { |
|
if (typeof x0 !== 'undefined') return { x: x0, y: y }; |
|
else if (typeof y0 !== 'undefined') return { x: x, y: y0 }; |
|
|
|
var left, |
|
tg = -1 / ((y1 - y0) / (x1 - x0)); |
|
|
|
return { |
|
x: left = (x1 * (x * tg - y + y0) + x0 * (x * -tg + y - y1)) / (tg * (x1 - x0) + y0 - y1), |
|
y: tg * left - tg * x + y |
|
}; |
|
} (x, y, x0, y0, x1, y1), |
|
o.x >= Math.min(x0, x1) && o.x <= Math.max(x0, x1) && o.y >= Math.min(y0, y1) && o.y <= Math.max(y0, y1)) |
|
) { |
|
var l1 = lineDistance(x, y, x0, y0), l2 = lineDistance(x, y, x1, y1); |
|
return l1 > l2 ? l2 : l1; |
|
} else { |
|
var a = y0 - y1, b = x1 - x0, c = x0 * y1 - y0 * x1; |
|
return Math.abs(a * x + b * y + c) / Math.sqrt(a * a + b * b); |
|
} |
|
}; |
|
|
|
if (item) { |
|
plot.showTooltip(item, that.tooltipOptions.snap ? item : pos); |
|
} else if (that.plotOptions.series.lines.show && that.tooltipOptions.lines === true) { |
|
var maxDistance = that.plotOptions.grid.mouseActiveRadius; |
|
|
|
var closestTrace = { |
|
distance: maxDistance + 1 |
|
}; |
|
|
|
var ttPos = pos; |
|
|
|
$.each(plot.getData(), function (i, series) { |
|
var xBeforeIndex = 0, |
|
xAfterIndex = -1; |
|
|
|
// Our search here assumes our data is sorted via the x-axis. |
|
// TODO: Improve efficiency somehow - search smaller sets of data. |
|
for (var j = 1; j < series.data.length; j++) { |
|
if (series.data[j - 1][0] <= pos.x && series.data[j][0] >= pos.x) { |
|
xBeforeIndex = j - 1; |
|
xAfterIndex = j; |
|
} |
|
} |
|
|
|
if (xAfterIndex === -1) { |
|
plot.hideTooltip(); |
|
return; |
|
} |
|
|
|
var pointPrev = { x: series.data[xBeforeIndex][0], y: series.data[xBeforeIndex][1] }, |
|
pointNext = { x: series.data[xAfterIndex][0], y: series.data[xAfterIndex][1] }; |
|
|
|
var distToLine = dotLineLength(series.xaxis.p2c(pos.x), series.yaxis.p2c(pos.y), series.xaxis.p2c(pointPrev.x), |
|
series.yaxis.p2c(pointPrev.y), series.xaxis.p2c(pointNext.x), series.yaxis.p2c(pointNext.y), false); |
|
|
|
if (distToLine < closestTrace.distance) { |
|
|
|
var closestIndex = lineDistance(pointPrev.x, pointPrev.y, pos.x, pos.y) < |
|
lineDistance(pos.x, pos.y, pointNext.x, pointNext.y) ? xBeforeIndex : xAfterIndex; |
|
|
|
var pointSize = series.datapoints.pointsize; |
|
|
|
// Calculate the point on the line vertically closest to our cursor. |
|
var pointOnLine = [ |
|
pos.x, |
|
pointPrev.y + ((pointNext.y - pointPrev.y) * ((pos.x - pointPrev.x) / (pointNext.x - pointPrev.x))) |
|
]; |
|
|
|
var item = { |
|
datapoint: pointOnLine, |
|
dataIndex: closestIndex, |
|
series: series, |
|
seriesIndex: i |
|
}; |
|
|
|
closestTrace = { |
|
distance: distToLine, |
|
item: item |
|
}; |
|
|
|
if (that.tooltipOptions.snap) { |
|
ttPos = { |
|
pageX: series.xaxis.p2c(pointOnLine[0]), |
|
pageY: series.yaxis.p2c(pointOnLine[1]) |
|
}; |
|
} |
|
} |
|
}); |
|
|
|
if (closestTrace.distance < maxDistance + 1) |
|
plot.showTooltip(closestTrace.item, ttPos); |
|
else |
|
plot.hideTooltip(); |
|
} else { |
|
plot.hideTooltip(); |
|
} |
|
} |
|
|
|
// Quick little function for setting the tooltip position. |
|
plot.setTooltipPosition = function (pos) { |
|
var $tip = that.getDomElement(); |
|
|
|
var totalTipWidth = $tip.outerWidth() + that.tooltipOptions.shifts.x; |
|
var totalTipHeight = $tip.outerHeight() + that.tooltipOptions.shifts.y; |
|
if ((pos.x - $(window).scrollLeft()) > ($(window)[that.wfunc]() - totalTipWidth)) { |
|
pos.x -= totalTipWidth; |
|
} |
|
if ((pos.y - $(window).scrollTop()) > ($(window)[that.hfunc]() - totalTipHeight)) { |
|
pos.y -= totalTipHeight; |
|
} |
|
|
|
/* |
|
The section applies the new positioning ONLY if pos.x and pos.y |
|
are numbers. If they are undefined or not a number, use the last |
|
known numerical position. This hack fixes a bug that kept pie |
|
charts from keeping their tooltip positioning. |
|
*/ |
|
|
|
if (isNaN(pos.x)) { |
|
that.tipPosition.x = that.tipPosition.xPrev; |
|
} |
|
else { |
|
that.tipPosition.x = pos.x; |
|
that.tipPosition.xPrev = pos.x; |
|
} |
|
if (isNaN(pos.y)) { |
|
that.tipPosition.y = that.tipPosition.yPrev; |
|
} |
|
else { |
|
that.tipPosition.y = pos.y; |
|
that.tipPosition.yPrev = pos.y; |
|
} |
|
|
|
}; |
|
|
|
// Quick little function for showing the tooltip. |
|
plot.showTooltip = function (target, position, targetPosition) { |
|
var $tip = that.getDomElement(); |
|
|
|
// convert tooltip content template to real tipText |
|
var tipText = that.stringFormat(that.tooltipOptions.content, target); |
|
if (tipText === '') |
|
return; |
|
|
|
$tip.html(tipText); |
|
plot.setTooltipPosition({ x: position.pageX, y: position.pageY }); |
|
$tip.css({ |
|
left: that.tipPosition.x + that.tooltipOptions.shifts.x, |
|
top: that.tipPosition.y + that.tooltipOptions.shifts.y |
|
}).show(); |
|
|
|
// run callback |
|
if (typeof that.tooltipOptions.onHover === 'function') { |
|
that.tooltipOptions.onHover(target, $tip); |
|
} |
|
}; |
|
|
|
// Quick little function for hiding the tooltip. |
|
plot.hideTooltip = function () { |
|
that.getDomElement().hide().html(''); |
|
}; |
|
|
|
plot.removeTooltip = function() { |
|
that.getDomElement().remove(); |
|
}; |
|
}; |
|
|
|
/** |
|
* get or create tooltip DOM element |
|
* @return jQuery object |
|
*/ |
|
FlotTooltip.prototype.getDomElement = function () { |
|
var $tip = $('<div>'); |
|
if (this.tooltipOptions && this.tooltipOptions.cssClass) { |
|
$tip = $('.' + this.tooltipOptions.cssClass); |
|
|
|
if( $tip.length === 0 ){ |
|
$tip = $('<div />').addClass(this.tooltipOptions.cssClass); |
|
$tip.appendTo('body').hide().css({position: 'absolute'}); |
|
|
|
if(this.tooltipOptions.defaultTheme) { |
|
$tip.css({ |
|
'background': '#fff', |
|
'z-index': '1040', |
|
'padding': '0.4em 0.6em', |
|
'border-radius': '0.5em', |
|
'font-size': '0.8em', |
|
'border': '1px solid #111', |
|
'display': 'none', |
|
'white-space': 'nowrap' |
|
}); |
|
} |
|
} |
|
} |
|
|
|
return $tip; |
|
}; |
|
|
|
/** |
|
* core function, create tooltip content |
|
* @param {string} content - template with tooltip content |
|
* @param {object} item - Flot item |
|
* @return {string} real tooltip content for current item |
|
*/ |
|
FlotTooltip.prototype.stringFormat = function (content, item) { |
|
var percentPattern = /%p\.{0,1}(\d{0,})/; |
|
var seriesPattern = /%s/; |
|
var colorPattern = /%c/; |
|
var xLabelPattern = /%lx/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded |
|
var yLabelPattern = /%ly/; // requires flot-axislabels plugin https://github.com/markrcote/flot-axislabels, will be ignored if plugin isn't loaded |
|
var xPattern = /%x\.{0,1}(\d{0,})/; |
|
var yPattern = /%y\.{0,1}(\d{0,})/; |
|
var xPatternWithoutPrecision = "%x"; |
|
var yPatternWithoutPrecision = "%y"; |
|
var customTextPattern = "%ct"; |
|
var nPiePattern = "%n"; |
|
|
|
var x, y, customText, p, n; |
|
|
|
// for threshold plugin we need to read data from different place |
|
if (typeof item.series.threshold !== "undefined") { |
|
x = item.datapoint[0]; |
|
y = item.datapoint[1]; |
|
customText = item.datapoint[2]; |
|
} |
|
|
|
// for CurvedLines plugin we need to read data from different place |
|
else if (typeof item.series.curvedLines !== "undefined") { |
|
x = item.datapoint[0]; |
|
y = item.datapoint[1]; |
|
} |
|
|
|
else if (typeof item.series.lines !== "undefined" && item.series.lines.steps) { |
|
x = item.series.datapoints.points[item.dataIndex * 2]; |
|
y = item.series.datapoints.points[item.dataIndex * 2 + 1]; |
|
// TODO: where to find custom text in this variant? |
|
customText = ""; |
|
} else { |
|
x = item.series.data[item.dataIndex][0]; |
|
y = item.series.data[item.dataIndex][1]; |
|
customText = item.series.data[item.dataIndex][2]; |
|
} |
|
|
|
// I think this is only in case of threshold plugin |
|
if (item.series.label === null && item.series.originSeries) { |
|
item.series.label = item.series.originSeries.label; |
|
} |
|
|
|
// if it is a function callback get the content string |
|
if (typeof(content) === 'function') { |
|
content = content(item.series.label, x, y, item); |
|
} |
|
|
|
// the case where the passed content is equal to false |
|
if (typeof(content) === 'boolean' && !content) { |
|
return ''; |
|
} |
|
|
|
/* replacement of %ct and other multi-character templates must |
|
precede the replacement of single-character templates |
|
to avoid conflict between '%c' and '%ct' and similar substrings |
|
*/ |
|
if (customText) |
|
content = content.replace(customTextPattern, customText); |
|
|
|
// percent match for pie charts and stacked percent |
|
if (typeof (item.series.percent) !== 'undefined') { |
|
p = item.series.percent; |
|
} else if (typeof (item.series.percents) !== 'undefined') { |
|
p = item.series.percents[item.dataIndex]; |
|
} |
|
if (typeof p === 'number') { |
|
content = this.adjustValPrecision(percentPattern, content, p); |
|
} |
|
|
|
// replace %n with number of items represented by slice in pie charts |
|
if (item.series.hasOwnProperty('pie')) { |
|
if (typeof (item.series.data[0][1] !== 'undefined')) { |
|
n = item.series.data[0][1]; |
|
} |
|
} |
|
if (typeof n === 'number') { |
|
content = content.replace(nPiePattern, n); |
|
} |
|
|
|
// series match |
|
if (typeof(item.series.label) !== 'undefined') { |
|
content = content.replace(seriesPattern, item.series.label); |
|
} else { |
|
//remove %s if label is undefined |
|
content = content.replace(seriesPattern, ""); |
|
} |
|
|
|
// color match |
|
if (typeof(item.series.color) !== 'undefined') { |
|
content = content.replace(colorPattern, item.series.color); |
|
} else { |
|
//remove %s if color is undefined |
|
content = content.replace(colorPattern, ""); |
|
} |
|
|
|
// x axis label match |
|
if (this.hasAxisLabel('xaxis', item)) { |
|
content = content.replace(xLabelPattern, item.series.xaxis.options.axisLabel); |
|
} else { |
|
//remove %lx if axis label is undefined or axislabels plugin not present |
|
content = content.replace(xLabelPattern, ""); |
|
} |
|
|
|
// y axis label match |
|
if (this.hasAxisLabel('yaxis', item)) { |
|
content = content.replace(yLabelPattern, item.series.yaxis.options.axisLabel); |
|
} else { |
|
//remove %ly if axis label is undefined or axislabels plugin not present |
|
content = content.replace(yLabelPattern, ""); |
|
} |
|
|
|
// time mode axes with custom dateFormat |
|
if (this.isTimeMode('xaxis', item) && this.isXDateFormat(item)) { |
|
content = content.replace(xPattern, this.timestampToDate(x, this.tooltipOptions.xDateFormat, item.series.xaxis.options)); |
|
} |
|
if (this.isTimeMode('yaxis', item) && this.isYDateFormat(item)) { |
|
content = content.replace(yPattern, this.timestampToDate(y, this.tooltipOptions.yDateFormat, item.series.yaxis.options)); |
|
} |
|
|
|
// set precision if defined |
|
if (typeof x === 'number') { |
|
content = this.adjustValPrecision(xPattern, content, x); |
|
} |
|
if (typeof y === 'number') { |
|
content = this.adjustValPrecision(yPattern, content, y); |
|
} |
|
|
|
// change x from number to given label, if given |
|
if (typeof item.series.xaxis.ticks !== 'undefined') { |
|
|
|
var ticks; |
|
if (this.hasRotatedXAxisTicks(item)) { |
|
// xaxis.ticks will be an empty array if tickRotor is being used, but the values are available in rotatedTicks |
|
ticks = 'rotatedTicks'; |
|
} else { |
|
ticks = 'ticks'; |
|
} |
|
|
|
// see https://github.com/krzysu/flot.tooltip/issues/65 |
|
var tickIndex = item.dataIndex + item.seriesIndex; |
|
|
|
for (var xIndex in item.series.xaxis[ticks]) { |
|
if (item.series.xaxis[ticks].hasOwnProperty(tickIndex) && !this.isTimeMode('xaxis', item)) { |
|
var valueX = (this.isCategoriesMode('xaxis', item)) ? item.series.xaxis[ticks][tickIndex].label : item.series.xaxis[ticks][tickIndex].v; |
|
if (valueX === x) { |
|
content = content.replace(xPattern, item.series.xaxis[ticks][tickIndex].label.replace(/\$/g, '$$$$')); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// change y from number to given label, if given |
|
if (typeof item.series.yaxis.ticks !== 'undefined') { |
|
for (var yIndex in item.series.yaxis.ticks) { |
|
if (item.series.yaxis.ticks.hasOwnProperty(yIndex)) { |
|
var valueY = (this.isCategoriesMode('yaxis', item)) ? item.series.yaxis.ticks[yIndex].label : item.series.yaxis.ticks[yIndex].v; |
|
if (valueY === y) { |
|
content = content.replace(yPattern, item.series.yaxis.ticks[yIndex].label.replace(/\$/g, '$$$$')); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// if no value customization, use tickFormatter by default |
|
if (typeof item.series.xaxis.tickFormatter !== 'undefined') { |
|
//escape dollar |
|
content = content.replace(xPatternWithoutPrecision, item.series.xaxis.tickFormatter(x, item.series.xaxis).replace(/\$/g, '$$')); |
|
} |
|
if (typeof item.series.yaxis.tickFormatter !== 'undefined') { |
|
//escape dollar |
|
content = content.replace(yPatternWithoutPrecision, item.series.yaxis.tickFormatter(y, item.series.yaxis).replace(/\$/g, '$$')); |
|
} |
|
|
|
return content; |
|
}; |
|
|
|
// helpers just for readability |
|
FlotTooltip.prototype.isTimeMode = function (axisName, item) { |
|
return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'time'); |
|
}; |
|
|
|
FlotTooltip.prototype.isXDateFormat = function (item) { |
|
return (typeof this.tooltipOptions.xDateFormat !== 'undefined' && this.tooltipOptions.xDateFormat !== null); |
|
}; |
|
|
|
FlotTooltip.prototype.isYDateFormat = function (item) { |
|
return (typeof this.tooltipOptions.yDateFormat !== 'undefined' && this.tooltipOptions.yDateFormat !== null); |
|
}; |
|
|
|
FlotTooltip.prototype.isCategoriesMode = function (axisName, item) { |
|
return (typeof item.series[axisName].options.mode !== 'undefined' && item.series[axisName].options.mode === 'categories'); |
|
}; |
|
|
|
// |
|
FlotTooltip.prototype.timestampToDate = function (tmst, dateFormat, options) { |
|
var theDate = $.plot.dateGenerator(tmst, options); |
|
return $.plot.formatDate(theDate, dateFormat, this.tooltipOptions.monthNames, this.tooltipOptions.dayNames); |
|
}; |
|
|
|
// |
|
FlotTooltip.prototype.adjustValPrecision = function (pattern, content, value) { |
|
|
|
var precision; |
|
var matchResult = content.match(pattern); |
|
if( matchResult !== null ) { |
|
if(RegExp.$1 !== '') { |
|
precision = RegExp.$1; |
|
value = value.toFixed(precision); |
|
|
|
// only replace content if precision exists, in other case use thickformater |
|
content = content.replace(pattern, value); |
|
} |
|
} |
|
return content; |
|
}; |
|
|
|
// other plugins detection below |
|
|
|
// check if flot-axislabels plugin (https://github.com/markrcote/flot-axislabels) is used and that an axis label is given |
|
FlotTooltip.prototype.hasAxisLabel = function (axisName, item) { |
|
return ($.inArray('axisLabels', this.plotPlugins) !== -1 && typeof item.series[axisName].options.axisLabel !== 'undefined' && item.series[axisName].options.axisLabel.length > 0); |
|
}; |
|
|
|
// check whether flot-tickRotor, a plugin which allows rotation of X-axis ticks, is being used |
|
FlotTooltip.prototype.hasRotatedXAxisTicks = function (item) { |
|
return ($.inArray('tickRotor',this.plotPlugins) !== -1 && typeof item.series.xaxis.rotatedTicks !== 'undefined'); |
|
}; |
|
|
|
// |
|
var init = function (plot) { |
|
new FlotTooltip(plot); |
|
}; |
|
|
|
// define Flot plugin |
|
$.plot.plugins.push({ |
|
init: init, |
|
options: defaultOptions, |
|
name: 'tooltip', |
|
version: '0.8.5' |
|
}); |
|
|
|
})(jQuery);
|
|
|