Based on this previous gist, but robust to changes in browser zoom level (text inside tooltip doesn’t get cut off when zoom < 100%).
forked from jebeck‘s block: Browser-zoom robust foreignObject tooltips
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>
SVG foreignObject tooltips in D3
</title>
<script src='https://d3js.org/d3.v3.min.js' charset='utf-8'></script>
<style type='text/css'>
svg {
display: block;
margin: 0 auto;
}
.svg-tooltip {
pointer-events: none;
}
.tooltip {
padding: 0.8333333333em;
color: #4A22FF;
}
.lead {
font-style: italic;
}
p {
margin: 0.4166666667em 0em;
font-size: 1em;
}
polygon {
pointer-events: none;
}
</style>
</head>
<body>
<script type="text/javascript" src='detect-zoom.min.js'></script>
<script type='text/javascript'>
var zoomPercentage = window.detectZoom.device();
d3.select(window).on('resize', function() {
zoomPercentage = window.detectZoom.device();
});
var margin = {top: 20, right: 10, bottom: 20, left: 10};
var width = 800 - margin.left - margin.right;
var height = 480 - margin.top - margin.bottom;
var svg = d3.select('body')
.append('svg')
.attr({
'width': width + margin.left + margin.right,
'height': height + margin.top + margin.bottom
})
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
svg.append('rect')
.attr({
'width': width * 0.8,
'height': height * 0.8,
'x': width * 0.1,
'y': height * 0.1,
'fill': '#F8F8F8'
});
var foWidth = 300;
var anchor = {'w': width/3, 'h': height/3};
var t = 50;
var k = 15;
var tip = {'w': (3/4 * t), 'h': k};
svg.append('circle')
.attr({
'r': 50,
'cx': anchor.w,
'cy': anchor.h,
'fill': '#12e72b',
'opacity': 0.35
})
.on('mouseover', function() {
var fo = svg.append('foreignObject')
.attr({
'x': anchor.w - tip.w,
'y': anchor.h + tip.h,
'width': foWidth,
'class': 'svg-tooltip'
});
var div = fo.append('xhtml:div')
.append('div')
.attr({
'class': 'tooltip'
});
div.append('p')
.attr('class', 'lead')
.html('Holmes was certainly not a difficult man to live with.');
div.append('p')
.html('He was quiet in his ways, and his habits were regular. It was rare for him to be up after ten at night, and he had invariably breakfasted and gone out before I rose in the morning.');
var foHeight = div[0][0].getBoundingClientRect().height;
console.log('Orig foHeight', foHeight);
console.log('zoomPercentage', zoomPercentage);
foHeight = foHeight/zoomPercentage;
console.log('New foHeight', foHeight);
fo.attr({
'height': foHeight
});
svg.insert('polygon', '.svg-tooltip')
.attr({
'points': "0,0 0," + foHeight + " " +
foWidth + "," + foHeight + " " +
foWidth + ",0 " + (t) + ",0 " +
tip.w + "," + (-tip.h) + " " +
(t/2) + ",0",
'height': foHeight + tip.h,
'width': foWidth,
'fill': '#D8D8D8',
'opacity': 0.75,
'transform': 'translate(' + (anchor.w - tip.w) + ',' + (anchor.h + tip.h) + ')'
});
})
.on('mouseout', function() {
svg.selectAll('.svg-tooltip').remove();
svg.selectAll('polygon').remove();
});
</script>
</body>
</html>
(function(root,ns,factory){"use strict";"undefined"!=typeof module&&module.exports?module.exports=factory(ns,root):"function"==typeof define&&define.amd?define("detect-zoom",function(){return factory(ns,root)}):root[ns]=factory(ns,root)})(window,"detectZoom",function(){var devicePixelRatio=function(){return window.devicePixelRatio||1},fallback=function(){return{zoom:1,devicePxPerCssPx:1}},ie8=function(){var zoom=Math.round(100*(screen.deviceXDPI/screen.logicalXDPI))/100;return{zoom:zoom,devicePxPerCssPx:zoom*devicePixelRatio()}},ie10=function(){var zoom=Math.round(100*(document.documentElement.offsetHeight/window.innerHeight))/100;return{zoom:zoom,devicePxPerCssPx:zoom*devicePixelRatio()}},webkitMobile=function(){var deviceWidth=90==Math.abs(window.orientation)?screen.height:screen.width,zoom=deviceWidth/window.innerWidth;return{zoom:zoom,devicePxPerCssPx:zoom*devicePixelRatio()}},webkit=function(){var important=function(str){return str.replace(/;/g," !important;")},div=document.createElement("div");div.innerHTML="1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>0",div.setAttribute("style",important("font: 100px/1em sans-serif; -webkit-text-size-adjust: none; text-size-adjust: none; height: auto; width: 1em; padding: 0; overflow: visible;"));var container=document.createElement("div");container.setAttribute("style",important("width:0; height:0; overflow:hidden; visibility:hidden; position: absolute;")),container.appendChild(div),document.body.appendChild(container);var zoom=1e3/div.clientHeight;return zoom=Math.round(100*zoom)/100,document.body.removeChild(container),{zoom:zoom,devicePxPerCssPx:zoom*devicePixelRatio()}},firefox4=function(){var zoom=mediaQueryBinarySearch("min--moz-device-pixel-ratio","",0,10,20,1e-4);return zoom=Math.round(100*zoom)/100,{zoom:zoom,devicePxPerCssPx:zoom}},firefox18=function(){return{zoom:firefox4().zoom,devicePxPerCssPx:devicePixelRatio()}},opera11=function(){var zoom=window.top.outerWidth/window.top.innerWidth;return zoom=Math.round(100*zoom)/100,{zoom:zoom,devicePxPerCssPx:zoom*devicePixelRatio()}},mediaQueryBinarySearch=function(property,unit,a,b,maxIter,epsilon){function binarySearch(a,b,maxIter){var mid=(a+b)/2;if(0>=maxIter||epsilon>b-a)return mid;var query="("+property+":"+mid+unit+")";return matchMedia(query).matches?binarySearch(mid,b,maxIter-1):binarySearch(a,mid,maxIter-1)}var matchMedia,head,style,div;window.matchMedia?matchMedia=window.matchMedia:(head=document.getElementsByTagName("head")[0],style=document.createElement("style"),head.appendChild(style),div=document.createElement("div"),div.className="mediaQueryBinarySearch",div.style.display="none",document.body.appendChild(div),matchMedia=function(query){style.sheet.insertRule("@media "+query+"{.mediaQueryBinarySearch "+"{text-decoration: underline} }",0);var matched="underline"==getComputedStyle(div,null).textDecoration;return style.sheet.deleteRule(0),{matches:matched}});var ratio=binarySearch(a,b,maxIter);return div&&(head.removeChild(style),document.body.removeChild(div)),ratio},detectFunction=function(){var func=fallback;return isNaN(screen.logicalXDPI)||isNaN(screen.systemXDPI)?window.navigator.msMaxTouchPoints?func=ie10:"orientation"in window&&"string"==typeof document.body.style.webkitMarquee?func=webkitMobile:"string"==typeof document.body.style.webkitMarquee?func=webkit:navigator.userAgent.indexOf("Opera")>=0?func=opera11:window.devicePixelRatio?func=firefox18:firefox4().zoom>.001&&(func=firefox4):func=ie8,func}();return{zoom:function(){return detectFunction().zoom},device:function(){return detectFunction().devicePxPerCssPx}}});