Just a little proof-of-concept here - using an SVG <foreignObject>
element as a container for a tooltip that can involve handy HTML features like text-wrapping and (semi-)dynamic sizing.
Gotchas so far:
Like an <svg>
element, a <foreignObject>
element needs a width and a height in order to be rendered.
However, specifying width
and/or height
can be delayed. Here I specify a width (foWidth
) of 300px, then find the height of the contained <div>
using getBoundingClientRect()
and use that to specify the height of the containing <foreignObject>
.
If you want to provide a complex shape for the tooltip, this can be done e.g. with an SVG <polygon>
but there's a slight paradox in needing to find the dynamic dimension of the <foreignObject>
before creating the <polygon>
, but the <polygon>
needs to appear higher than the <foreignObject>
in the SVG in order to be layered underneath it. The solution here is d3.insert(el, selector)
.
Chrome doesn't create the SVG foreignObject
elements properly - they appear in the DOM as foreignobject
and can't then be selected by d3.selectAll('foreignObject')
(nor d3.selectAll('foreignobject')
because D3 knows about the proper names of things in the SVG XML namespace). Solution: use a class to identify all <foreignObject>
s.
xxxxxxxxxx
<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: 10px;
color: #4A22FF;
}
.lead {
font-style: italic;
}
p {
margin: 5px 0px;
}
polygon {
pointer-events: none;
}
</style>
</head>
<body>
<script type='text/javascript'>
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, k = 15;
var tip = {'w': (3/4 * t), 'h': k};
svg.append('circle')
.attr({
'r': 50,
'cx': anchor.w,
'cy': anchor.h,
'fill': '#7413E8',
'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;
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>
Modified http://d3js.org/d3.v3.min.js to a secure url
https://d3js.org/d3.v3.min.js