Skip to content

Instantly share code, notes, and snippets.

@jebeck
Last active May 30, 2023 05:03
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jebeck/10699411 to your computer and use it in GitHub Desktop.
Save jebeck/10699411 to your computer and use it in GitHub Desktop.
SVG foreignObject tooltips in D3

SVG foreignObject tooltips in D3

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.

<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>
SVG foreignObject tooltips in D3
</title>
<script src='http://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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment