11

This is a follow up from my previous question.

I have a progressbar.js circle that animates on scroll. If there is just one circle it works as expected.

Now I want to create many of these animated circles by looping through an object with different key-values pairs.

For example:

  var divsValues = {
    'total-score-circle': 0.75, 
    'general-score-circle': 0.80, 
    'speed-score-circle': 0.85, 
    'privacy-score-circle': 0.90,
  };

For each key-value pair, the key is a div ID and the value is number that tells the animation how far to go.

Below is the code where I try to implement my loop, but the problem is that only the last circle is animated on scroll. All the circles appear in their "pre-animation" state, but only the last circle actually becomes animated when you scroll to the bottom.

I need each circle to animate once it is in the viewport.

//Loop through my divs and create animated circle for each one
function makeCircles() {
  var divsValues = {
    'total-score-circle': 0.75,
    'general-score-circle': 0.80,
    'speed-score-circle': 0.85,
    'privacy-score-circle': 0.90,
  };

  for (var i in divsValues) {
    if (divsValues.hasOwnProperty(i)) {
      bgCircles(i, divsValues[i]);
    }
  }
}
makeCircles();

// Check if element is scrolled into view
function isScrolledIntoView(elem) {
  var docViewTop = jQuery(window).scrollTop();
  var docViewBottom = docViewTop + jQuery(window).height();
  var elemTop = jQuery(elem).offset().top;
  var elemBottom = elemTop + jQuery(elem).height();

  return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
}

//Circle design and animation
function bgCircles(divid, countvalue) {
  // Design the circle using progressbar.js
  bar = new ProgressBar.Circle(document.getElementById(divid), {
    color: '#ddd',
    // This has to be the same size as the maximum width to
    // prevent clipping
    strokeWidth: 4,
    trailWidth: 4,
    easing: 'easeInOut',
    duration: 1400,
    text: {
      autoStyleContainer: false
    },
    from: {
      color: '#ddd',
      width: 4
    },
    to: {
      color: '#888',
      width: 4
    },
    // Set default step function for all animate calls
    step: function(state, circle) {
      circle.path.setAttribute('stroke', state.color);
      circle.path.setAttribute('stroke-width', state.width);

      var value = Math.round(circle.value() * 100);
      if (value === 0) {
        circle.setText('');
      } else {
        circle.setText(value + '%');
      }
    }
  });
  bar.text.style.fontFamily = '"Montserrat", sans-serif';
  bar.text.style.fontSize = '1.7rem';
  bar.trail.setAttribute('stroke-linecap', 'round');
  bar.path.setAttribute('stroke-linecap', 'round');

  //Animate the circle when scrolled into view
  window.onscroll = function() {
    if (isScrolledIntoView(jQuery('#' + divid))) bar.animate(countvalue);
    else bar.animate(0); // or bar.set(0)
  }
}
#total-score-circle,
#general-score-circle,
#speed-score-circle,
#privacy-score-circle {
  margin: 0.8em auto;
  width: 100px;
  height: 100px;
  position: relative;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/progressbar.js/1.0.1/progressbar.min.js"></script>

<div id="total-score-circle"></div>
<div id="general-score-circle"></div>
<div id="speed-score-circle"></div>
<div id="privacy-score-circle"></div>

While researching this problem I learned that JavaScript will only output the last value of a loop, which I thought could be the cause of my problem.

So I tried to replace the for loop with these solutions...

Solution 1: Same problem as before, only the last loop animates on scroll.

  for (var i in divsValues) {
    (function(){
      var ii = i;
        if (divsValues.hasOwnProperty(ii)) {
          bgCircles(ii, divsValues[ii]);
        }
    })();        
  }

Solution 2: Again, same problem as before, only the last loop animates on scroll.

  for (var i in divsValues) {
    let ii = i;
      if (divsValues.hasOwnProperty(ii)) {
        bgCircles(ii, divsValues[ii]);
      }
  }

Solution 3: Again, same problem as before, only the last loop animates on scroll.

  for (var i in divsValues) {
    try{throw i}
    catch(ii) {
      if (divsValues.hasOwnProperty(ii)) {
        bgCircles(ii, divsValues[ii]);
      }
    }
  }

So now I'm thinking maybe the problem is not the loop, but something I can't see or figure out.

user7637745
  • 1,659
  • 2
  • 13
  • 26
TinyTiger
  • 814
  • 2
  • 16
  • 33

3 Answers3

2

The loop you have will run so fast that the browser engine wont be able to render the changes, I would suggest either you use setInterval() method or continuous setTimeout() method which will add some delay to your code so that the browser can render the changes you are making.

For your special case I would suggest:

var i = 0;
var tobecleared = setInterval(timer,1000);

function timer(){
    var p = get_ith_key_from_divsvalues(i);//implement this method
    console.log(p);
    bgCircles(p, divsValues[p]);
    i++;
    if(i == Object.keys(divsValues).length)
         clearInterval(tobecleared);
}
function get_ith_key_from_divsvalues(i){
     var j = -1;
     for(var property in divsValues){
          j++;
          if(j==i)
                return property;
     }
}

Note : window.onscroll is being overwritten in each call that is why only the last circle responds.

Adriano
  • 2,862
  • 4
  • 25
  • 37
Anurag Kumar
  • 547
  • 7
  • 9
  • Thanks for the tip. Could you elaborate what you mean by `implement this method`? I only just started learning JS and I'm not sure what I am supposed to do with that function. – TinyTiger Jun 17 '18 at 06:36
  • function get_ith_key_from_divsvalues(int i){ var j = 0; for(var property in divsValues){ if(j==i) return property; j++; } } – Anurag Kumar Jun 17 '18 at 06:41
  • Are you saying pull the divsValues object out of the function and use your delay below that? I tired it here but it doesn't work: https://jsfiddle.net/yvo1rpk9/ – TinyTiger Jun 17 '18 at 06:56
  • Debugging is a hard thing to do my friend, i would suggest print all the suspected values and see where is it lacking, i am trying too. – Anurag Kumar Jun 17 '18 at 07:12
  • I debugged my code, there were some typos. I commented your function call and ran the code in my terminal it outputs in an expected way. – Anurag Kumar Jun 17 '18 at 07:29
  • I ran your jsfiddle code with my degugged code inserted in it. It outputs 4 translucent kind of circles one by one in every one second. – Anurag Kumar Jun 17 '18 at 07:33
  • Thanks, I made an updated fiddle here: https://jsfiddle.net/yvo1rpk9/4/. There is still the original problem, only the last circle animated on scroll. None of the others animate. Only change is that each circle appears in a delayed fashion. Appreciate the help, but maybe the problem is something else? – TinyTiger Jun 17 '18 at 07:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/173288/discussion-between-tinytiger-and-anurag-kumar). – TinyTiger Jun 17 '18 at 07:53
2

You were quite close.

Here are the fixes:


In the function bgCircles(...), use var to declare the bar in that function's scope:

var bar = new ProgressBar.Circle(document.getElementById(divid), {

When you set the animate scrolled into view checker event, you assign a new function over-and-over to window.onscroll. Since you are using jQuery, consider jQuery's .scroll event handler and use it like this:

$(window).scroll(function () {
    if (isScrolledIntoView(jQuery('#' + divid))) bar.animate(countvalue);
    else bar.animate(0); // or bar.set(0)
});

The solution in whole:

//Loop through my divs and create animated circle for each one
function makeCircles() {
  var divsValues = {
    'total-score-circle': 0.75,
    'general-score-circle': 0.80,
    'speed-score-circle': 0.85,
    'privacy-score-circle': 0.90,
  };

  for (var i in divsValues) {
    if (divsValues.hasOwnProperty(i)) {
      bgCircles(i, divsValues[i]);
    }
  }
}
makeCircles();

// Check if element is scrolled into view
function isScrolledIntoView(elem) {
  var docViewTop = jQuery(window).scrollTop();
  var docViewBottom = docViewTop + jQuery(window).height();
  var elemTop = jQuery(elem).offset().top;
  var elemBottom = elemTop + jQuery(elem).height();

  return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
}

//Circle design and animation
function bgCircles(divid, countvalue) {
  // Design the circle using progressbar.js
  var bar = new ProgressBar.Circle(document.getElementById(divid), {
    color: '#ddd',
    // This has to be the same size as the maximum width to
    // prevent clipping
    strokeWidth: 4,
    trailWidth: 4,
    easing: 'easeInOut',
    duration: 1400,
    text: {
      autoStyleContainer: false
    },
    from: {
      color: '#ddd',
      width: 4
    },
    to: {
      color: '#888',
      width: 4
    },
    // Set default step function for all animate calls
    step: function(state, circle) {
      circle.path.setAttribute('stroke', state.color);
      circle.path.setAttribute('stroke-width', state.width);

      var value = Math.round(circle.value() * 100);
      if (value === 0) {
        circle.setText('');
      } else {
        circle.setText(value + '%');
      }
    }
  });
  bar.text.style.fontFamily = '"Montserrat", sans-serif';
  bar.text.style.fontSize = '1.7rem';
  bar.trail.setAttribute('stroke-linecap', 'round');
  bar.path.setAttribute('stroke-linecap', 'round');

  //Animate the circle when scrolled into view
  $(window).scroll(function () {
    if (isScrolledIntoView(jQuery('#' + divid))) bar.animate(countvalue);
    else bar.animate(0); // or bar.set(0)
  });
}
#total-score-circle,
#general-score-circle,
#speed-score-circle,
#privacy-score-circle {
  margin: 0.8em auto;
  width: 100px;
  height: 100px;
  position: relative;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/progressbar.js/1.0.1/progressbar.min.js"></script>

<div id="total-score-circle"></div>
<div id="general-score-circle"></div>
<div id="speed-score-circle"></div>
<div id="privacy-score-circle"></div>

Note:

Since I didn't edit any of your circle animation/circle visibility checking functions, I assume you intended the current state of your animate-when-scrolled-and-in-view functionality the way, that it is right now. In this current state, your script does/has the following side-effects:

  • If you don't scroll the page, not at all, your circles won't start to animate, even, when they are visible. Solution: encapsulate the visibility checker lines to a separate function and run, when creating the circles.

  • If you scroll through a circle, its animation with the percent will going to go to its default state, which is 0%. Solution: change the visibility checker function, when the particular element is not visible because of overscroll, return that state as visible too. This way your circles will stay at 100%, even when you scroll over them.


Regarding performance and best practices:

  • When using jQuery, make sure to call jQuery(...) or its shorthand $(...) as few times as possible. Use variables to store elements, properties, and data.

  • It's better to separate longer/larger, monolithic functions into smaller functions with a more narrow, but also more clear scope of functionality.

  • When using event listeners, make sure to run as few of them as possible. Structure your HTML and your JavaScript code to have clear and performant ways to access and modify your crucial elements, properties, and data.

user7637745
  • 1,659
  • 2
  • 13
  • 26
  • 1
    Thanks so much! I am trying to get rid of the first side effect you mentioned... if the circle is visible on page load I want it to animate immediately, not wait until there is a scroll. While the circles outside the onload viewport should be animated once scrolled into view. I tried your solution of encapsulating the visibility checker and running that function when creating the circles, but the circles that are visible on page load still need a scroll before animation. See fiddle here: http://jsfiddle.net/z51h89wy/17/ – TinyTiger Jun 27 '18 at 03:58
  • You're welcome! I see, you need a slight refactoring and it's gonna be okay. If you cannot solve the problem the way you want it, please create another question with a well-structured and clearly phrased question. That way, others will see the full solution in the future. ;) – user7637745 Jun 27 '18 at 08:01
0

There are two fixes you need to apply.

  1. Currently bar is the global variable, so it's always the same, to fix it declare it with var.

  2. Use window.addEventListener to attach scroll event to window, by setting handler with window.onscroll you constantly override event handler and usage of addEventListener allow you attach more than one event handler.

//Loop through my divs and create animated circle for each one
$( document ).ready(function(){
function makeCircles() {
  var divsValues = {
    'total-score-circle': 0.75,
    'general-score-circle': 0.80,
    'speed-score-circle': 0.85,
    'privacy-score-circle': 0.90,
  };

  for (var i in divsValues) {
    if (divsValues.hasOwnProperty(i)) {
      bgCircles(i, divsValues[i]);
    }
  }
}
makeCircles();

// Check if element is scrolled into view
function isScrolledIntoView(elem) {
  var docViewTop = jQuery(window).scrollTop();
  var docViewBottom = docViewTop + jQuery(window).height();
  var elemTop = jQuery(elem).offset().top;
  var elemBottom = elemTop + jQuery(elem).height();

  return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
}

//Circle design and animation
function bgCircles(divid, countvalue) {
  // Design the circle using progressbar.js
  var bar = new ProgressBar.Circle(document.getElementById(divid), {
    color: '#ddd',
    // This has to be the same size as the maximum width to
    // prevent clipping
    strokeWidth: 4,
    trailWidth: 4,
    easing: 'easeInOut',
    duration: 1400,
    text: {
      autoStyleContainer: false
    },
    from: {
      color: '#ddd',
      width: 4
    },
    to: {
      color: '#888',
      width: 4
    },
    // Set default step function for all animate calls
    step: function(state, circle) {
      circle.path.setAttribute('stroke', state.color);
      circle.path.setAttribute('stroke-width', state.width);

      var value = Math.round(circle.value() * 100);
      if (value === 0) {
        circle.setText('');
      } else {
        circle.setText(value + '%');
      }
    }
  });
  bar.text.style.fontFamily = '"Montserrat", sans-serif';
  bar.text.style.fontSize = '1.7rem';
  bar.trail.setAttribute('stroke-linecap', 'round');
  bar.path.setAttribute('stroke-linecap', 'round');

  //Animate the circle when scrolled into view
  window.addEventListener('scroll', function() {
    if (isScrolledIntoView(jQuery('#' + divid))) bar.animate(countvalue);
    else bar.animate(0); // or bar.set(0)
  })
}
})
#total-score-circle,
#general-score-circle,
#speed-score-circle,
#privacy-score-circle {
  margin: 0.8em auto;
  width: 100px;
  height: 100px;
  position: relative;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/progressbar.js/1.0.1/progressbar.min.js"></script>

<div id="total-score-circle"></div>
<div id="general-score-circle"></div>
<div id="speed-score-circle"></div>
<div id="privacy-score-circle"></div>
Andriy Ivaneyko
  • 15,838
  • 3
  • 43
  • 65