← Charts

Bar Grow

A bar chart whose bars grow up from the axis one after another, overshoot a touch and settle, while their values count up above them.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-bar-grow to a container with a height, and one child per bar with data-value and data-label.

Options

Add any of these attributes to the same element.

data-colorBar colour, on the chart or on one bar. Default #a855f7.
data-maxTop of the scale. Default: the largest value.
data-suffixText after each value, e.g. %.
data-loopDrop and regrow every few seconds.
data-durationTime for each bar to grow. Default 1100 ms.
data-staggerGap between bars. Default 90 ms.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Bar Grow: a bar chart whose bars grow up from the axis one after another,
    // overshoot a touch and settle, while their values count up above them.
    // Usage:
    //   <div data-bar-grow style="height: 220px">
    //     <div data-value="42" data-label="Mon"></div>
    //     <div data-value="68" data-label="Tue"></div> …
    //   </div>
    // Optional: data-color on the chart or on a bar (default #a855f7), data-max
    // (top of the scale; default is the largest value), data-suffix (e.g. "%"),
    // data-duration, data-stagger (ms), data-loop (drop and regrow every few seconds).
    var DEFAULTS = { color: '#a855f7', duration: 1100, stagger: 90, hold: 2600 };

    var css =
      '.bgr{display:flex;align-items:stretch;gap:0.6em;font-size:13px}' +
      '.bgr-col{flex:1;display:flex;flex-direction:column;align-items:stretch;min-width:0}' +
      '.bgr-track{flex:1;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}' +
      '.bgr-val{text-align:center;font-weight:600;font-variant-numeric:tabular-nums;margin-bottom:4px}' +
      '.bgr-bar{border-radius:6px 6px 2px 2px;transform-origin:50% 100%}' +
      '.bgr-label{text-align:center;opacity:.65;margin-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function build(el) {
      var d = el.dataset;
      var items = Array.from(el.children).map(function (c) {
        return { value: +c.dataset.value || 0, label: c.dataset.label || '', color: c.dataset.color };
      });
      var max = +d.max || Math.max.apply(null, items.map(function (i) { return i.value; })) || 1;
      el.classList.add('bgr');
      el.setAttribute('role', 'img');
      el.setAttribute('aria-label', items.map(function (i) { return (i.label ? i.label + ': ' : '') + i.value + (d.suffix || ''); }).join(', '));
      el.textContent = '';
      return items.map(function (it) {
        var col = document.createElement('div');
        col.className = 'bgr-col';
        col.setAttribute('aria-hidden', 'true');
        var track = document.createElement('div');
        track.className = 'bgr-track';
        var val = document.createElement('div');
        val.className = 'bgr-val';
        val.textContent = it.value + (d.suffix || '');
        var bar = document.createElement('div');
        bar.className = 'bgr-bar';
        bar.style.height = (it.value / max) * 100 + '%';
        bar.style.background = it.color || d.color || DEFAULTS.color;
        track.appendChild(val);
        track.appendChild(bar);
        col.appendChild(track);
        if (it.label) {
          var lab = document.createElement('div');
          lab.className = 'bgr-label';
          lab.textContent = it.label;
          col.appendChild(lab);
        }
        el.appendChild(col);
        return { bar: bar, val: val, value: it.value };
      });
    }

    function grow(el, bars) {
      var d = el.dataset;
      var duration = +d.duration || DEFAULTS.duration;
      var stagger = +d.stagger || DEFAULTS.stagger;
      var suffix = d.suffix || '';
      bars.forEach(function (b, i) {
        anime.animate(b.bar, { scaleY: [0, 1], duration: duration, delay: i * stagger, ease: 'outBack(1.5)' });
        var counter = { n: 0 };
        anime.animate(b.val, { opacity: [0, 1], duration: 300, delay: i * stagger });
        anime.animate(counter, {
          n: b.value,
          duration: duration * 0.8,
          delay: i * stagger,
          ease: 'out(3)',
          onUpdate: function () { b.val.textContent = Math.round(counter.n) + suffix; }
        });
      });
      if (d.loop !== undefined && d.loop !== 'false') {
        setTimeout(function () {
          anime.animate(bars.map(function (b) { return b.bar; }), {
            scaleY: 0, duration: 400, delay: anime.stagger(40), ease: 'in(2)',
            onComplete: function () { grow(el, bars); }
          });
          anime.animate(bars.map(function (b) { return b.val; }), { opacity: 0, duration: 300 });
        }, duration + bars.length * stagger + DEFAULTS.hold);
      }
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          grow(e.target, e.target._bgr);
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-bar-grow]').forEach(function (el) {
        if (el._bgr) return;
        var bars = (el._bgr = build(el));
        if (reduced) return;
        bars.forEach(function (b) { anime.utils.set(b.bar, { scaleY: 0 }); b.val.style.opacity = 0; });
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-bar-grow data-suffix="%" style="height: 220px">
  <div data-value="42" data-label="Mon"></div>
  <div data-value="68" data-label="Tue"></div>
  <div data-value="55" data-label="Wed"></div>
</div>