Skip to content

Instantly share code, notes, and snippets.

@GerHobbelt
Forked from zmaril/index.html
Created July 8, 2012 12:02
Show Gist options
  • Save GerHobbelt/3070659 to your computer and use it in GitHub Desktop.
Save GerHobbelt/3070659 to your computer and use it in GitHub Desktop.
d3 bootstrap popovers (fix)
# Editor backup files
*.bak
*~

Uses the markup and css from bootstrap to make pretty popovers. Find the files here

This gist is derived from Zack Maril's gist 3012212 and corrects the issues of that gist:

  • race condition fix: when you quickly mouseout and mouseenter a tooltipped node again, the remove timer is cleared so the tip remains visible like it should. (This means you could end up inside a circle node without getting a tooltip, only maybe a quick flash of one.)

  • when it's the tooltip which causes the mouseout event to fire, which would happen for the the given tooltip positioning when you hover over the approximately right half of a circle, then this won't cause the tooltip to be removed and consequently flicker in & out of existence any more, as the removal is deferred until you leave the tooltip area. Move over tooltip and compare with original behaviour to see the difference: now the tooltip sticks around until you move away.

As the tooltip has padding, the latter already happened when visually you're not yet 'over' the tooltip (but still over the 5px padding area there).

Testing

When you want to see / test the above going wrong, you can either see the original gist here or go to this gist which is a clone of Zack's but has a red border around the tooltip div, so you can see the padding at work, together with the mouseout/mouseover events causing the tooltip flickering and 'odd' moments of disappearance.

Notes

From the specific to the generic; things to take home for your own projects?

  • watch the console log to see the code at work. (Keep in mind that the code will b0rk on any IE browser as long as you haven't hit [F12]; IE doesn't have a console object until you've opened the [F12] debugger once.)

  • each node has its own 'tip' instance in the .each(...) closure; this matches Zack's, except his removal code would do a d3.selectAll() on the tooltip class, thus removing all tips all at once, including your latest tip which possibly maybe should have stayed 'alive' while another node's should be removed. This code keeps a reference to this node's tip using the each(...) closure. (I.e.: the var has moved.)

  • the if (tip ...) checking everywhere may seem pedantic in some places but better be safe then sorry; remove the checks and then go nuts with your mouse like a squirrel on crack: errors won't exactly be flying, but you'll encounter some wicked failures, thanks to not checking your async+closures code.

<!doctype html>
<head>
<style>
body {
font: 10px sans-serif;
}
#main {
left: 25%;
position: absolute;
}
#main #text {
padding-bottom: 10px;
}
</style>
<link rel="stylesheet" href="tooltip.css">
<script src="http://d3js.org/d3.v2.min.js?2.8.1"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="js/vendor/jquery-1.7.2.min.js"><\/script>')</script>
<script src="tooltip.js"></script>
<script src="index.js"></script>
</head>
<body>
<div id="graphic"></div>
</body>
</html>
var graphic;
graphic = new Object;
graphic.create = function() {
var g, height, i, j, points, size, spacing, width, _i, _j, _len, _len1, _ref, _ref1;
width = $(document).width() / 2;
height = $(document).height() * .85;
size = d3.min([width, height]);
graphic.svg = d3.select("#graphic").append("svg").attr("width", size).attr("height", size);
g = graphic.svg.append("g");
points = [];
spacing = 30;
_ref = d3.range(0, height - spacing, spacing);
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
i = _ref[_i];
_ref1 = d3.range(0, width - spacing, spacing);
for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) {
j = _ref1[_j];
points.push({
x: i,
y: j
});
}
}
return g.selectAll("circle").data(points).enter().append("circle").attr("cx", function(d, i) {
return d.x;
}).attr("cy", function(d, i) {
return d.y;
}).attr("r", function(d, i) {
return Math.round(Math.random() * spacing / 2 + 1);
}).tooltip(function(d, i) {
var r, svg;
r = +d3.select(this).attr('r');
svg = d3.select(document.createElement("svg")).attr("height", 50);
g = svg.append("g");
g.append("rect").attr("width", r * 10).attr("height", 10);
g.append("text").text("10 times the radius of the cirlce").attr("dy", "25");
return {
type: "popover",
title: "It's a me, Rectangle",
content: svg,
detection: "shape",
placement: "fixed",
gravity: "right",
position: [d.x, d.y],
displacement: [r + 2, -72],
mousemove: false
};
});
};
$(document).ready(graphic.create);
/* Taken from bootstrap: https://github.com/twitter/bootstrap/blob/master/less/tooltip.less */
.fade {
opacity: 0;
-webkit-transition: opacity 0.15s linear;
-moz-transition: opacity 0.15s linear;
-ms-transition: opacity 0.15s linear;
-o-transition: opacity 0.15s linear;
transition: opacity 0.15s linear;
}
.fade.in {
opacity: 1;
}
.tooltip {
position: absolute;
z-index: 1020;
display: block;
padding: 5px;
font-size: 11px;
opacity: 0.5;
filter: alpha(opacity=0.5);
visibility: visible;
}
.tooltip.in {
opacity: 0.8;
filter: alpha(opacity=80);
}
.tooltip.top {
margin-top: -2px;
}
.tooltip.right {
margin-left: 2px;
}
.tooltip.bottom {
margin-top: 2px;
}
.tooltip.left {
margin-left: -2px;
}
.tooltip.top .arrow {
bottom: 0;
left: 50%;
margin-left: -5px;
border-top: 5px solid #000000;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
}
.tooltip.left .arrow {
top: 50%;
right: 0;
margin-top: -5px;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 5px solid #000000;
}
.tooltip.bottom .arrow {
top: 0;
left: 50%;
margin-left: -5px;
border-right: 5px solid transparent;
border-bottom: 5px solid #000000;
border-left: 5px solid transparent;
}
.tooltip.right .arrow {
top: 50%;
left: 0;
margin-top: -5px;
border-top: 5px solid transparent;
border-right: 5px solid #000000;
border-bottom: 5px solid transparent;
}
.tooltip-inner {
max-width: 200px;
padding: 3px 8px;
color: #ffffff;
text-align: center;
text-decoration: none;
background-color: #000000;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.arrow {
position: absolute;
width: 0;
height: 0;
}
.popover {
position: absolute;
top: 0;
left: 0;
z-index: 1010;
display: none;
padding: 5px;
}
.popover.top {
margin-top: -5px;
}
.popover.right {
margin-left: 5px;
}
.popover.bottom {
margin-top: 5px;
}
.popover.left {
margin-left: -5px;
}
.popover.top .arrow {
bottom: 0;
left: 50%;
margin-left: -5px;
border-top: 5px solid #000000;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
}
.popover.right .arrow {
top: 50%;
left: 0;
margin-top: -5px;
border-top: 5px solid transparent;
border-right: 5px solid #000000;
border-bottom: 5px solid transparent;
}
.popover.bottom .arrow {
top: 0;
left: 50%;
margin-left: -5px;
border-right: 5px solid transparent;
border-bottom: 5px solid #000000;
border-left: 5px solid transparent;
}
.popover.left .arrow {
top: 50%;
right: 0;
margin-top: -5px;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 5px solid #000000;
}
.popover-inner {
width: 280px;
padding: 3px;
overflow: hidden;
background: #000000;
background: rgba(0, 0, 0, 0.8);
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
-webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
}
.popover-title {
padding: 9px 15px;
line-height: 1;
background-color: #f5f5f5;
border-bottom: 1px solid #eee;
-webkit-border-radius: 3px 3px 0 0;
-moz-border-radius: 3px 3px 0 0;
border-radius: 3px 3px 0 0;
margin: 0;
}
.popover-content {
padding: 14px;
background-color: #ffffff;
-webkit-border-radius: 0 0 3px 3px;
-moz-border-radius: 0 0 3px 3px;
border-radius: 0 0 3px 3px;
-webkit-background-clip: padding-box;
-moz-background-clip: padding-box;
background-clip: padding-box;
}
.popover-content p,
.popover-content ul,
.popover-content ol {
margin-bottom: 0;
}
d3.selection.prototype.tooltip = function(o, f) {
var body, clipped, clipper, d, defaults, height, holder, optionsList, parent, positions, sets, voronois, width;
if (arguments.length < 2) {
f = o;
}
body = d3.select('body');
defaults = {
type: "tooltip",
text: "You need to pass in a string for the text value",
title: "Title value",
content: "Content examples",
detection: "shape",
placement: "fixed",
gravity: "right",
position: [100, 100],
displacement: [0, 0],
mousemove: false
};
optionsList = [];
voronois = [];
this.each(function(d, i) {
var opt;
opt = f.apply(this, arguments);
optionsList.push(opt);
if (opt.detection === 'voronoi') {
return voronois.push([opt, i]);
}
});
if (voronois.length !== 0) {
parent = d3.select(this[0][0].ownerSVGElement);
holder = parent.append("g").attr("id", "__clip__holder__");
console.log(voronois);
positions = (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = voronois.length; _i < _len; _i++) {
d = voronois[_i];
_results.push(d[0].position);
}
return _results;
})();
console.log(positions);
sets = d3.geom.voronoi(positions);
height = parent.attr("height");
width = parent.attr("width");
clipper = d3.geom.polygon([[0, 0], [0, height], [width, height], [width, 0]]).clip;
clipped = positions.map(clipper);
holder.append("g").attr("id", "clipPaths").selectAll("clipPath").data(voronois).enter().append("clipPath").attr("id", function(d, i) {
return "clip-" + i;
}).append("circle").attr("cx", function(d) {
return d[0].position[0];
}).attr("cy", function(d) {
return d[0].position[1];
}).attr("r", function(d) {
return 20;
});
holder.append("g").attr("id", "clipped").selectAll("path").data(voronois).enter().append("path").attr("d", function(d, i) {
return "M" + (clipped[i].join('L')) + "Z";
}).attr("clip-path", function(d, i) {
return "url(#clip-" + i + ")";
});
}
return this.each(function(d, i) {
var el, move_tip, options, tip, htimer, tip_state;
options = optionsList[i];
el = d3.select(this);
move_tip = function(selection) {
var center, offsets;
center = [0, 0];
if (options.placement === "mouse") {
center = d3.mouse(body.node());
} else {
offsets = this.ownerSVGElement.getBoundingClientRect();
center[0] = offsets.left;
center[1] = offsets.top;
center[0] += options.position[0];
center[1] += options.position[1];
center[0] += window.scrollX;
center[1] += window.scrollY;
}
center[0] += options.displacement[0];
center[1] += options.displacement[1];
return selection.style("left", "" + center[0] + "px").style("top", "" + center[1] + "px").style("display", "block");
};
el.on("mouseover", function() {
var inner;
console.log("mouseover", this, arguments, options, el, tip, tip_state);
tip_state |= 1;
if (tip) {
console.log("***** TIP already built ***** ", this, arguments, options, el, tip, tip_state);
// update tooltip texts?
if (options.type === "tooltip") {
tip.select(".tooltip-inner").html(options.text);
}
if (options.type === "popover") {
inner = tip.select(".popover-inner");
inner.select(".popover-title").text(options.title);
inner.select(".popover-content").select("p").remove();
inner.select(".popover-content").append("p").html(options.content[0][0].outerHTML);
}
} else {
tip = body.append("div").classed(options.type, true).classed(options.gravity, true).classed('fade', true).style("display", "none");
if (options.type === "tooltip") {
tip.append("div").html(options.text).attr("class", "tooltip-inner");
}
if (options.type === "popover") {
inner = tip.append("div").attr("class", "popover-inner");
inner.append("h3").text(options.title).attr("class", "popover-title");
inner.append("div").attr("class", "popover-content").append("p").html(options.content[0][0].outerHTML);
}
tip.append("div").attr("class", "arrow");
tip.on("mouseenter", function() {
console.log("TT.mouseenter", this, arguments, options, el, tip_state);
tip_state |= 2;
}).on("mouseleave", function() {
console.log("TT.mouseleave ******** ", this, arguments, options, el, tip_state);
tip_state &= ~2;
console.log("TT.mouseleave ******** ", this, arguments, options, el, tip_state);
});
}
if (htimer) clearTimeout(htimer);
/* htimer = */ setTimeout(function() {
if (tip && (tip_state & 1))
tip.classed('in', true);
}, 10);
return tip.style("display", "").call(move_tip.bind(this));
});
if (options.mousemove) {
el.on("mousemove", function() {
console.log("mousemove", this, arguments, options, el);
return tip.call(move_tip.bind(this));
});
}
return el.on("mouseout", function() {
var tip_remover;
console.log("mouseout", this, arguments, options, el, tip);
tip_remover = function() {
console.log("tip_remover", this, arguments, options, el, tip, tip_state);
if (tip && (tip_state & 1) && !(tip_state & 2)) {
tip_state = 0;
tip.classed('in', false);
setTimeout(function() {
if (tip)
tip.remove();
tip = null;
}, 500);
} else {
if (htimer) clearTimeout(htimer);
htimer = setTimeout(tip_remover, 150);
}
};
if (htimer) clearTimeout(htimer);
htimer = setTimeout(tip_remover, 150);
});
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment