363 lines
14 KiB
JavaScript
363 lines
14 KiB
JavaScript
|
|
/* 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 += "<div class='footer'>" + d.footer + "</div>";
|
|
return html;
|
|
|
|
};
|
|
|
|
/*
|
|
Function that returns the position (relative to the viewport/document.body)
|
|
the tooltip should be placed in.
|
|
Should return: {
|
|
left: <leftPos>,
|
|
top: <topPos>
|
|
}
|
|
*/
|
|
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).select('#'+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', 'fixed')
|
|
.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;
|
|
};
|