/* Model which can be instantiated to handle tooltip rendering.
Example usage:
var tip = nv.models.tooltip().gravity('w').distance(23)
.data(myDataObject);
tip(); //just invoke the returned function to render tooltip.
*/
nv.models.tooltip = function() {
"use strict";
/*
Tooltip data. If data is given in the proper format, a consistent tooltip is generated.
Example Format of data:
{
key: "Date",
value: "August 2009",
series: [
{key: "Series 1", value: "Value 1", color: "#000"},
{key: "Series 2", value: "Value 2", color: "#00f"}
]
}
*/
var id = "nvtooltip-" + Math.floor(Math.random() * 100000) // Generates a unique id when you create a new tooltip() object.
, data = null
, gravity = 'w' // Can be 'n','s','e','w'. Determines how tooltip is positioned.
, distance = 25 // Distance to offset tooltip from the mouse location.
, snapDistance = 0 // Tolerance allowed before tooltip is moved from its current position (creates 'snapping' effect)
, classes = null // Attaches additional CSS classes to the tooltip DIV that is created.
, hidden = true // Start off hidden, toggle with hide/show functions below.
, hideDelay = 200 // Delay (in ms) before the tooltip hides after calling hide().
, tooltip = null // d3 select of the tooltip div.
, lastPosition = { left: null, top: null } // Last position the tooltip was in.
, enabled = true // True -> tooltips are rendered. False -> don't render tooltips.
, duration = 100 // Tooltip movement duration, in ms.
, headerEnabled = true // If is to show the tooltip header.
, nvPointerEventsClass = "nv-pointer-events-none" // CSS class to specify whether element should not have mouse events.
;
// Format function for the tooltip values column.
// d is value,
// i is series index
// p is point containing the value
var valueFormatter = function(d, i, p) {
return d;
};
// Format function for the tooltip header value.
var headerFormatter = function(d) {
return d;
};
var keyFormatter = function(d, i) {
return d;
};
// By default, the tooltip model renders a beautiful table inside a DIV, returned as HTML
// You can override this function if a custom tooltip is desired. For instance, you could directly manipulate
// the DOM by accessing elem and returning false.
var contentGenerator = function(d, elem) {
if (d === null) {
return '';
}
var table = d3.select(document.createElement("table"));
if (headerEnabled) {
var theadEnter = table.selectAll("thead")
.data([d])
.enter().append("thead");
theadEnter.append("tr")
.append("td")
.attr("colspan", 3)
.append("strong")
.classed("x-value", true)
.html(headerFormatter(d.value));
}
var tbodyEnter = table.selectAll("tbody")
.data([d])
.enter().append("tbody");
var trowEnter = tbodyEnter.selectAll("tr")
.data(function(p) { return p.series})
.enter()
.append("tr")
.classed("highlight", function(p) { return p.highlight});
trowEnter.append("td")
.classed("legend-color-guide",true)
.append("div")
.style("background-color", function(p) { return p.color});
trowEnter.append("td")
.classed("key",true)
.classed("total",function(p) { return !!p.total})
.html(function(p, i) { return keyFormatter(p.key, i)});
trowEnter.append("td")
.classed("value",true)
.html(function(p, i) { return valueFormatter(p.value, i, p) });
trowEnter.filter(function (p,i) { return p.percent !== undefined }).append("td")
.classed("percent", true)
.html(function(p, i) { return "(" + d3.format('%')(p.percent) + ")" });
trowEnter.selectAll("td").each(function(p) {
if (p.highlight) {
var opacityScale = d3.scale.linear().domain([0,1]).range(["#fff",p.color]);
var opacity = 0.6;
d3.select(this)
.style("border-bottom-color", opacityScale(opacity))
.style("border-top-color", opacityScale(opacity))
;
}
});
var html = table.node().outerHTML;
if (d.footer !== undefined)
html += "
";
return html;
};
/*
Function that returns the position (relative to the viewport/document.body)
the tooltip should be placed in.
Should return: {
left: ,
top:
}
*/
var position = function() {
var pos = {
left: d3.event !== null ? d3.event.clientX : 0,
top: d3.event !== null ? d3.event.clientY : 0
};
if(getComputedStyle(document.body).transform != 'none') {
// Take the offset into account, as now the tooltip is relative
// to document.body.
var client = document.body.getBoundingClientRect();
pos.left -= client.left;
pos.top -= client.top;
}
return pos;
};
var dataSeriesExists = function(d) {
if (d && d.series) {
if (nv.utils.isArray(d.series)) {
return true;
}
// if object, it's okay just convert to array of the object
if (nv.utils.isObject(d.series)) {
d.series = [d.series];
return true;
}
}
return false;
};
// Calculates the gravity offset of the tooltip. Parameter is position of tooltip
// relative to the viewport.
var calcGravityOffset = function(pos) {
var height = tooltip.node().offsetHeight,
width = tooltip.node().offsetWidth,
clientWidth = document.documentElement.clientWidth, // Don't want scrollbars.
clientHeight = document.documentElement.clientHeight, // Don't want scrollbars.
left, top, tmp;
// calculate position based on gravity
switch (gravity) {
case 'e':
left = - width - distance;
top = - (height / 2);
if(pos.left + left < 0) left = distance;
if((tmp = pos.top + top) < 0) top -= tmp;
if((tmp = pos.top + top + height) > clientHeight) top -= tmp - clientHeight;
break;
case 'w':
left = distance;
top = - (height / 2);
if (pos.left + left + width > clientWidth) left = - width - distance;
if ((tmp = pos.top + top) < 0) top -= tmp;
if ((tmp = pos.top + top + height) > clientHeight) top -= tmp - clientHeight;
break;
case 'n':
left = - (width / 2) - 5; // - 5 is an approximation of the mouse's height.
top = distance;
if (pos.top + top + height > clientHeight) top = - height - distance;
if ((tmp = pos.left + left) < 0) left -= tmp;
if ((tmp = pos.left + left + width) > clientWidth) left -= tmp - clientWidth;
break;
case 's':
left = - (width / 2);
top = - height - distance;
if (pos.top + top < 0) top = distance;
if ((tmp = pos.left + left) < 0) left -= tmp;
if ((tmp = pos.left + left + width) > clientWidth) left -= tmp - clientWidth;
break;
case 'center':
left = - (width / 2);
top = - (height / 2);
break;
default:
left = 0;
top = 0;
break;
}
return { 'left': left, 'top': top };
};
/*
Positions the tooltip in the correct place, as given by the position() function.
*/
var positionTooltip = function() {
nv.dom.read(function() {
var pos = position(),
gravityOffset = calcGravityOffset(pos),
left = pos.left + gravityOffset.left,
top = pos.top + gravityOffset.top;
// delay hiding a bit to avoid flickering
if (hidden) {
tooltip
.interrupt()
.transition()
.delay(hideDelay)
.duration(0)
.style('opacity', 0);
} else {
// using tooltip.style('transform') returns values un-usable for tween
var old_translate = 'translate(' + lastPosition.left + 'px, ' + lastPosition.top + 'px)';
var new_translate = 'translate(' + Math.round(left) + 'px, ' + Math.round(top) + 'px)';
var translateInterpolator = d3.interpolateString(old_translate, new_translate);
var is_hidden = tooltip.style('opacity') < 0.1;
tooltip
.interrupt() // cancel running transitions
.transition()
.duration(is_hidden ? 0 : duration)
// using tween since some versions of d3 can't auto-tween a translate on a div
.styleTween('transform', function (d) {
return translateInterpolator;
}, 'important')
// Safari has its own `-webkit-transform` and does not support `transform`
.styleTween('-webkit-transform', function (d) {
return translateInterpolator;
})
.style('-ms-transform', new_translate)
.style('opacity', 1);
}
lastPosition.left = left;
lastPosition.top = top;
});
};
// Creates new tooltip container, or uses existing one on DOM.
function initTooltip() {
if (!tooltip || !tooltip.node()) {
// Create new tooltip div if it doesn't exist on DOM.
var data = [1];
tooltip = d3.select(document.body).selectAll('#'+id).data(data);
tooltip.enter().append('div')
.attr("class", "nvtooltip " + (classes ? classes : "xy-tooltip"))
.attr("id", id)
.style("top", 0).style("left", 0)
.style('opacity', 0)
.style('position', 'absolute')
.selectAll("div, table, td, tr").classed(nvPointerEventsClass, true)
.classed(nvPointerEventsClass, true);
tooltip.exit().remove()
}
}
// Draw the tooltip onto the DOM.
function nvtooltip() {
if (!enabled) return;
if (!dataSeriesExists(data)) return;
nv.dom.write(function () {
initTooltip();
// Generate data and set it into tooltip.
// Bonus - If you override contentGenerator and return false, you can use something like
// Angular, React or Knockout to bind the data for your tooltip directly to the DOM.
var newContent = contentGenerator(data, tooltip.node());
if (newContent) {
tooltip.node().innerHTML = newContent;
}
positionTooltip();
});
return nvtooltip;
}
nvtooltip.nvPointerEventsClass = nvPointerEventsClass;
nvtooltip.options = nv.utils.optionsFunc.bind(nvtooltip);
nvtooltip._options = Object.create({}, {
// simple read/write options
duration: {get: function(){return duration;}, set: function(_){duration=_;}},
gravity: {get: function(){return gravity;}, set: function(_){gravity=_;}},
distance: {get: function(){return distance;}, set: function(_){distance=_;}},
snapDistance: {get: function(){return snapDistance;}, set: function(_){snapDistance=_;}},
classes: {get: function(){return classes;}, set: function(_){classes=_;}},
enabled: {get: function(){return enabled;}, set: function(_){enabled=_;}},
hideDelay: {get: function(){return hideDelay;}, set: function(_){hideDelay=_;}},
contentGenerator: {get: function(){return contentGenerator;}, set: function(_){contentGenerator=_;}},
valueFormatter: {get: function(){return valueFormatter;}, set: function(_){valueFormatter=_;}},
headerFormatter: {get: function(){return headerFormatter;}, set: function(_){headerFormatter=_;}},
keyFormatter: {get: function(){return keyFormatter;}, set: function(_){keyFormatter=_;}},
headerEnabled: {get: function(){return headerEnabled;}, set: function(_){headerEnabled=_;}},
position: {get: function(){return position;}, set: function(_){position=_;}},
// Deprecated options
chartContainer: {get: function(){return document.body;}, set: function(_){
// deprecated after 1.8.3
nv.deprecated('chartContainer', 'feature removed after 1.8.3');
}},
fixedTop: {get: function(){return null;}, set: function(_){
// deprecated after 1.8.1
nv.deprecated('fixedTop', 'feature removed after 1.8.1');
}},
offset: {get: function(){return {left: 0, top: 0};}, set: function(_){
// deprecated after 1.8.1
nv.deprecated('offset', 'use chart.tooltip.distance() instead');
}},
// options with extra logic
hidden: {get: function(){return hidden;}, set: function(_){
if (hidden != _) {
hidden = !!_;
nvtooltip();
}
}},
data: {get: function(){return data;}, set: function(_){
// if showing a single data point, adjust data format with that
if (_.point) {
_.value = _.point.x;
_.series = _.series || {};
_.series.value = _.point.y;
_.series.color = _.point.color || _.series.color;
}
data = _;
}},
// read only properties
node: {get: function(){return tooltip.node();}, set: function(_){}},
id: {get: function(){return id;}, set: function(_){}}
});
nv.utils.initOptions(nvtooltip);
return nvtooltip;
};