block by tophtucker 2e72c41ffb8e7ecab8d9

Arrow connector helper

Full Screen

It is conceivable that one might want a setup whereby someone can arrange a Web Page such that certain things point to certain other things without writing any javascript or a ton of custom css or whatever. This is a solution.

1. HTML

In the data-arrow-target attribute of an HTML element, provide a CSS selector. If the selector matches more than one element, lines will be drawn from the element to all the matching elements.

E.g.: <div id="one" data-arrow-target="#two"></div>

2. JavaScript

arrowConnector() returns a render function.


// Gets your renderer
var connect = arrowConnector();

// Renders for the first time
connect();

// Render on resize
d3.select(window).on("resize", connect);

3. CSS

Arrows get a class .arrow-connector. Do as you will with it.

The end.

FAQ

index.html

<html>
<head>
<style>
  .arrow-connector {
    stroke: black;
  }
  #one, #two { 
    position: absolute; 
    width: 100px;
    height: 100px;
  }
  #one {
    top: 50px;
    left: 50px;
    background: #f0f;
  }
  #two {
    top: 250px;
    left: 500px;
    background: #0f0;
  }
</style>
</head>

<body>
<div id="one" data-arrow-target="#two"></div>
<div id="two"></div>

<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="arrowConnector.js"></script>
<script type="text/javascript">

var connect = arrowConnector();
connect();
d3.select(window).on("resize", connect);

</script>

</body>
</html>

arrowConnector.js

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;

}