|
|
/*
|
|
|
* jquery.flot.tooltip
|
|
|
*
|
|
|
* description: easy-to-use tooltips for Flot charts
|
|
|
* version: 0.8.4
|
|
|
* authors: Krzysztof Urbas @krzysu [myviews.pl],Evan Steinkerchner @Roundaround
|
|
|
* website: https://github.com/krzysu/flot.tooltip
|
|
|
*
|
|
|
* build on 2014-08-06
|
|
|
* released under MIT License, 2012
|
|
|
*/
|
|
|
(function ($) {
|
|
|
// plugin options, default values
|
|
|
var defaultOptions = {
|
|
|
tooltip: false,
|
|
|
tooltipOpts: {
|
|
|
id: "flotTip",
|
|
|
content: "%s | X: %x | Y: %y",
|
|
|
// allowed templates are:
|
|
|
// %s -> series label,
|
|
|
// %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
|
|
|
xDateFormat: null,
|
|
|
yDateFormat: null,
|
|
|
monthNames: null,
|
|
|
dayNames: null,
|
|
|
shifts: {
|
|
|
x: 10,
|
|
|
y: 20
|
|
|
},
|
|
|
defaultTheme: true,
|
|
|
lines: false,
|
|
|
|
|
|
// callbacks
|
|
|
onHover: function (flotItem, $tooltipEl) {},
|
|
|
|
|
|
$compat: false
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
// if not enabled return
|
|
|
if (that.plotOptions.tooltip === false || typeof that.plotOptions.tooltip === 'undefined') return;
|
|
|
|
|
|
// shortcut to access tooltip options
|
|
|
that.tooltipOptions = that.plotOptions.tooltipOpts;
|
|
|
|
|
|
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);
|
|
|
|
|
|
$(eventHolder).bind('mousemove', mouseMove);
|
|
|
});
|
|
|
|
|
|
plot.hooks.shutdown.push(function (plot, eventHolder){
|
|
|
$(plot.getPlaceholder()).unbind("plothover", plothover);
|
|
|
$(eventHolder).unbind("mousemove", mouseMove);
|
|
|
});
|
|
|
|
|
|
function mouseMove(e){
|
|
|
var pos = {};
|
|
|
pos.x = e.pageX;
|
|
|
pos.y = e.pageY;
|
|
|
plot.setTooltipPosition(pos);
|
|
|
}
|
|
|
|
|
|
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, pos);
|
|
|
} else if (that.plotOptions.series.lines.show && that.tooltipOptions.lines === true) {
|
|
|
var maxDistance = that.plotOptions.grid.mouseActiveRadius;
|
|
|
|
|
|
var closestTrace = {
|
|
|
distance: maxDistance + 1
|
|
|
};
|
|
|
|
|
|
$.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 (closestTrace.distance < maxDistance + 1)
|
|
|
plot.showTooltip(closestTrace.item, pos);
|
|
|
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;
|
|
|
}
|
|
|
that.tipPosition.x = pos.x;
|
|
|
that.tipPosition.y = pos.y;
|
|
|
};
|
|
|
|
|
|
// Quick little function for showing the tooltip.
|
|
|
plot.showTooltip = function (target, position) {
|
|
|
var $tip = that.getDomElement();
|
|
|
|
|
|
// convert tooltip content template to real tipText
|
|
|
var tipText = that.stringFormat(that.tooltipOptions.content, target);
|
|
|
|
|
|
$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('');
|
|
|
};
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* get or create tooltip DOM element
|
|
|
* @return jQuery object
|
|
|
*/
|
|
|
FlotTooltip.prototype.getDomElement = function () {
|
|
|
var $tip = $('#' + this.tooltipOptions.id);
|
|
|
|
|
|
if( $tip.length === 0 ){
|
|
|
$tip = $('<div />').attr('id', this.tooltipOptions.id);
|
|
|
$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 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 x, y, customText, p;
|
|
|
|
|
|
// 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];
|
|
|
} 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);
|
|
|
}
|
|
|
|
|
|
// 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);
|
|
|
}
|
|
|
|
|
|
// 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, "");
|
|
|
}
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
if (item.series.xaxis[ticks].length > 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);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// change y from number to given label, if given
|
|
|
if (typeof item.series.yaxis.ticks !== 'undefined') {
|
|
|
for (var index in item.series.yaxis.ticks) {
|
|
|
if (item.series.yaxis.ticks.hasOwnProperty(index)) {
|
|
|
var valueY = (this.isCategoriesMode('yaxis', item)) ? item.series.yaxis.ticks[index].label : item.series.yaxis.ticks[index].v;
|
|
|
if (valueY === y) {
|
|
|
content = content.replace(yPattern, item.series.yaxis.ticks[index].label);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 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, '$$'));
|
|
|
}
|
|
|
|
|
|
if (customText)
|
|
|
content = content.replace(customTextPattern, customText);
|
|
|
|
|
|
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(this.plotPlugins, 'axisLabels') !== -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(this.plotPlugins, 'tickRotor') !== -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.4'
|
|
|
});
|
|
|
|
|
|
})(jQuery);
|
|
|
|