block by GerHobbelt 3070621

d3 bootstrap popovers (exhibit A)

Full Screen

index.js

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);

index.html

<!doctype html>
<head>
<style>
body {
  font: 10px sans-serif;
}
#main {
  left: 25%;
  position: absolute;
}
#main #text {
  padding-bottom: 10px;
}
.popover
{
  border: solid red 1px;
}
</style>
<link rel="stylesheet" href="tooltip.css">
<script src="//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>

readme.mkd

Uses the markup and css from bootstrap to make pretty popovers. 
Find the files [here](https://github.com/zmaril/d3-bootstrap-plugins)

See [gist 3070659](http://bl.ocks.org/3070659) for the fix & explanation.

Testing
-------

Position mouse over the right half of a circle node (a little more to the right please, so you end up within the tooltip red border when it shows up ;-) ); move mouse very little to see odd behaviour.

Firefox is particularly wicked: position mouse in that area, on the **edge** of a circle node, manoeuvre subtly to see a hail of mouseout/mouseover events being fired from the circle node (see console.log).

tooltip.css

/* 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;
  filter: alpha(opacity=0);
  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;
}

tooltip.js

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;
    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() {
console.log("mouseover", this, arguments, options);
      var inner, tip;
      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");
      setTimeout(function() {
        return 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);
        return d3.select("." + options.type).call(move_tip.bind(this));
      });
    }
    return el.on("mouseout", function() {
      var tip;
console.log("mouseout", this, arguments, options);
      tip = d3.selectAll("." + options.type).classed('in', false);
      return setTimeout(function() {
        return tip.remove();
      }, 150);
    });
  });
};