I want to create simple bar chart with animated bars (width from 0 to final size).
I started from css transitions but opera and chrome sometimes have problems.
Now I try to use mechanism what I saw here:
https://www.w3schools.com/w3css/w3css_progressbar.asp
using JS.
I have few "bar-areas":
<div id="chart-bars">
<div class="chart-bar-area">
<div class="chart-bar" data-percent="80" ></div>
</div>
<div class="chart-bar-area">
<div class="chart-bar" data-percent="60" ></div>
</div>
</div>
and JS code which works fine with one bar, but if I try to implement that mechanism for all in loop this script works correctly only for last one bar. Other bars not grooving.
My JS code below:
var foo = document.getElementById('chart-bars');
var width;
var elem;
var id;
for (var i = 0; i < foo.children.length; i++) {
elem = foo.children[i].children[0];
width = 1;
id = setInterval(frame, 10);
function frame() {
if (width >= elem.dataset.percent) {
clearInterval(id);
} else {
width++;
elem.style.width = width + '%';
}
}
}
Can somebody help me ?
Thanks
This is because value of i is out of scope of setInterval callback, setInterval takes only last value of iteration as it is a window method. So, you have to go with the closure or solution like this.
var foo = document.querySelectorAll(".chart-bar");
var width;
var elem;
var id;
for (var i = 0; i < foo.length; i++) {
var myElem = {
x: i
}
width = 1;
id = setInterval(function() {
if (width >= foo[this.x].dataset.percent) {
clearInterval(id);
} else {
width++;
foo[this.x].style.width = width + '%';
}
}.bind(myElem), 10);//Here you are binding that setInterval should run on the myElem object context not window context
}
Congratulations! Today will learn what a "closure" is.
You are experiencing a scoping issue. The reason this only works for the last bar is because the frame function only sees elem as it currently exists on scope. That is to say, by the time your setInterval runs your loop will have ended and elem will be firmly set to what amounts to foo.children[foo.children.length - 1].children[0]
The way to fix this is by creating a new closure. That is to say, you "close" the variable in a scope.
var foo = document.getElementById('chart-bars');
for (var i = 0; i < foo.children.length; i++) {
(function (elem, width) {
var id = setInterval(frame, 10);
function frame() {
if (width >= elem.dataset.percent) {
clearInterval(id);
} else {
width++;
elem.style.width = width + '%';
}
}
})(foo.children[i].children[0], 1)
}
now this, specifically, is a far from perfect way of accomplishing your goal, but I wrote it this way in order to preserve as much of your code as possible in an attempt to reduce the cognitive load for your specific example.
Essentially what I am doing is wrapping your code in an IIFE (Immediately Invoked Function Expression), which creates a new scope with your elem and width variables fixed to that function's scope.
A further step in the right direction would be to pull your frame function outside of the loop and have it only created once and have it accept as its arguments an elem, width, and id parameter, but I'll leave that as an exercise for the reader :-)
why don't you use the specific jquery lybraries to create the charts like morrischart or chartjs, there are very simple and they work very well
there are the documentations
http://morrisjs.github.io/morris.js/
https://www.chartjs.org
Looks like a variable scope issue, could you try something like this :
var foo = document.getElementById('chart-bars');
for (var i = 0; i < foo.children.length; i++) {
var elem = foo.children[i].children[0];
elem.style.width = '1%';
var id = setInterval(function() { frame(elem, id) }, 10);
}
function frame(elem, idInterval) {
var width = parseInt(elem.style.width);
if (width >= elem.dataset.percent) {
clearInterval(idInterval);
} else {
width++;
elem.style.width = width + '%';
}
}
Basically before you were erasing at each loop your previous elem, width, and id variables because they were declared outside the for loop. Hence the weird behavior.
Edit : putting the frame function outside so that it's clearer.
Edit2 : removing width from frame function as it is not needed.
Related
I have a class that takes some coordinate and duration data. I want to use it to animate an svg. In more explicit terms, I want to use that data to change svg attributes over a time frame.
I'm using a step function and requestAnimationFrame outside the class:
function step(timestamp) {
if (!start) start = timestamp
var progress = timestamp - start;
var currentX = parseInt(document.querySelector('#start').getAttribute('cx'));
var moveX = distancePerFrame(circleMove.totalFrames(), circleMove.xLine);
document.querySelector('#start').setAttribute('cx', currentX + moveX);
if (progress < circleMove.duration) {
window.requestAnimationFrame(step);
}
}
var circleMove = new SingleLineAnimation(3000, startXY, endXY)
var start = null
function runProgram() {
window.requestAnimationFrame(step);
}
I can make it a method, replacing the circleLine with this. That works fine for the first run through, but when it calls the this.step callback a second time, well, we're in a callback black hole and the reference to this is broken. Doing the old self = this won't work either, once we jump into the callback this is undefined(I'm not sure why). Here it is as a method:
step(timestamp) {
var self = this;
if (!start) start = timestamp
var progress = timestamp - start;
var currentX = parseInt(document.querySelector('#start').getAttribute('cx'));
var moveX = distancePerFrame(self.totalFrames(), self.xLine);
document.querySelector('#start').setAttribute('cx', currentX + moveX);
if (progress < self.duration) {
window.requestAnimationFrame(self.step);
}
}
Any ideas on how to keep the "wiring" inside the Object?
Here's the code that more or less works with the step function defined outside the class.
class SingleLineAnimation {
constructor(duration, startXY, endXY) {
this.duration = duration;
this.xLine = [ startXY[0], endXY[0] ];
this.yLine = [ startXY[1], endXY[1] ];
}
totalFrames(framerate = 60) { // Default to 60htz ie, 60 frames per second
return Math.floor(this.duration * framerate / 1000);
}
frame(progress) {
return this.totalFrames() - Math.floor((this.duration - progress) / 17 );
}
}
This will also be inserted into the Class, for now it's just a helper function:
function distancePerFrame(totalFrames, startEndPoints) {
return totalFrames > 0 ? Math.floor(Math.abs(startEndPoints[0] - startEndPoints[1]) / totalFrames) : 0;
}
And click a button to...
function runProgram() {
window.requestAnimationFrame(step);
}
You need to bind the requestAnimationFrame callback function to a context. The canonical way of doing this is like this:
window.requestAnimationFrame(this.step.bind(this))
but it's not ideal because you're repeatedly calling .bind and creating a new function reference over and over, once per frame.
If you had a locally scoped variable set to this.step.bind(this) you could pass that and avoid that continual rebinding.
An alternative is this:
function animate() {
var start = performance.now();
el = document.querySelector('#start');
// use var self = this if you need to refer to `this` inside `frame()`
function frame(timestamp) {
var progress = timestamp - start;
var currentX = parseInt(el.getAttribute('cx'));
var moveX = distancePerFrame(circleMove.totalFrames(), circleMove.xLine);
el.setAttribute('cx', currentX + moveX);
if (progress < circleMove.duration) {
window.requestAnimationFrame(frame);
}
}
window.requestAnimationFrame(frame);
}
i.e. you're setting up the initial state, and then doing the animation within a purely locally scoped function that's called pseudo-recursively by requestAnimationFrame.
NB: either version of the code will interact badly if you inadvertently call another function that initiates an animation at the same time.
I want to update my progress bar in html while function is running. Is there any way I can do this?
function animateProgressBar() {
var numIteration = 100;
for (var i = 1; i <= numIteration; i++) {
//do some calculations
width = (i/numIteration)*100;
htmlElement.style.width = width + '%';
}
}
The problem is that your code is blocking. Therefore the page isnt rendered, and the status bar doesnt change. You may use a setTimeout recursively:
function animateProgressBar(numIteration,index=1) {
width = (index/numIteration)*100;
htmlElement.style.width = width + '%';
if(index<numIteration) setTimeout(animateProgressBar,100,numIteration,index+1);
}
Start like this:
animateProgressBar(100); // from 1 to 100
Note that index=1 in the parameters is ES6, so just use it on modern browsers...
I'm trying to make a faux loading screen, and I need delays between loading messages of about 20-50ms or so so that people can actually see what's going on before it cuts to the initialized screen. The button that activates this goes to the following function:
function gameinit() {
for (k = 0; k <=1; k += 0.125) {
setTimeout(function () {
var nexttxt = "Loading... " + toString(100 * k) + "%"
}, 20);
displayupdate(nexttxt);
}
}
However this comes up as an incorrect syntax (on JSfiddle - https://jsfiddle.net/YoshiBoy13/xLn7wbg6/2/) when I use JShint - specifically lines four and five. I've looked at the guides for this and everything seems to be in order. What am I doing wrong?
(Note: displayupdate(nexttxt) updates the <p> tags with the next line of text)
When executing the script, nothing happens - the sixteen lines of text on the HTML move up as normal, the top eight being replaced with the eight generated by the gameinit() function, but the gameinit() only generates blank. If the script is executed again, it just outputs eight lines of 112.5% (as if it was the 9th iteration of the for loop).
I'm almost certain it's something elementary that I've missed, could someone please tell me what I've done wrong?
Use setInterval() instead, you can clear interval using clearInterval()
function gameinit() {
displayupdate("Loading... 0%");
var k = 0;
var inter = setInterval(function() {
if (k < 1) {
k += .25;
displayupdate("Loading... " + 100 * k + "%")
} else {
clearInterval(inter);
}
}, 2000);
}
function displayupdate(d) {
console.log(d);
}
gameinit();
here is another function can do this better ---- setInterval
var txt = '';
var time = 0;
var id = setInterval(function(){
console.log("loading..."+time/8*100+"%");
if(time++>7)
clearInterval(id);
},1000);
setTimeout doesn't work as you would expect it to work inside loops. You have to create a closure for each loop variable passed on to setTimeout, or create a new function to execute the setTimeout operation.
function gameinit() {
for (var k = 0; k <= 1; k += 0.125) {
doSetTimeOut(k);
}
}
function doSetTimeOut(k) {
setTimeout(function() {
var nexttxt = "Loading... " + toString(100 * k) + "%"
}, 20);
displayupdate(nexttxt);
}
This question already has answers here:
How do I add a delay in a JavaScript loop?
(32 answers)
Closed 9 years ago.
Experts.
Javascript not producing desired delay effect.
From other questions, on SO I got to know that, problem is with settimeout and the way I am using it.
But still I am not able to comprehend, how Settimeout works.
So I am putting code here.
Need to use Javascript only, because of knowledge purpose.
Actually I am trying to clear my concepts about this, closure in javascript.
Are they kind of twisted things of Javascript?
var objImg = new Object();
var h;
var w;
var no = 100;
while (no != 500) {
setTimeout(function () {
size(no, no);
}, 2000);
/* it's get executed once, instead of repeating with while loop
Does it leave loop in mid? I get image with 500px height and
width, but effect is not acheived.
*/
no = no + 50;
}
function size(h, w) {
var objImg = document.getElementsByName('ford').item(0);
objImg.style.height = h + 'px';
objImg.style.width = w + 'px';
}
You have two problems :
no will have the value of end of loop when the callback is called
you're programming all your timeouts 2000 ms from the same time, the time the loop run.
Here's how you could fix that :
var t = 0
while (no != 500) {
(function(no) {
t += 2000;
setTimeout(function() { size(no,no);} ,t);
})(no);
no = no+50; // could be written no += 50
}
The immediately executed function creates a scope which protects the value of no.
A little explanation about (function(no) { :
The scope of a variable is either
the global scope
a function
The code above could have been written as
var t = 0
while (no != 500) {
(function(no2) {
t += 2000;
setTimeout(function() { size(no2,no2);} ,t);
})(no);
no += 50;
}
Here it's probably more clear that we have two variables :
no, whose value changes with each iteration and is 500 when the timeouts are called
no2, in fact one variable no2 per call of the inner anonymous function
Each time the inner anonymous function is called, it declares a new no2 variable, whose value is no at the time of call (during iteration). This variable no2 is thus protected and is used by the callback given to setTimeout.
Why not just use setInterval() instead?
var objImg = new Object();
var h;
var w;
var no = 100;
var myInterval = window.setInterval(function() {
size(no, no);
no = no + 50;
if (no >= 500) clearInterval(myInterval);
}, 2000);
function size(h, w) {
var objImg = document.getElementsByName('ford').item(0);
objImg.style.height = h + 'px';
objImg.style.width = w + 'px';
}
Your problem is with your size() function syntax & algorithm:
var objImg = new Object();
var h;
var w;
var no = 100;
var int = window.setInterval(function () {
size(no,no);
no += 50;
},2000)
function size(h, w) {
if (h == 500){
window.clearInterval(int);
return;
}
var height = h + 'px';
var width = w + 'px';
document.getElementsByName('ford').item(0).style.height = height;
document.getElementsByName('ford').item(0).style.width = width;
}
http://jsfiddle.net/AQtNY/2/
I want to use setInterval to animate a couple things. First I'd like to be able to specify a series of page elements, and have them set their background color, which will gradually fade out. Once the color returns to normal the timer is no longer necessary.
So I've got
function setFadeColor(nodes) {
var x = 256;
var itvlH = setInterval(function () {
for (i in nodes) {
nodes[i].style.background-color = "rgb(0,"+(--x)+",0);";
}
if (x <= 0) {
// would like to call
clearInterval(itvlH);
// but itvlH isn't in scope...?
}
},50);
}
Further complicating the situation is I'd want to be able to have multiple instances of this going on. I'm thinking maybe I'll push the live interval handlers into an array and clean them up as they "go dead" but how will I know when they do? Only inside the interval closure do I actually know when it has finished.
What would help is if there was a way to get the handle to the interval from within the closure.
Or I could do something like this?
function intRun() {
for (i in nodes) {
nodes[i].style.background-color = "rgb(0,"+(--x)+",0);";
}
if (x <= 0) {
// now I can access an array containing all handles to intervals
// but how do I know which one is ME?
clearInterval(itvlH);
}
}
var handlers = [];
function setFadeColor(nodes) {
var x = 256;
handlers.push(setInterval(intRun,50);
}
Your first example will work fine and dandy ^_^
function setFadeColor(nodes) {
var x = 256;
var itvlH = setInterval(function () {
for (i in nodes) {
nodes[i].style.background-color = "rgb(0,"+(--x)+",0);";
}
if (x <= 0) {
clearInterval(itvlH);
// itvlH IS in scope!
}
},50);
}
Did you test it at all?
I've used code like your first block, and it works fine. Also this jsFiddle works as well.
I think you could use a little trick to store the handler. Make an object first. Then set the handler as a property, and later access the object's property. Like so:
function setFadeColor(nodes) {
var x = 256;
var obj = {};
// store the handler as a property of the object which will be captured in the closure scope
obj.itvlH = setInterval(function () {
for (i in nodes) {
nodes[i].style.background-color = "rgb(0,"+(--x)+",0);";
}
if (x <= 0) {
// would like to call
clearInterval(obj.itvlH);
// but itvlH isn't in scope...?
}
},50);
}
You can write helper function like so:
function createDisposableTimerInterval(closure, delay) {
var cancelToken = {};
var handler = setInterval(function() {
if (cancelToken.cancelled) {
clearInterval(handler);
} else {
closure(cancelToken);
}
}, delay);
return handler;
}
// Example:
var i = 0;
createDisposableTimerInterval(function(token) {
if (i < 10) {
console.log(i++);
} else {
// Don't need that timer anymore
token.cancelled = true;
}
}, 2000);