| /* 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 += "<div class='footer'>" + d.footer + "</div>"; |
| 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 <div> with scrollbars, subtract the scroll offsets. |
| //You need to also add any offset between the <svg> element and its containing <div> |
| //Finally, add any offset of the containing <div> 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 <body> element. |
| body = document.getElementsByTagName('body')[0]; |
| } |
| |
| container.style.left = 0; |
| container.style.top = 0; |
| container.style.opacity = 0; |
| var safeContent = DOMPurify.sanitize(content, {RETURN_TRUSTED_TYPE: true}); |
| container.innerHTML = safeContent; |
| body.appendChild(container); |
| |
| //If the parent container is an overflow <div> 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); |
| }; |
| |
| })(); |