Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I draw an arrow between two points in d3v4?

I have created a custom path renderer that draws an arrow between the nodes in my d3 graph as shown in the snippet. I have one last issue I am getting stuck on,

How would I rotate the arrow portion so that it is pointing from the direction of the curve instead of the direction of the source?

var w2 = 6,
  ar2 = w2 * 2,
  ah = w2 * 3,
  baseHeight = 30;

// Arrow function
function CurvedArrow(context, index) {
  this._context = context;
  this._index = index;
}
CurvedArrow.prototype = {
  areaStart: function() {
    this._line = 0;
  },
  areaEnd: function() {
    this._line = NaN;
  },
  lineStart: function() {
    this._point = 0;
  },
  lineEnd: function() {
    if (this._line || (this._line !== 0 && this._point === 1)) {
      this._context.closePath();
    }
    this._line = 1 - this._line;
  },
  point: function(x, y) {
    x = +x, y = +y; // jshint ignore:line
    switch (this._point) {
      case 0:
        this._point = 1;
        this._p1x = x;
        this._p1y = y;
        break;
      case 1:
        this._point = 2; // jshint ignore:line
      default:
        var p1x = this._p1x,
          p1y = this._p1y,
          p2x = x,
          p2y = y,
          dx = p2x - p1x,
          dy = p2y - p1y,
          px = dy,
          py = -dx,
          pr = Math.sqrt(px * px + py * py),
          nx = px / pr,
          ny = py / pr,
          dr = Math.sqrt(dx * dx + dy * dy),
          wx = dx / dr,
          wy = dy / dr,
          ahx = wx * ah,
          ahy = wy * ah,
          awx = nx * ar2,
          awy = ny * ar2,
          phx = nx * w2,
          phy = ny * w2,

          //Curve figures
          alpha = Math.floor((this._index - 1) / 2),
          direction = p1y < p2y ? -1 : 1,
          height = (baseHeight + alpha * 3 * ar2) * direction,


          //             r5
          //r7         r6|\
          // ------------  \
          // ____________  /r4
          //r1         r2|/
          //             r3

          r1x = p1x - phx,
          r1y = p1y - phy,
          r2x = p2x - phx - ahx,
          r2y = p2y - phy - ahy,
          r3x = p2x - awx - ahx,
          r3y = p2y - awy - ahy,
          r4x = p2x,
          r4y = p2y,
          r5x = p2x + awx - ahx,
          r5y = p2y + awy - ahy,
          r6x = p2x + phx - ahx,
          r6y = p2y + phy - ahy,
          r7x = p1x + phx,
          r7y = p1y + phy,
          //Curve 1
          c1mx = (r2x + r1x) / 2,
          c1my = (r2y + r1y) / 2,
          m1b = (c1mx - r1x) / (r1y - c1my),
          den1 = Math.sqrt(1 + Math.pow(m1b, 2)),
          mp1x = c1mx + height * (1 / den1),
          mp1y = c1my + height * (m1b / den1),
          //Curve 2
          c2mx = (r7x + r6x) / 2,
          c2my = (r7y + r6y) / 2,
          m2b = (c2mx - r6x) / (r6y - c2my),
          den2 = Math.sqrt(1 + Math.pow(m2b, 2)),
          mp2x = c2mx + height * (1 / den2),
          mp2y = c2my + height * (m2b / den2);

        this._context.moveTo(r1x, r1y);
        this._context.quadraticCurveTo(mp1x, mp1y, r2x, r2y);
        this._context.lineTo(r3x, r3y);
        this._context.lineTo(r4x, r4y);
        this._context.lineTo(r5x, r5y);
        this._context.lineTo(r6x, r6y);
        this._context.quadraticCurveTo(mp2x, mp2y, r7x, r7y);

        break;
    }
  }
};
var w = 600,
  h = 220;
var t0 = Date.now();

var points = [{
  R: 100,
  r: 3,
  speed: 2,
  phi0: 190
}];
var path = d3.line()
  .curve(function(ctx) {
    return new CurvedArrow(ctx, 1);
  });

var svg = d3.select("svg");
var container = svg.append("g")
  .attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")

container.selectAll("g.planet").data(points).enter().append("g")
  .attr("class", "planet").each(function(d, i) {
    d3.select(this).append("circle").attr("r", d.r).attr("cx", d.R)
      .attr("cy", 0).attr("class", "planet");
  });
container.append("path");
var planet = d3.select('.planet circle');

d3.timer(function() {
  var delta = (Date.now() - t0);
  planet.attr("transform", function(d) {
    return "rotate(" + d.phi0 + delta * d.speed / 50 + ")";
  });

  var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
  g.setAttributeNS(null, "transform", planet.attr('transform'));
  var matrix = g.transform.baseVal.consolidate().matrix;
  svg.selectAll("path").attr('d', function(d) {
    return path([
      [0, 0],
      [matrix.a * 100, matrix.b * 100]
    ])
  });
});
path {
  stroke: #11a;
  fill: #eee;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="220"></svg>
like image 727
Andrew Avatar asked Oct 30 '22 19:10

Andrew


1 Answers

I ended up doing what @Mark suggested in the comments, I calculate the point that is the height of the curve away along the normal midway between the two points, then calculate the unit vectors from the start point to the mid point and again from the midpoint to the end. I can then use those to get all the required points.

var arrowRadius = 6,
  arrowPointRadius = arrowRadius * 2,
  arrowPointHeight = arrowRadius * 3,
  baseHeight = 30;

// Arrow function
function CurvedArrow(context, index) {
  this._context = context;
  this._index = index;
}
CurvedArrow.prototype = {
  areaStart: function() {
    this._line = 0;
  },
  areaEnd: function() {
    this._line = NaN;
  },
  lineStart: function() {
    this._point = 0;
  },
  lineEnd: function() {
    if (this._line || (this._line !== 0 && this._point === 1)) {
      this._context.closePath();
    }
    this._line = 1 - this._line;
  },
  point: function(x, y) {
    x = +x, y = +y; // jshint ignore:line
    switch (this._point) {
      case 0:
        this._point = 1;
        this._p1x = x;
        this._p1y = y;
        break;
      case 1:
        this._point = 2; // jshint ignore:line
      default:
        var p1x = this._p1x,
          p1y = this._p1y,
          p2x = x,
          p2y = y,

          //Curve figures

          //             mp1
          //              |
          //              | height
          //              |
          // p1 ----------------------- p2
          //
          alpha = Math.floor((this._index - 1) / 2),
          direction = p1y < p2y ? -1 : 1,
          height = (baseHeight + alpha * 3 * arrowPointRadius) * direction,
          c1mx = (p2x + p1x) / 2,
          c1my = (p2y + p1y) / 2,
          m1b = (c1mx - p1x) / (p1y - c1my),
          den1 = Math.sqrt(1 + Math.pow(m1b, 2)),
          // Perpendicular point from the midpoint.
          mp1x = c1mx + height * (1 / den1),
          mp1y = c1my + height * (m1b / den1),

          // Arrow figures
          dx = p2x - mp1x,
          dy = p2y - mp1y,
          dr = Math.sqrt(dx * dx + dy * dy),
          // Normal unit vectors
          nx = dy / dr,
          wy = nx,
          wx = dx / dr,
          ny = -wx,
          ahx = wx * arrowPointHeight,
          ahy = wy * arrowPointHeight,
          awx = nx * arrowPointRadius,
          awy = ny * arrowPointRadius,
          phx = nx * arrowRadius,
          phy = ny * arrowRadius,

          // Start arrow offset.
          sdx = mp1x - p1x,
          sdy = mp1y - p1y,
          spr = Math.sqrt(sdy * sdy + sdx * sdx),
          snx = sdy / spr,
          sny = -sdx / spr,
          sphx = snx * arrowRadius,
          sphy = sny * arrowRadius,

          //             r5
          //r7         r6|\
          // ------------  \
          // ____________  /r4
          //r1         r2|/
          //             r3

          r1x = p1x - sphx,
          r1y = p1y - sphy,
          r2x = p2x - phx - ahx,
          r2y = p2y - phy - ahy,
          r3x = p2x - awx - ahx,
          r3y = p2y - awy - ahy,
          r4x = p2x,
          r4y = p2y,
          r5x = p2x + awx - ahx,
          r5y = p2y + awy - ahy,
          r6x = p2x + phx - ahx,
          r6y = p2y + phy - ahy,
          r7x = p1x + sphx,
          r7y = p1y + sphy,
          mpc1x = mp1x - phx,
          mpc1y = mp1y - phy,
          mpc2x = mp1x + phx,
          mpc2y = mp1y + phy;

        this._context.moveTo(r1x, r1y);
        this._context.quadraticCurveTo(mpc1x, mpc1y, r2x, r2y);
        this._context.lineTo(r3x, r3y);
        this._context.lineTo(r4x, r4y);
        this._context.lineTo(r5x, r5y);
        this._context.lineTo(r6x, r6y);
        this._context.quadraticCurveTo(mpc2x, mpc2y, r7x, r7y);
        this._context.closePath();

        break;
    }
  }
};

var w = 600,
  h = 220;
var t0 = Date.now();

var points = [{
  R: 100,
  r: 3,
  speed: 2,
  phi0: 190
}];
var path = d3.line()
  .curve(function(ctx) {
    return new CurvedArrow(ctx, 1);
  });

var svg = d3.select("svg");
var container = svg.append("g")
  .attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")

container.selectAll("g.planet").data(points).enter().append("g")
  .attr("class", "planet").each(function(d, i) {
    d3.select(this).append("circle").attr("r", d.r).attr("cx", d.R)
      .attr("cy", 0).attr("class", "planet");
  });
container.append("path");
var planet = d3.select('.planet circle');

d3.timer(function() {
  var delta = (Date.now() - t0);
  planet.attr("transform", function(d) {
    return "rotate(" + d.phi0 + delta * d.speed / 50 + ")";
  });

  var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
  g.setAttributeNS(null, "transform", planet.attr('transform'));
  var matrix = g.transform.baseVal.consolidate().matrix;
  svg.selectAll("path").attr('d', function(d) {
    return path([
      [0, 0],
      [matrix.a * 100, matrix.b * 100]
    ])
  });
});
path {
  stroke: #11a;
  fill: #eee;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="600" height="220"></svg>
like image 131
Andrew Avatar answered Nov 12 '22 14:11

Andrew