function arrowConnector() { var svg, arrows; function render() { if(d3.select(".arrow-connector-container").empty()) { svg = d3.select("body").append("svg") .attr("xmlns", "http://www.w3.org/2000/svg") .classed("arrow-connector-container", true) .style("position", "absolute") .style("top", "0") .style("left", "0") .style("width", "100%") .style("height", "100%") .style("pointer-events", "none"); } else { svg = d3.select(".arrow-connector-container"); } arrows = svg.selectAll("line") .data(getTargets()); arrows.enter() .append("line") .classed("arrow-connector", true); arrows .attr("x1", function(d) { return d[0].x }) .attr("y1", function(d) { return d[0].y }) .attr("x2", function(d) { return d[1].x }) .attr("y2", function(d) { return d[1].y }); } function getTargets() { var targets = []; d3.selectAll("[data-arrow-target]") .each(function(d,i) { fromCorners = edgesToCorners(this); d3.selectAll(this.dataset.arrowTarget).each(function(dd,ii) { var toCorners = edgesToCorners(this); // check all possible combinations of eligible endpoints for the shortest distance var fromClosest, toClosest, distance; fromCorners.forEach(function(from) { toCorners.forEach(function(to) { if(distance == null || hypotenuse( to.x-from.x, to.y-from.y ) < distance) { distance = hypotenuse( to.x-from.x, to.y-from.y ); fromClosest = from; toClosest = to; } }); }); targets.push([fromClosest,toClosest]); }); }); return targets; } // gets from the sides of a bounding rect (left, right, top, bottom) // to its corners (topleft, topright, bottomleft, bottomright) function edgesToCorners(element) { var corners = []; ["left","right"].forEach(function(i) { ["top","bottom"].forEach(function(j) { corners.push({"x":i,"y":j}); }); }); return corners.map(function(corner) { return { "x": element.getBoundingClientRect()[corner.x] + window.pageXOffset, "y": element.getBoundingClientRect()[corner.y] + window.pageYOffset }; }); } // this seems good to have function hypotenuse(a, b) { return Math.sqrt( Math.pow(a,2) + Math.pow(b,2) ); } return render; }