/* Tooltip rendering model for nvd3 charts. window.nv.models.tooltip is the updated,new way to render tooltips. window.nv.tooltip.show is the old tooltip code. window.nv.tooltip.* also has various helper methods. */ (function() { "use strict"; window.nv.tooltip = {}; /* 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. */ window.nv.models.tooltip = function() { var content = null //HTML contents of the tooltip. If null, the content is generated via the data variable. , data = null /* Tooltip data. If data is given in the proper format, a consistent tooltip is generated. 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" } ] } */ , gravity = 'w' //Can be 'n','s','e','w'. Determines how tooltip is positioned. , distance = 50 //Distance to offset tooltip from the mouse location. , snapDistance = 25 //Tolerance allowed before tooltip is moved from its current position (creates 'snapping' effect) , fixedTop = null //If not null, this fixes the top position of the tooltip. , classes = null //Attaches additional CSS classes to the tooltip DIV that is created. , chartContainer = null //Parent DIV, of the SVG Container that holds the chart. , tooltipElem = null //actual DOM element representing the tooltip. , position = {left: null, top: null} //Relative position of the tooltip inside chartContainer. , enabled = true //True -> tooltips are rendered. False -> don't render tooltips. //Generates a unique id when you create a new tooltip() object , id = "nvtooltip-" + Math.floor(Math.random() * 100000) ; //CSS class to specify whether element should not have mouse events. var nvPointerEventsClass = "nv-pointer-events-none"; //Format function for the tooltip values column var valueFormatter = function(d,i) { return d; }; //Format function for the tooltip header value. var headerFormatter = function(d) { return d; }; //By default, the tooltip model renders a beautiful table inside a DIV. //You can override this function if a custom tooltip is desired. var contentGenerator = function(d) { if (content != null) return content; if (d == null) return ''; var table = d3.select(document.createElement("table")); 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) .html(function(p) {return p.key}); trowEnter.append("td") .classed("value",true) .html(function(p,i) { return valueFormatter(p.value,i) }); 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; }; var dataSeriesExists = function(d) { if (d && d.series && d.series.length > 0) return true; return false; }; //In situations where the chart is in a 'viewBox', re-position the tooltip based on how far chart is zoomed. function convertViewBoxRatio() { if (chartContainer) { var svg = d3.select(chartContainer); if (svg.node().tagName !== "svg") { svg = svg.select("svg"); } var viewBox = (svg.node()) ? svg.attr('viewBox') : null; if (viewBox) { viewBox = viewBox.split(' '); var ratio = parseInt(svg.style('width')) / viewBox[2]; position.left = position.left * ratio; position.top = position.top * ratio; } } } //Creates new tooltip container, or uses existing one on DOM. function getTooltipContainer(newContent) { var body; if (chartContainer) body = d3.select(chartContainer); else body = d3.select("body"); var container = body.select(".nvtooltip"); if (container.node() === null) { //Create new tooltip div if it doesn't exist on DOM. container = body.append("div") .attr("class", "nvtooltip " + (classes? classes: "xy-tooltip")) .attr("id",id) ; } container.node().innerHTML = newContent; container.style("top",0).style("left",0).style("opacity",0); container.selectAll("div, table, td, tr").classed(nvPointerEventsClass,true) container.classed(nvPointerEventsClass,true); return container.node(); } //Draw the tooltip onto the DOM. function nvtooltip() { if (!enabled) return; if (!dataSeriesExists(data)) return; convertViewBoxRatio(); var left = position.left; var top = (fixedTop != null) ? fixedTop : position.top; var container = getTooltipContainer(contentGenerator(data)); tooltipElem = container; if (chartContainer) { var svgComp = chartContainer.getElementsByTagName("svg")[0]; var boundRect = (svgComp) ? svgComp.getBoundingClientRect() : chartContainer.getBoundingClientRect(); var svgOffset = {left:0,top:0}; if (svgComp) { var svgBound = svgComp.getBoundingClientRect(); var chartBound = chartContainer.getBoundingClientRect(); var svgBoundTop = svgBound.top; //Defensive code. Sometimes, svgBoundTop can be a really negative // number, like -134254. That's a bug. // If such a number is found, use zero instead. FireFox bug only if (svgBoundTop < 0) { var containerBound = chartContainer.getBoundingClientRect(); svgBoundTop = (Math.abs(svgBoundTop) > containerBound.height) ? 0 : svgBoundTop; } svgOffset.top = Math.abs(svgBoundTop - chartBound.top); svgOffset.left = Math.abs(svgBound.left - chartBound.left); } //If the parent container is an overflow
with scrollbars, subtract the scroll offsets. //You need to also add any offset between the element and its containing
//Finally, add any offset of the containing
on the whole page. left += chartContainer.offsetLeft + svgOffset.left - 2*chartContainer.scrollLeft; top += chartContainer.offsetTop + svgOffset.top - 2*chartContainer.scrollTop; } if (snapDistance && snapDistance > 0) { top = Math.floor(top/snapDistance) * snapDistance; } nv.tooltip.calcTooltipPosition([left,top], gravity, distance, container); return nvtooltip; }; nvtooltip.nvPointerEventsClass = nvPointerEventsClass; nvtooltip.content = function(_) { if (!arguments.length) return content; content = _; return nvtooltip; }; //Returns tooltipElem...not able to set it. nvtooltip.tooltipElem = function() { return tooltipElem; }; nvtooltip.contentGenerator = function(_) { if (!arguments.length) return contentGenerator; if (typeof _ === 'function') { contentGenerator = _; } return nvtooltip; }; nvtooltip.data = function(_) { if (!arguments.length) return data; data = _; return nvtooltip; }; nvtooltip.gravity = function(_) { if (!arguments.length) return gravity; gravity = _; return nvtooltip; }; nvtooltip.distance = function(_) { if (!arguments.length) return distance; distance = _; return nvtooltip; }; nvtooltip.snapDistance = function(_) { if (!arguments.length) return snapDistance; snapDistance = _; return nvtooltip; }; nvtooltip.classes = function(_) { if (!arguments.length) return classes; classes = _; return nvtooltip; }; nvtooltip.chartContainer = function(_) { if (!arguments.length) return chartContainer; chartContainer = _; return nvtooltip; }; nvtooltip.position = function(_) { if (!arguments.length) return position; position.left = (typeof _.left !== 'undefined') ? _.left : position.left; position.top = (typeof _.top !== 'undefined') ? _.top : position.top; return nvtooltip; }; nvtooltip.fixedTop = function(_) { if (!arguments.length) return fixedTop; fixedTop = _; return nvtooltip; }; nvtooltip.enabled = function(_) { if (!arguments.length) return enabled; enabled = _; return nvtooltip; }; nvtooltip.valueFormatter = function(_) { if (!arguments.length) return valueFormatter; if (typeof _ === 'function') { valueFormatter = _; } return nvtooltip; }; nvtooltip.headerFormatter = function(_) { if (!arguments.length) return headerFormatter; if (typeof _ === 'function') { headerFormatter = _; } return nvtooltip; }; //id() is a read-only function. You can't use it to set the id. nvtooltip.id = function() { return id; }; return nvtooltip; }; //Original tooltip.show function. Kept for backward compatibility. // pos = [left,top] nv.tooltip.show = function(pos, content, gravity, dist, parentContainer, classes) { //Create new tooltip div if it doesn't exist on DOM. var container = document.createElement('div'); container.className = 'nvtooltip ' + (classes ? classes : 'xy-tooltip'); var body = parentContainer; if ( !parentContainer || parentContainer.tagName.match(/g|svg/i)) { //If the parent element is an SVG element, place tooltip in the element. body = document.getElementsByTagName('body')[0]; } container.style.left = 0; container.style.top = 0; container.style.opacity = 0; container.innerHTML = content; body.appendChild(container); //If the parent container is an overflow
with scrollbars, subtract the scroll offsets. if (parentContainer) { pos[0] = pos[0] - parentContainer.scrollLeft; pos[1] = pos[1] - parentContainer.scrollTop; } nv.tooltip.calcTooltipPosition(pos, gravity, dist, container); }; //Looks up the ancestry of a DOM element, and returns the first NON-svg node. nv.tooltip.findFirstNonSVGParent = function(Elem) { while(Elem.tagName.match(/^g|svg$/i) !== null) { Elem = Elem.parentNode; } return Elem; }; //Finds the total offsetTop of a given DOM element. //Looks up the entire ancestry of an element, up to the first relatively positioned element. nv.tooltip.findTotalOffsetTop = function ( Elem, initialTop ) { var offsetTop = initialTop; do { if( !isNaN( Elem.offsetTop ) ) { offsetTop += (Elem.offsetTop); } } while( Elem = Elem.offsetParent ); return offsetTop; }; //Finds the total offsetLeft of a given DOM element. //Looks up the entire ancestry of an element, up to the first relatively positioned element. nv.tooltip.findTotalOffsetLeft = function ( Elem, initialLeft) { var offsetLeft = initialLeft; do { if( !isNaN( Elem.offsetLeft ) ) { offsetLeft += (Elem.offsetLeft); } } while( Elem = Elem.offsetParent ); return offsetLeft; }; //Global utility function to render a tooltip on the DOM. //pos = [left,top] coordinates of where to place the tooltip, relative to the SVG chart container. //gravity = how to orient the tooltip //dist = how far away from the mouse to place tooltip //container = tooltip DIV nv.tooltip.calcTooltipPosition = function(pos, gravity, dist, container) { var height = parseInt(container.offsetHeight), width = parseInt(container.offsetWidth), windowWidth = nv.utils.windowSize().width, windowHeight = nv.utils.windowSize().height, scrollTop = window.pageYOffset, scrollLeft = window.pageXOffset, left, top; windowHeight = window.innerWidth >= document.body.scrollWidth ? windowHeight : windowHeight - 16; windowWidth = window.innerHeight >= document.body.scrollHeight ? windowWidth : windowWidth - 16; gravity = gravity || 's'; dist = dist || 20; var tooltipTop = function ( Elem ) { return nv.tooltip.findTotalOffsetTop(Elem, top); }; var tooltipLeft = function ( Elem ) { return nv.tooltip.findTotalOffsetLeft(Elem,left); }; switch (gravity) { case 'e': left = pos[0] - width - dist; top = pos[1] - (height / 2); var tLeft = tooltipLeft(container); var tTop = tooltipTop(container); if (tLeft < scrollLeft) left = pos[0] + dist > scrollLeft ? pos[0] + dist : scrollLeft - tLeft + left; if (tTop < scrollTop) top = scrollTop - tTop + top; if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height; break; case 'w': left = pos[0] + dist; top = pos[1] - (height / 2); var tLeft = tooltipLeft(container); var tTop = tooltipTop(container); if (tLeft + width > windowWidth) left = pos[0] - width - dist; if (tTop < scrollTop) top = scrollTop + 5; if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height; break; case 'n': left = pos[0] - (width / 2) - 5; top = pos[1] + dist; var tLeft = tooltipLeft(container); var tTop = tooltipTop(container); if (tLeft < scrollLeft) left = scrollLeft + 5; if (tLeft + width > windowWidth) left = left - width/2 + 5; if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height; break; case 's': left = pos[0] - (width / 2); top = pos[1] - height - dist; var tLeft = tooltipLeft(container); var tTop = tooltipTop(container); if (tLeft < scrollLeft) left = scrollLeft + 5; if (tLeft + width > windowWidth) left = left - width/2 + 5; if (scrollTop > tTop) top = scrollTop; break; case 'none': left = pos[0]; top = pos[1] - dist; var tLeft = tooltipLeft(container); var tTop = tooltipTop(container); break; } container.style.left = left+'px'; container.style.top = top+'px'; container.style.opacity = 1; container.style.position = 'absolute'; return container; }; //Global utility function to remove tooltips from the DOM. nv.tooltip.cleanup = function() { // Find the tooltips, mark them for removal by this class (so others cleanups won't find it) var tooltips = document.getElementsByClassName('nvtooltip'); var purging = []; while(tooltips.length) { purging.push(tooltips[0]); tooltips[0].style.transitionDelay = '0 !important'; tooltips[0].style.opacity = 0; tooltips[0].className = 'nvtooltip-pending-removal'; } setTimeout(function() { while (purging.length) { var removeMe = purging.pop(); removeMe.parentNode.removeChild(removeMe); } }, 500); }; })();