Search code examples
javascriptjquerysvgsnap.svgsvg-animate

Snap.svg & JavaScript: Creating Shapes and Animating Each on Delay inside a For Loop


first-time/long-time (quack, quack).

I'm a bit frustrated, and just about stumped, by this riddle I can't quite solve in Snap.svg. It's probably an oversight that I'll kick myself for missing, but I'm not seeing it at this point.

I have x & y data that I draw from a DOM element and store into a series of arrays, filter based on certain values in certain columns, and eventually create multiple instances of the ChartLine object in js. Basically, it sorts a certain column by quantity, assigns each value's row a color from a RainbowVis.js object, pushes all the relevant values from each row into an array for y, and draws a path on the line chart where y is the value and x is a steadily-increasing integer in a For loop.

What I'm currently doing here, in the draw() function, is: for each relevant column, create a <circle> with the variable "dot" with the object's x & y variables, assign the attributes, animate the radius from 0 to 8 in a quarter-second, and add the x & y values of i to a string to be used in a <path> I create right after the For loop. I then animate the path, etc., etc.

Without the setTimeout(), it works well. The circles and paths all animate simultaneously on load. However, I want to add a delay to each .animate with the number of milliseconds increasing by polyDelayInterval in each iteration, so each "dot" animates as the line arrives at it. At the VERY least, I want to animate all of the "dots" after the path is done animating.

The problem is, no matter what I've tried so far, I can only get the last set of "dots" (at the highest x value for each line) to animate; the rest stay at r:0. I've read several somewhat-similar posts, both here and elswhere; I've searched up and down the Snap.svg's docs on their site. I just cannot find what I'm doing wrong. Thanks in advance!

var svgMFLC = Snap('svg#ElementID');


function ChartLine(x, y, color, row) {  
  this.x = x;
  this.y = y;
  this.color = color;
  this.row = row;
  var propNames = Object.keys(this.row);
  var yAdjust;
  var resetX = this.x;

  for (var i = 0; i < propNames.length; i++) { //  get only the calculated score columns and their values
    if (propNames[i].toLowerCase().includes(calcKeyword)) {
      yAdjustedToChartArea = chartBottom - (this.row[propNames[i]] * yInterval);
      this.y.push(yAdjustedToChartArea);    //  returns the value of that score column and pushes it to the y array
    }
  }

  this.draw = function () {
    var points = "M";   //  the string that will determine the coordinates of each line
    var dotShadow = svgMFLC.filter(Snap.filter.shadow(0, 0, 2, "#000000", 0.4));
    var polyTime = 1500;    //  in milliseconds
    var dot;
    var polyDelayInterval = polyTime / (semesterCols.length - 1);
    for (var i = 0; i < semesterCols.length; i++) { //  for each data point, create a "dot"
      dot = svgMFLC.circle(this.x, this.y[i], 0);
      dot.attr({
        fill: this.color,
        stroke: "none",
        filter: dotShadow,
        class: "chartPointMFLC"
      });
      setTimeout(function () {
        dot.animate({ r: 8 }, 250, mina.easeout);
      }, polyDelayInterval * i);
      points += this.x + " " + this.y[i] + " L";
      this.x = this.x + xInterval;
    }
    points = points.slice(0, -2);   //  take away the excessive " L" from the end of the points string
    var poly = svgMFLC.path(points);    
    var polyLength = poly.getTotalLength(); 
    poly.attr({
      fill: "none",
      stroke: this.color,
      class: "chartLineMFLC",
      strokeDasharray: polyLength + " " + polyLength, //  setting the strokeDash attributes will help create the "drawing the line" effect when animated
      strokeDashoffset: polyLength
    });
    poly.animate({ strokeDashoffset: 0.00 }, polyTime);
    this.x = resetX;
  }
}

Solution

  • I can't put a tested solution up without the full code to test, but the problem is almost certainly that you at least need to get a closure for your 'dot' element.

    So this line...

    setTimeout(function () {
        dot.animate({ r: 8 }, 250, mina.easeout);
    }, polyDelayInterval * i);
    

    When it comes to call that function, 'dot' will be the last 'dot' from the loop. So you need to create a closure (create functional scope for dot).

    So something a bit like...

    (function() {
        var laterDot = dot;
        setTimeout(function () {
            laterDot.animate({ r: 8 }, 250, mina.easeout);
        }, polyDelayInterval * i)
    })();