4
\$\begingroup\$

I have created a very basic animated percentage bar with HTML, CSS, and JS the only problem is that I am trying to devise a way to also animate the increase and/or decrease of the percentage output to go along with an animated percentage bar. In the example below and in this JsFiddle I have successfully created that with the only problem being that it doesn't seem to be the most efficient or effective way of doing it.

In the code snippet directly below I'm creating this animated effect by...

  1. Setting x equal to setInterval
  2. Capturing the width of percent bar on the left and removing the px from the end of the string.
  3. Capturing the width of percent bar on the right and removing the px from the end of the string.
  4. Displays the percent value for the left (blue) bar inside the tooltip that can be seen when hovered over.
  5. Displays the percent value for the right (red) bar inside the tooltip that can be seen when hovered over.
  6. Displays the percent value of the left (blue) bar below the percent bar.
  7. Displays the percent value of the right (red) bar below the percent bar.
  8. All of this code below will run every 64 Milliseconds.
  9. This code will only run for 2000 Milliseconds which is the same amount of time that I have set the transition for the percent bars.

Note: The whole point of the code below is to give the illusion that the percent values are increasing as either of the percent bars are increasing. In short, the goal is to make it seem more animated rather than the number all of a sudden seeing the number jump from one number to the next.

There just has to be a better way of achieving the same effect (or better) rather than pulling data from the DOM every 64 Milliseconds. There are tons of real-time graph's out on the web that achieve the same effect but I can't figure out how so I came up with my own and don't really think that they do it this way either. Any ideas??? I would only like to use pure Javascript with no libraries such as jQuery.

  var x = setInterval(function() {
    var left = parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) / (parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) + parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, ''))) * 100;
    var right = parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, '')) / (parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) + parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, ''))) * 100;
    p_bar_left.querySelector('.percent-value').innerText = left.toFixed(2) + '%';
    document.querySelector('#blue').querySelector('.percent-amount').innerText = left.toFixed(2) + '%';
    p_bar_right.querySelector('.percent-value').innerText = right.toFixed(2) + '%';
    document.querySelector('#red').querySelector('.percent-amount').innerText = right.toFixed(2) + '%';
  }, 64);
  setTimeout(function() {
    clearInterval(x)
  }, 2000);

var good = document.querySelector('#good');
var bad = document.querySelector('#bad');
var p_bar_left = document.querySelector('#progressbar-left');
var p_bar_right = document.querySelector('#progressbar-right');
var counter_left = 0;
var counter_right = 0;
var percent_left = 0;
var percent_right = 0;

function changePercent(increment, which) {
  if (which == 'left') {
    counter_left += increment;
  } else if (which == 'right') {
    counter_right += increment;
  } else {
    throw "Don't know which value to increase.";
  }
  percent_left = (counter_left / (counter_left + counter_right)) * 100;
  percent_right = (counter_right / (counter_left + counter_right)) * 100;
  p_bar_left.style.width = percent_left + '%';
  p_bar_right.style.width = percent_right + '%';
  document.querySelector('#total-amount').innerText = counter_right + counter_left;

  var x = setInterval(function() {
    var left = parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) / (parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) + parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, ''))) * 100;
    var right = parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, '')) / (parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, '')) + parseInt(window.getComputedStyle(p_bar_right).getPropertyValue('width').replace(/px/i, ''))) * 100;
    p_bar_left.querySelector('.percent-value').innerText = left.toFixed(2) + '%';
    document.querySelector('#blue').querySelector('.percent-amount').innerText = left.toFixed(2) + '%';
    p_bar_right.querySelector('.percent-value').innerText = right.toFixed(2) + '%';
    document.querySelector('#red').querySelector('.percent-amount').innerText = right.toFixed(2) + '%';
  }, 64);
  setTimeout(function() {
    clearInterval(x)
  }, 2000);
}

good.addEventListener('click', function() {
  changePercent(1, 'left');
});
bad.addEventListener('click', function() {
  changePercent(1, 'right');
});

var tooltip = document.querySelectorAll('.tooltip');
var tooltipelement = document.querySelectorAll('#progressbar-left, #progressbar-right');

for (var x = tooltipelement.length; x--;) {
  tooltipelement[x].addEventListener('mousemove', function(e) {
    for (var i = tooltip.length; i--;) {
      tooltip[i].style.left = e.pageX + 20 + 'px';
      tooltip[i].style.top = e.pageY + 'px';
    }
  });
}
#progressbar-container {
  display: flex;
  position: relative;
  width: 50vw;
  height: 32px;
  border: 2px solid black;
  background-color: #ccc;
  justify-content: space-between;
}

#progressbar-left {
  position: relative;
  height: 100%;
  background-color: blue;
  transition: width 2s;
  align-items: center;
  justify-content: center;
}

#progressbar-right {
  position: relative;
  height: 100%;
  background-color: red;
  transition: width 2s;
  align-items: center;
  justify-content: center;
}

.tooltip {
  display: none;
  position: fixed;
  width: auto;
  height: auto;
  padding: 6px;
  background-color: black;
  text-align: center;
  border-radius: 6px;
  z-index: 1;
}

.object {
  display: inline-block;
  color: #fff;
}

.percent-value {
  display: inline-block;
  color: #fff;
}

#progressbar-left:hover .tooltip {
  display: block;
}

#progressbar-right:hover .tooltip {
  display: block;
}

#total {
  display: block;
  font-weight: bold;
}

#total-amount {
  display: inline-block;
  font-weight: normal;
}

#blue,
#red {
  display: block;
  font-weight: bold;
}

.percent-amount {
  display: inline-block;
  font-weight: normal;
}
<body>
  <input type="button" value="Good" id="good">
  <input type="button" value="Bad" id="bad">
  <div id="progressbar-container">
    <div id="progressbar-left">
      <div class="tooltip">
        <span class="tooltiptext">
          <span class="object">Blue</span>
        <span class="percent-value"></span>
        </span>
      </div>
    </div>
    <div id="progressbar-right">
      <div class="tooltip">
        <span class="tooltiptext">
          <span class="object">Red</span>
        <span class="percent-value"></span>
        </span>
      </div>
    </div>
  </div>
  <span id="total">Total: <p id="total-amount">0</p></span>
  <span id="blue">Percent Blue: <p class="percent-amount">0%</p></span>
  <span id="red">Percent Red: <p class="percent-amount">0%</p></span>
</body>

JsFiddle

Note: This question was moved from stackoverflow

\$\endgroup\$
1
  • \$\begingroup\$ Maybe you should reverse engineer some of the completion bars that you like. \$\endgroup\$
    – chicks
    Apr 1, 2017 at 12:44

2 Answers 2

3
\$\begingroup\$

From my experience, the main problem I see is that you are having a hard time differentiating what is domain logic from view. As a consequence, you are giving responsibilities to your javascript code that make it more complicated.

In summary:

Prefer CSS to handle the view, and JS to handle the changes you want to make in your model (HTML).

The domain logic of your problem is reduced to:

$$Percentage\,of\,blue\,bar = 100 * \frac{good}{total}\quad(By\,rule\,of\,three)$$

And now with this percentage you can achieve anything you want as $$Percent\,of\,red\,bar = 100 - Percentage\,of\,blue\,bar$$

In the following example, I control this by using the HTML5 progress element

Codepen with Percentage Bar made using HTML5 progress element

Look that all logic comes down to:

  • When you press Good => increment good and total, then update the model.
  • When Bad is pressed => only increment total. Making a bad counter is unnecessary and complicates things.
  • To update the model => make the width of your bar to be the value returned by the mentioned formula. In my code; completed() function refers to the formula.

The most complex case in this scenario, is to calculate the difference between last percentage, however, it is not required in any way to solve your problem.

Now, time for the logic of the view. CSS allows to make animations of this type using for example; transition property. In the following code I only tweked the progress bar like the one you presented:

Codepen with the expected Percentage bar

Note that the only property responsible for all the logic is

transition: width .3s ease-out;

Being:

  • width: the value that represents the percentage. The one you use to change it from js
  • .3s (or 0.3s): is the time I want the animation to last
  • ease-out: is one of the standard animations provided.

In case you want to handle the animation using only js, the common scenario to do it, would be in case of drawing directly on screen, like using the canvas element (Specially used when developing games). However, developing this logic is quite more complex and unnecessary for most cases, since you have to take care of available hardware resources where js code will be executed.

This is the approximation which you tried to solve the problem, and if you still have curiosity: Yes there is a better way to solve it (And probably, always exist better ways to solve common things). However you have to take into account that generally when drawing directly on the screen, one decides the size of the elements, reason why it simplifies the problem enough since now you would know the maximum width of your progress bar.

Knowing this, the logic of the view would now be something more like:

  • Starting a loop that lasts the time you expect will be your animation length (.3s is the time I used). This can be achieved in several ways, with setInterval, setTimeout, using classic loops referencing the current time, or preferably using window.requestAnimationFrame that is particularly developed for this task.
  • And inside the loop, you only have to change the width of the rectangle that would represent the element of the bar, making the product of the maximum width of the progress bar with the percentage already calculated (good/total)

Edit: I missunderstood the question, so i continued here:

I can't think a pure CSS solution for this, but if exist, it would be better than this.

This is how i could do the same, but using the last approach.

Preview

However @blackmiaool pointed most corrections, but one thing i tough was important and not pointed:

element.addEventListener('transitionend', function() {
  //Logic to stop your animation. Like showing your final expected value
});

If you use this, you can decouple your animation time, so, you can change your CSS transition time to whatever without changing your code.

PD: I'm not english speaker, so, i will be glad if someone can correct my mistakes :)

\$\endgroup\$
5
  • \$\begingroup\$ I do like the way that you made your progress bar and I will probably make mine resemble yours in the inner workings. But it still doesn't solve the animation of the percent counter. \$\endgroup\$
    – Alex Lowe
    Apr 2, 2017 at 16:44
  • \$\begingroup\$ Oh oh!!! So Sorry!!! I don't know how i misunderstood your question! Well, i don't know how to solve this only with CSS, but i will update my answer in the next minutes solving this with the last approach. \$\endgroup\$
    – Francute
    Apr 3, 2017 at 8:40
  • \$\begingroup\$ No you can't simply use that. 1: what if the user click the button again before the transitionend? 2: the event won't be triggerred after the first click. I added the listener to my code for debugging, you can test it. \$\endgroup\$ Apr 4, 2017 at 5:16
  • \$\begingroup\$ @blackmiaool, I tough, if user clicks the button again before transitionend fire, the event will trigger only for the last click, so, the animation will continue until it reachs the desired value, because is based on what is rendered, and ends based on your model to be more precise. However, i don't know if transitionend implementation differ on browsers or not. \$\endgroup\$
    – Francute
    Apr 4, 2017 at 19:35
  • \$\begingroup\$ @Francute If you read the setTimeout section of my answer.... If you actually try it, you would find it's not that simple. \$\endgroup\$ Apr 5, 2017 at 3:01
1
\$\begingroup\$

innerText

Difference between text content vs inner text

querySelector

Instead of

document.querySelector('#red').querySelector('.percent-amount');

I'd use

document.querySelector('#red .percent-amount');

And as I mentioned in this answer, you can write a shorthand for it.

var $ = document.querySelector.bind(document);
$('#red .percent-amount');

setInterval 64ms

Nowadays we use requestAnimationFrame for this propose.

parseInt(window.getComputedStyle(p_bar_left).getPropertyValue('width').replace(/px/i, ''))

Since the only variable in this verbose snippet is p_bar_left, I'd write a function for it

function width(dom) {
        return parseInt(window.getComputedStyle(dom)
                              .getPropertyValue('width')
                              .replace(/px/i, ''));
}

calculation of left and right

The right is actually 100 - left;

setTimeout

Since you don't prevent use clicking the buttons frequently, the new changePercent may be invoked before the end of the old one. In this condition, there would be two setInterval work in the browser. I add a stop function as return of your changePercent to solve the problem.

querySelector in interval

querySelector occupies cpu. Since the doms you are using don't change, just find them once.

var $ = document.querySelector.bind(document);

var good = $('#good');
var bad = $('#bad');
var p_bar_left = $('#progressbar-left');
var p_bar_right = $('#progressbar-right');
var counter_left = 0;
var counter_right = 0;
var percent_left = 0;
var percent_right = 0;



var blueAmount = $('#blue .percent-amount');
var redAmount = $('#red .percent-amount');
var percentLeft = $('#progressbar-left .percent-value');
var percentRight = $('#progressbar-right .percent-value');
var totalAmount=$('#total-amount');

p_bar_left.addEventListener("transitionend",function(){//for debugging, ignore it
    console.log("transitionend");
});
function changePercent(increment, which) {
    if (which == 'left') {
        counter_left += increment;
    } else if (which == 'right') {
        counter_right += increment;
    } else {
        throw "Don't know which value to increase.";
    }
    percent_left = (counter_left / (counter_left + counter_right)) * 100;
    percent_right = (counter_right / (counter_left + counter_right)) * 100;
    p_bar_left.style.width = percent_left + '%';
    p_bar_right.style.width = percent_right + '%';
    totalAmount.textContent = counter_right + counter_left;

    var updating = true;

    function updateWidth() {
        if (!updating) {
            //ensure the result is correct
            updatePercent(percent_left.toFixed(2) + '%', percent_right.toFixed(2) + '%');
            return;
        }

        function width(dom) {
            return parseInt(window.getComputedStyle(dom).getPropertyValue('width').replace(/px/i, ''))
        }

        var left = width(p_bar_left) / (width(p_bar_left) + width(p_bar_right)) * 100;
        var right = 100 - left;

        function updatePercent(left, right) {
            blueAmount.textContent = left;
            redAmount.textContent = right;
            percentLeft.textContent = leftFixed;
            percentRight.textContent = rightFixed;
        }
        var leftFixed = left.toFixed(2) + '%';
        var rightFixed = right.toFixed(2) + '%';
        updatePercent(leftFixed, rightFixed);

        requestAnimationFrame(updateWidth);
    }
    requestAnimationFrame(updateWidth);
    setTimeout(function () {
        updating = false;
    }, 2000);
    return function stop() {
        updating = false;
    }
}

var stop;
good.addEventListener('click', function () {
    stop && stop();
    stop = changePercent(1, 'left');
});
bad.addEventListener('click', function () {
    stop && stop();
    stop = changePercent(1, 'right');
});

var tooltip = document.querySelectorAll('.tooltip');
var tooltipelement = document.querySelectorAll('#progressbar-left, #progressbar-right');

for (var x = tooltipelement.length; x--;) {
    tooltipelement[x].addEventListener('mousemove', function (e) {
        for (var i = tooltip.length; i--;) {
            tooltip[i].style.left = e.pageX + 20 + 'px';
            tooltip[i].style.top = e.pageY + 'px';
        }
    });
}
#progressbar-container {
  display: flex;
  position: relative;
  width: 50vw;
  height: 32px;
  border: 2px solid black;
  background-color: #ccc;
  justify-content: space-between;
}

#progressbar-left {
  position: relative;
  height: 100%;
  background-color: blue;
  transition: width 2s;
  align-items: center;
  justify-content: center;
}

#progressbar-right {
  position: relative;
  height: 100%;
  background-color: red;
  transition: width 2s;
  align-items: center;
  justify-content: center;
}

.tooltip {
  display: none;
  position: fixed;
  width: auto;
  height: auto;
  padding: 6px;
  background-color: black;
  text-align: center;
  border-radius: 6px;
  z-index: 1;
}

.object {
  display: inline-block;
  color: #fff;
}

.percent-value {
  display: inline-block;
  color: #fff;
}

#progressbar-left:hover .tooltip {
  display: block;
}

#progressbar-right:hover .tooltip {
  display: block;
}

#total {
  display: block;
  font-weight: bold;
}

#total-amount {
  display: inline-block;
  font-weight: normal;
}

#blue,
#red {
  display: block;
  font-weight: bold;
}

.percent-amount {
  display: inline-block;
  font-weight: normal;
}
<body>
  <input type="button" value="Good" id="good">
  <input type="button" value="Bad" id="bad">
  <div id="progressbar-container">
    <div id="progressbar-left">
      <div class="tooltip">
        <span class="tooltiptext">
          <span class="object">Blue</span>
        <span class="percent-value"></span>
        </span>
      </div>
    </div>
    <div id="progressbar-right">
      <div class="tooltip">
        <span class="tooltiptext">
          <span class="object">Red</span>
        <span class="percent-value"></span>
        </span>
      </div>
    </div>
  </div>
  <span id="total">Total: <p id="total-amount">0</p></span>
  <span id="blue">Percent Blue: <p class="percent-amount">0%</p></span>
  <span id="red">Percent Red: <p class="percent-amount">0%</p></span>
</body>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.