/* 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; };