1 /* Tooltip rendering model for nvd3 charts.
2 window.nv.models.tooltip is the updated,new way to render tooltips.
4 window.nv.tooltip.show is the old tooltip code.
5 window.nv.tooltip.* also has various helper methods.
9 window.nv.tooltip = {};
11 /* Model which can be instantiated to handle tooltip rendering.
13 var tip = nv.models.tooltip().gravity('w').distance(23)
16 tip(); //just invoke the returned function to render tooltip.
18 window.nv.models.tooltip = function() {
19 var content = null //HTML contents of the tooltip. If null, the content is generated via the data variable.
20 , data = null /* Tooltip data. If data is given in the proper format, a consistent tooltip is generated.
41 , gravity = 'w' //Can be 'n','s','e','w'. Determines how tooltip is positioned.
42 , distance = 50 //Distance to offset tooltip from the mouse location.
43 , snapDistance = 25 //Tolerance allowed before tooltip is moved from its current position (creates 'snapping' effect)
44 , fixedTop = null //If not null, this fixes the top position of the tooltip.
45 , classes = null //Attaches additional CSS classes to the tooltip DIV that is created.
46 , chartContainer = null //Parent DIV, of the SVG Container that holds the chart.
47 , tooltipElem = null //actual DOM element representing the tooltip.
48 , position = {left: null, top: null} //Relative position of the tooltip inside chartContainer.
49 , enabled = true //True -> tooltips are rendered. False -> don't render tooltips.
50 //Generates a unique id when you create a new tooltip() object
51 , id = "nvtooltip-" + Math.floor(Math.random() * 100000)
54 //CSS class to specify whether element should not have mouse events.
55 var nvPointerEventsClass = "nv-pointer-events-none";
57 //Format function for the tooltip values column
58 var valueFormatter = function(d,i) {
62 //Format function for the tooltip header value.
63 var headerFormatter = function(d) {
67 //By default, the tooltip model renders a beautiful table inside a DIV.
68 //You can override this function if a custom tooltip is desired.
69 var contentGenerator = function(d) {
70 if (content != null) return content;
72 if (d == null) return '';
74 var table = d3.select(document.createElement("table"));
75 var theadEnter = table.selectAll("thead")
77 .enter().append("thead");
78 theadEnter.append("tr")
82 .classed("x-value",true)
83 .html(headerFormatter(d.value));
85 var tbodyEnter = table.selectAll("tbody")
87 .enter().append("tbody");
88 var trowEnter = tbodyEnter.selectAll("tr")
89 .data(function(p) { return p.series})
92 .classed("highlight", function(p) { return p.highlight})
95 trowEnter.append("td")
96 .classed("legend-color-guide",true)
98 .style("background-color", function(p) { return p.color});
99 trowEnter.append("td")
101 .html(function(p) {return p.key});
102 trowEnter.append("td")
103 .classed("value",true)
104 .html(function(p,i) { return valueFormatter(p.value,i) });
107 trowEnter.selectAll("td").each(function(p) {
109 var opacityScale = d3.scale.linear().domain([0,1]).range(["#fff",p.color]);
112 .style("border-bottom-color", opacityScale(opacity))
113 .style("border-top-color", opacityScale(opacity))
118 var html = table.node().outerHTML;
119 if (d.footer !== undefined)
120 html += "<div class='footer'>" + d.footer + "</div>";
125 var dataSeriesExists = function(d) {
126 if (d && d.series && d.series.length > 0) return true;
131 //In situations where the chart is in a 'viewBox', re-position the tooltip based on how far chart is zoomed.
132 function convertViewBoxRatio() {
133 if (chartContainer) {
134 var svg = d3.select(chartContainer);
135 if (svg.node().tagName !== "svg") {
136 svg = svg.select("svg");
138 var viewBox = (svg.node()) ? svg.attr('viewBox') : null;
140 viewBox = viewBox.split(' ');
141 var ratio = parseInt(svg.style('width')) / viewBox[2];
143 position.left = position.left * ratio;
144 position.top = position.top * ratio;
149 //Creates new tooltip container, or uses existing one on DOM.
150 function getTooltipContainer(newContent) {
153 body = d3.select(chartContainer);
155 body = d3.select("body");
157 var container = body.select(".nvtooltip");
158 if (container.node() === null) {
159 //Create new tooltip div if it doesn't exist on DOM.
160 container = body.append("div")
161 .attr("class", "nvtooltip " + (classes? classes: "xy-tooltip"))
167 container.node().innerHTML = newContent;
168 container.style("top",0).style("left",0).style("opacity",0);
169 container.selectAll("div, table, td, tr").classed(nvPointerEventsClass,true)
170 container.classed(nvPointerEventsClass,true);
171 return container.node();
176 //Draw the tooltip onto the DOM.
177 function nvtooltip() {
178 if (!enabled) return;
179 if (!dataSeriesExists(data)) return;
181 convertViewBoxRatio();
183 var left = position.left;
184 var top = (fixedTop != null) ? fixedTop : position.top;
185 var container = getTooltipContainer(contentGenerator(data));
186 tooltipElem = container;
187 if (chartContainer) {
188 var svgComp = chartContainer.getElementsByTagName("svg")[0];
189 var boundRect = (svgComp) ? svgComp.getBoundingClientRect() : chartContainer.getBoundingClientRect();
190 var svgOffset = {left:0,top:0};
192 var svgBound = svgComp.getBoundingClientRect();
193 var chartBound = chartContainer.getBoundingClientRect();
194 var svgBoundTop = svgBound.top;
196 //Defensive code. Sometimes, svgBoundTop can be a really negative
197 // number, like -134254. That's a bug.
198 // If such a number is found, use zero instead. FireFox bug only
199 if (svgBoundTop < 0) {
200 var containerBound = chartContainer.getBoundingClientRect();
201 svgBoundTop = (Math.abs(svgBoundTop) > containerBound.height) ? 0 : svgBoundTop;
203 svgOffset.top = Math.abs(svgBoundTop - chartBound.top);
204 svgOffset.left = Math.abs(svgBound.left - chartBound.left);
206 //If the parent container is an overflow <div> with scrollbars, subtract the scroll offsets.
207 //You need to also add any offset between the <svg> element and its containing <div>
208 //Finally, add any offset of the containing <div> on the whole page.
209 left += chartContainer.offsetLeft + svgOffset.left - 2*chartContainer.scrollLeft;
210 top += chartContainer.offsetTop + svgOffset.top - 2*chartContainer.scrollTop;
213 if (snapDistance && snapDistance > 0) {
214 top = Math.floor(top/snapDistance) * snapDistance;
217 nv.tooltip.calcTooltipPosition([left,top], gravity, distance, container);
221 nvtooltip.nvPointerEventsClass = nvPointerEventsClass;
223 nvtooltip.content = function(_) {
224 if (!arguments.length) return content;
229 //Returns tooltipElem...not able to set it.
230 nvtooltip.tooltipElem = function() {
234 nvtooltip.contentGenerator = function(_) {
235 if (!arguments.length) return contentGenerator;
236 if (typeof _ === 'function') {
237 contentGenerator = _;
242 nvtooltip.data = function(_) {
243 if (!arguments.length) return data;
248 nvtooltip.gravity = function(_) {
249 if (!arguments.length) return gravity;
254 nvtooltip.distance = function(_) {
255 if (!arguments.length) return distance;
260 nvtooltip.snapDistance = function(_) {
261 if (!arguments.length) return snapDistance;
266 nvtooltip.classes = function(_) {
267 if (!arguments.length) return classes;
272 nvtooltip.chartContainer = function(_) {
273 if (!arguments.length) return chartContainer;
278 nvtooltip.position = function(_) {
279 if (!arguments.length) return position;
280 position.left = (typeof _.left !== 'undefined') ? _.left : position.left;
281 position.top = (typeof _.top !== 'undefined') ? _.top : position.top;
285 nvtooltip.fixedTop = function(_) {
286 if (!arguments.length) return fixedTop;
291 nvtooltip.enabled = function(_) {
292 if (!arguments.length) return enabled;
297 nvtooltip.valueFormatter = function(_) {
298 if (!arguments.length) return valueFormatter;
299 if (typeof _ === 'function') {
305 nvtooltip.headerFormatter = function(_) {
306 if (!arguments.length) return headerFormatter;
307 if (typeof _ === 'function') {
313 //id() is a read-only function. You can't use it to set the id.
314 nvtooltip.id = function() {
323 //Original tooltip.show function. Kept for backward compatibility.
325 nv.tooltip.show = function(pos, content, gravity, dist, parentContainer, classes) {
327 //Create new tooltip div if it doesn't exist on DOM.
328 var container = document.createElement('div');
329 container.className = 'nvtooltip ' + (classes ? classes : 'xy-tooltip');
331 var body = parentContainer;
332 if ( !parentContainer || parentContainer.tagName.match(/g|svg/i)) {
333 //If the parent element is an SVG element, place tooltip in the <body> element.
334 body = document.getElementsByTagName('body')[0];
337 container.style.left = 0;
338 container.style.top = 0;
339 container.style.opacity = 0;
340 container.innerHTML = content;
341 body.appendChild(container);
343 //If the parent container is an overflow <div> with scrollbars, subtract the scroll offsets.
344 if (parentContainer) {
345 pos[0] = pos[0] - parentContainer.scrollLeft;
346 pos[1] = pos[1] - parentContainer.scrollTop;
348 nv.tooltip.calcTooltipPosition(pos, gravity, dist, container);
351 //Looks up the ancestry of a DOM element, and returns the first NON-svg node.
352 nv.tooltip.findFirstNonSVGParent = function(Elem) {
353 while(Elem.tagName.match(/^g|svg$/i) !== null) {
354 Elem = Elem.parentNode;
359 //Finds the total offsetTop of a given DOM element.
360 //Looks up the entire ancestry of an element, up to the first relatively positioned element.
361 nv.tooltip.findTotalOffsetTop = function ( Elem, initialTop ) {
362 var offsetTop = initialTop;
365 if( !isNaN( Elem.offsetTop ) ) {
366 offsetTop += (Elem.offsetTop);
368 } while( Elem = Elem.offsetParent );
372 //Finds the total offsetLeft of a given DOM element.
373 //Looks up the entire ancestry of an element, up to the first relatively positioned element.
374 nv.tooltip.findTotalOffsetLeft = function ( Elem, initialLeft) {
375 var offsetLeft = initialLeft;
378 if( !isNaN( Elem.offsetLeft ) ) {
379 offsetLeft += (Elem.offsetLeft);
381 } while( Elem = Elem.offsetParent );
385 //Global utility function to render a tooltip on the DOM.
386 //pos = [left,top] coordinates of where to place the tooltip, relative to the SVG chart container.
387 //gravity = how to orient the tooltip
388 //dist = how far away from the mouse to place tooltip
389 //container = tooltip DIV
390 nv.tooltip.calcTooltipPosition = function(pos, gravity, dist, container) {
392 var height = parseInt(container.offsetHeight),
393 width = parseInt(container.offsetWidth),
394 windowWidth = nv.utils.windowSize().width,
395 windowHeight = nv.utils.windowSize().height,
396 scrollTop = window.pageYOffset,
397 scrollLeft = window.pageXOffset,
400 windowHeight = window.innerWidth >= document.body.scrollWidth ? windowHeight : windowHeight - 16;
401 windowWidth = window.innerHeight >= document.body.scrollHeight ? windowWidth : windowWidth - 16;
403 gravity = gravity || 's';
406 var tooltipTop = function ( Elem ) {
407 return nv.tooltip.findTotalOffsetTop(Elem, top);
410 var tooltipLeft = function ( Elem ) {
411 return nv.tooltip.findTotalOffsetLeft(Elem,left);
416 left = pos[0] - width - dist;
417 top = pos[1] - (height / 2);
418 var tLeft = tooltipLeft(container);
419 var tTop = tooltipTop(container);
420 if (tLeft < scrollLeft) left = pos[0] + dist > scrollLeft ? pos[0] + dist : scrollLeft - tLeft + left;
421 if (tTop < scrollTop) top = scrollTop - tTop + top;
422 if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height;
425 left = pos[0] + dist;
426 top = pos[1] - (height / 2);
427 var tLeft = tooltipLeft(container);
428 var tTop = tooltipTop(container);
429 if (tLeft + width > windowWidth) left = pos[0] - width - dist;
430 if (tTop < scrollTop) top = scrollTop + 5;
431 if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height;
434 left = pos[0] - (width / 2) - 5;
436 var tLeft = tooltipLeft(container);
437 var tTop = tooltipTop(container);
438 if (tLeft < scrollLeft) left = scrollLeft + 5;
439 if (tLeft + width > windowWidth) left = left - width/2 + 5;
440 if (tTop + height > scrollTop + windowHeight) top = scrollTop + windowHeight - tTop + top - height;
443 left = pos[0] - (width / 2);
444 top = pos[1] - height - dist;
445 var tLeft = tooltipLeft(container);
446 var tTop = tooltipTop(container);
447 if (tLeft < scrollLeft) left = scrollLeft + 5;
448 if (tLeft + width > windowWidth) left = left - width/2 + 5;
449 if (scrollTop > tTop) top = scrollTop;
454 var tLeft = tooltipLeft(container);
455 var tTop = tooltipTop(container);
460 container.style.left = left+'px';
461 container.style.top = top+'px';
462 container.style.opacity = 1;
463 container.style.position = 'absolute';
468 //Global utility function to remove tooltips from the DOM.
469 nv.tooltip.cleanup = function() {
471 // Find the tooltips, mark them for removal by this class (so others cleanups won't find it)
472 var tooltips = document.getElementsByClassName('nvtooltip');
474 while(tooltips.length) {
475 purging.push(tooltips[0]);
476 tooltips[0].style.transitionDelay = '0 !important';
477 tooltips[0].style.opacity = 0;
478 tooltips[0].className = 'nvtooltip-pending-removal';
481 setTimeout(function() {
483 while (purging.length) {
484 var removeMe = purging.pop();
485 removeMe.parentNode.removeChild(removeMe);