Bradley Singer
Bradley Singer

Reputation: 376

D3 Multiple SVG Objects Following Path

I am very new to D3, and I wanted to make a line with multiple, evenly spaced arrows running along said line to indicate the flow of a circuit.

This is basically what I'm going for (with the arrows animated along the line in an infinite loop) (link in comment, not high enough reputation)

I found this great example of an image animated to follow a path with correct rotation. http://bl.ocks.org/KoGor/8163268

My problem is that I don't know how to place all the extra arrows on to my path. I considered breaking up my path into many equal length paths in a group end to end and animating them all at once, but that seemed more complicated than it needed to be.

Any idea how I should proceed?

Here is what I have so far: https://jsfiddle.net/singerbradley/wcfg2mec/16/

Code

var points = [
  [480, 200],
  [580, 400],
  [680, 100],
  [780, 300],
  [180, 300],
  [280, 100],
  [380, 400]
];

var svg = d3.select("body").append("svg")
  .attr("width", 960)
  .attr("height", 500);

var path = svg.append("path")
  .data([points])
  .attr("d", d3.svg.line()
    .tension(1) // Catmull–Rom
    .interpolate("linear")); //basis-open

var arrow = svg.append("polygon")
  .attr("points", "0,24, 15,12, 0,0") // x,y points
	.attr("transform", "translate(" + points[3] + ")");

transition();

function transition() {
  arrow.transition()
    .duration(10000)
    .ease("linear")
    .attrTween("transform", translateAlong(path.node()))
    .each("end", transition); //infinite loop
}

// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
  var l = path.getTotalLength();
  var t0 = 0;
  return function(d, i, a) {
    return function(t) {
    	var p0 = path.getPointAtLength(t0 * l); //previous point
      var p = path.getPointAtLength(t * l); //current point
      var angle = Math.atan2(p.y - p0.y, p.x - p0.x) * 180 / Math.PI;//angle for tangent
      t0 = t;
      //Shifting center to center of arrow
      // xoffset and yoffset should be half the original width and height
      var xoffset = 12, yoffset = 12;
      var centerX = p.x - xoffset;
      var centerY = p.y - yoffset;
      return "translate(" + centerX + "," + centerY + ")rotate(" + angle + " " + xoffset + " " + yoffset + ")";
    };
  };
}
path {
  fill: none;
  stroke: #000;
  stroke-width: 1px;
}

polygon {
  fill: steelblue;
  stroke: #fff;
  stroke-width: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

d3.js

Upvotes: 1

Views: 892

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102219

There are several ways for doing this. Here is my solution.

First, if you want 10 arrows, let's create the data array with 10 elements:

var arrowData = d3.range(10);

And append the arrows accordingly:

var arrow = svg.selectAll(".arrow")
    .data(arrowData)
    .enter()
    .append("polygon")
    .attr("points", "0,24, 15,12, 0,0");

Then, at every second, we'll call transition() for a different arrow, using an IIFE with a setTimeout:

(function loop() {
    if (counter++ > 8) return;
    setTimeout(function() {
        var thisPolygon = d3.selectAll("polygon").filter(function(d, i) {
            return i == counter;
        });
        transition(thisPolygon);
        loop()
    }, 1000)
}());

For this to work, we slightly modify the transition function:

function transition(elem) {
    elem.transition()
        .duration(10000)
        .ease("linear")
        .attrTween("transform", translateAlong(path.node()))
        .each("end", function() {
            return transition(elem)
        }); 
}

Here is your updated fiddle: https://jsfiddle.net/3o7vzvfa/. Here is another one, with 50 arrows: https://jsfiddle.net/buLjg7d3/

And here the same code in a Stack snippet:

var points = [
    [480, 200],
    [580, 400],
    [680, 100],
    [780, 300],
    [180, 300],
    [280, 100],
    [380, 400]
];

var arrowData = d3.range(10);

var svg = d3.select("body").append("svg")
    .attr("width", 960)
    .attr("height", 500);

var path = svg.append("path")
    .data([points])
    .attr("d", d3.svg.line()
        .tension(1) // Catmull–Rom
        .interpolate("linear")); //basis-open

var arrow = svg.selectAll(".arrow")
    .data(arrowData)
    .enter()
    .append("polygon")
    .attr("points", "0,24, 15,12, 0,0");

var counter = -1;

(function loop() {
    if (counter++ > 8) return;
    setTimeout(function() {
        var thisPolygon = d3.selectAll("polygon").filter(function(d, i) {
            return i == counter;
        });
        transition(thisPolygon);
        loop()
    }, 1000)
}())

function transition(elem) {
    elem.transition()
        .duration(10000)
        .ease("linear")
        .attrTween("transform", translateAlong(path.node()))
        .each("end", function() {
            return transition(elem)
        }); //infinite loop
}

// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
    var l = path.getTotalLength();
    var t0 = 0;
    return function(d, i, a) {
        return function(t) {
            var p0 = path.getPointAtLength(t0 * l); //previous point
            var p = path.getPointAtLength(t * l); //current point
            var angle = Math.atan2(p.y - p0.y, p.x - p0.x) * 180 / Math.PI; //angle for tangent
            t0 = t;
            //Shifting center to center of arrow
            // xoffset and yoffset should be half the original width and height
            var xoffset = 12,
                yoffset = 12;
            var centerX = p.x - xoffset;
            var centerY = p.y - yoffset;
            return "translate(" + centerX + "," + centerY + ")rotate(" + angle + " " + xoffset + " " + yoffset + ")";
        };
    };
}
path {
  fill: none;
  stroke: #000;
  stroke-width: 1px;
}

polygon {
  fill: steelblue;
  stroke: #fff;
  stroke-width: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Upvotes: 3

Related Questions