← Charts

Pie Slice Pop-out

Point at a slice and it slides out of the pie on a soft shadow while the others dim, and its legend key lights up with the figure. Tap a slice or a key on a phone, and with autoplay the slices take turns until someone points.

RentFoodTravelBillsFunSavings

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-pie-slice-pop-out to a container with a height, with one child per slice: its text is the name and data-value the figure.

Options

Add any of these attributes to the same element.

data-colorsComma-separated colours, used in turn. Default purple, pink, blue…
data-colorOn a slice: its colour.
data-prefix / data-suffixText around the figures in the legend, e.g. $.
data-legendfalse hides the legend.
data-offsetHow far a slice slides out, as a share of the radius. Default 0.12.
data-durationTime for a slice to slide out. Default 450 ms.
data-autoplayPop each slice in turn until someone points at one.

Slices show their share of the whole inside them when there’s room. The legend keys are buttons, so it works from the keyboard. From a script: animacious.pieSlicePopOut(el, 2), or -1 to put it back.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Pie Slice Pop-out: a pie chart whose slices slide out from the centre
    // when you point at them (or tap them, or their legend key), with a soft
    // shadow beneath, while the rest of the pie dims a little.
    // Usage:
    //   <div data-pie-slice-pop-out data-suffix="%" style="height:240px">
    //     <span data-value="45">Rent</span>
    //     <span data-value="25">Food</span>
    //     <span data-value="30">Other</span>
    //   </div>
    // Each child is one slice, clockwise from twelve o'clock: its text is the
    // name. Each slice shows its share of the whole inside it.
    // Optional: data-colors (comma-separated, default purple, pink, blue…),
    // data-color on a slice, data-prefix / data-suffix (around the values in
    // the legend), data-legend="false" (no legend), data-offset (how far a
    // slice slides out, as a share of the radius, default 0.12),
    // data-duration (ms for a slice to slide, default 450), data-autoplay
    // (pop each slice in turn until someone points at one).
    // Script use: animacious.pieSlicePopOut(el, index) pops a slice (-1 for none).
    var DEFAULTS = { duration: 450, offset: 0.12, interval: 1700, idle: 4000 };
    var PALETTE = ['#a855f7', '#ec4899', '#0ea5e9', '#f59e0b', '#10b981', '#6366f1', '#f43f5e', '#14b8a6'];
    var NS = 'http://www.w3.org/2000/svg';

    var css =
      '.psp{display:flex;align-items:center;justify-content:center;gap:14px 22px;font-size:12px;font-variant-numeric:tabular-nums}' +
      '.psp-below{flex-direction:column}' +
      '.psp svg{display:block;flex:none;overflow:visible}' +
      '.psp-slice{cursor:pointer;transition:opacity .3s}' +
      '.psp-slice path{stroke:#fff;stroke-width:1.5;stroke-linejoin:round}' +
      '.psp-on .psp-slice{opacity:.55}' +
      '.psp-on .psp-slice.psp-act{opacity:1;filter:drop-shadow(0 6px 8px rgba(30,10,60,.28))}' +
      '.psp-pct{fill:#fff;font-weight:700;text-anchor:middle;pointer-events:none}' +
      '.psp-legend{display:flex;flex-direction:column;gap:3px;min-width:0}' +
      '.psp-below .psp-legend{flex-direction:row;flex-wrap:wrap;justify-content:center;gap:2px 6px}' +
      '.psp-key{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:7px;padding:3px 8px;border-radius:8px;cursor:pointer;color:inherit;font:inherit;white-space:nowrap;transition:background .25s,opacity .25s}' +
      '.psp-key:focus-visible{outline:2px solid currentColor;outline-offset:1px}' +
      '.psp-key i{width:10px;height:10px;border-radius:3px;flex:none;background:var(--psp-c)}' +
      '.psp-key b{font-weight:700;margin-left:auto;padding-left:8px}' +
      '.psp-on .psp-key{opacity:.55}' +
      '.psp-on .psp-key.psp-act{opacity:1;background:rgba(127,127,127,.12)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function mk(tag, attrs, parent) {
      var n = document.createElementNS(NS, tag);
      for (var k in attrs) n.setAttribute(k, attrs[k]);
      if (parent) parent.appendChild(n);
      return n;
    }

    function setup(el) {
      var d = el.dataset;
      var cols = d.colors ? d.colors.split(',') : PALETTE;
      var slices = [];
      Array.prototype.forEach.call(el.children, function (s) {
        var v = parseFloat(s.getAttribute('data-value'));
        if (!(v > 0)) return;
        slices.push({ name: s.textContent.trim(), v: v, color: s.getAttribute('data-color') || cols[slices.length % cols.length].trim(), k: 0 });
      });
      if (!slices.length) return null;
      var total = slices.reduce(function (a, s) { return a + s.v; }, 0);
      var pre = d.prefix || '', suf = d.suffix || '';
      var thumb = !!el.closest('[aria-hidden="true"]');
      var c = {
        el: el, slices: slices, total: total, on: -1, thumb: thumb,
        offset: d.offset !== undefined ? +d.offset : DEFAULTS.offset,
        dur: +d.duration || DEFAULTS.duration,
        autoplay: d.autoplay !== undefined && d.autoplay !== 'false'
      };
      el.classList.add('psp');
      el.innerHTML = '';
      var a = -Math.PI / 2;
      slices.forEach(function (s) {
        s.a0 = a; a += s.v / total * Math.PI * 2; s.a1 = a;
        s.mid = (s.a0 + s.a1) / 2;
        s.pct = Math.round(s.v / total * 100) + '%';
      });
      c.svg = mk('svg', { 'aria-hidden': 'true' }, el);
      c.svg.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') user(c, -1); });
      if (d.legend !== 'false') {
        c.legend = document.createElement('div');
        c.legend.className = 'psp-legend';
        el.appendChild(c.legend);
        slices.forEach(function (s, i) {
          var k = document.createElement(thumb ? 'span' : 'button');
          if (!thumb) { k.type = 'button'; k.setAttribute('aria-pressed', 'false'); }
          k.className = 'psp-key';
          k.style.setProperty('--psp-c', s.color);
          k.innerHTML = '<i></i>';
          k.appendChild(document.createTextNode(s.name));
          var b = document.createElement('b');
          b.textContent = pre + s.v.toLocaleString('en-GB') + suf;
          k.appendChild(b);
          k.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') user(c, i); });
          k.addEventListener('click', function () { user(c, c.on === i ? -1 : i); });
          c.legend.appendChild(k);
          s.key = k;
        });
        c.legend.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') user(c, -1); });
      }
      el.setAttribute('role', 'figure');
      el.setAttribute('aria-label', 'Pie chart. ' + slices.map(function (s) { return s.name + ' ' + pre + s.v.toLocaleString('en-GB') + suf + ' (' + s.pct + ')'; }).join(', ') + '.');
      document.addEventListener('pointerdown', function (e) { if (c.on >= 0 && !el.contains(e.target)) user(c, -1); });
      el._psp = c;
      layout(c);
      return c;
    }

    function arc(cx, cy, r, a0, a1) {
      if (a1 - a0 > Math.PI * 2 - 1e-4) {
        return 'M' + (cx - r) + ',' + cy + 'A' + r + ',' + r + ' 0 1 1 ' + (cx + r) + ',' + cy + 'A' + r + ',' + r + ' 0 1 1 ' + (cx - r) + ',' + cy + 'Z';
      }
      var large = a1 - a0 > Math.PI ? 1 : 0;
      return 'M' + cx + ',' + cy + 'L' + (cx + r * Math.cos(a0)) + ',' + (cy + r * Math.sin(a0)) +
        'A' + r + ',' + r + ' 0 ' + large + ' 1 ' + (cx + r * Math.cos(a1)) + ',' + (cy + r * Math.sin(a1)) + 'Z';
    }

    function layout(c) {
      var el = c.el, W = el.clientWidth, H = el.clientHeight;
      if (!W || !H) return;
      c.W = W; c.H = H;
      var side = !c.legend || (W >= H * 1.5 && W >= 250);
      el.classList.toggle('psp-below', !side);
      var S;
      if (!c.legend) S = Math.min(W, H);
      else if (side) S = Math.min(H, W - c.legend.offsetWidth - 22);
      else S = Math.min(W, H - c.legend.offsetHeight - 14);
      S = Math.max(60, Math.floor(S));
      c.svg.setAttribute('width', S);
      c.svg.setAttribute('height', S);
      c.svg.setAttribute('viewBox', '0 0 ' + S + ' ' + S);
      c.svg.innerHTML = '';
      var cx = S / 2, R = S / 2 / (1 + c.offset + 0.03);
      c.cx = cx; c.R = R;
      var fs = Math.max(9, Math.min(14, R / 6));
      c.slices.forEach(function (s, i) {
        var g = mk('g', { class: 'psp-slice' }, c.svg);
        mk('path', { d: arc(cx, cx, R, s.a0, s.a1), fill: s.color }, g);
        if (s.a1 - s.a0 > 0.38) {
          mk('text', { class: 'psp-pct', x: cx + Math.cos(s.mid) * R * 0.64, y: cx + Math.sin(s.mid) * R * 0.64, dy: '.35em', 'font-size': fs }, g).textContent = s.pct;
        }
        g.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') user(c, i); });
        g.addEventListener('click', function () { user(c, c.on === i ? -1 : i); });
        s.g = g;
      });
      render(c);
    }

    function render(c) {
      if (!c.R) return;
      c.slices.forEach(function (s) {
        var dist = c.R * c.offset * s.k, sc = 1 + 0.035 * s.k;
        var tx = c.cx + Math.cos(s.mid) * dist, ty = c.cx + Math.sin(s.mid) * dist;
        s.g.setAttribute('transform', 'translate(' + tx + ',' + ty + ') scale(' + sc + ') translate(' + -c.cx + ',' + -c.cx + ')');
      });
    }

    function pop(c, i) {
      if (i === c.on) return;
      c.on = i;
      c.el.classList.toggle('psp-on', i >= 0);
      c.slices.forEach(function (s, j) {
        var act = j === i;
        if (s.g) s.g.classList.toggle('psp-act', act);
        if (s.key) {
          s.key.classList.toggle('psp-act', act);
          if (!c.thumb) s.key.setAttribute('aria-pressed', act ? 'true' : 'false');
        }
        if (s.anim) s.anim.pause();
        var target = act ? 1 : 0;
        if (reduced()) { s.k = target; return; }
        s.anim = anime.animate(s, {
          k: target, duration: act ? c.dur : c.dur * 0.8, ease: act ? 'outBack(1.8)' : 'out(3)',
          onUpdate: function () { render(c); }
        });
      });
      render(c);
    }

    function user(c, i) {
      c.userAt = Date.now();
      pop(c, i);
    }

    function tick(c) {
      c.timer = null;
      if (!c.visible) return;
      if (!c.userAt || Date.now() - c.userAt > DEFAULTS.idle) {
        if (c.userAt && c.on >= 0) { c.userAt = 0; }
        c.auto = (c.auto === undefined ? -1 : c.auto) + 1;
        if (c.auto > c.slices.length) c.auto = 0;
        pop(c, c.auto === c.slices.length ? -1 : c.auto);
      }
      c.timer = setTimeout(function () { tick(c); }, DEFAULTS.interval);
    }

    function intro(c) {
      anime.animate(c.svg, { opacity: [0, 1], scale: [0.7, 1], rotate: [-40, 0], duration: 900, ease: 'out(4)' });
      if (c.legend) anime.animate(c.legend.children, { opacity: [0, 1], x: [10, 0], duration: 500, delay: anime.stagger(60, { start: 250 }), ease: 'out(3)' });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var c = e.target._psp;
          c.visible = e.isIntersecting;
          if (!c.visible) return;
          if (!c.seen) { c.seen = true; if (!reduced()) intro(c); }
          if (c.autoplay && !reduced() && !c.timer) c.timer = setTimeout(function () { tick(c); }, 1100);
        });
      }, { threshold: 0.3 });
      var ro = new ResizeObserver(function (entries) {
        entries.forEach(function (e) {
          var c = e.target._psp;
          if (c && (e.target.clientWidth !== c.W || e.target.clientHeight !== c.H)) layout(c);
        });
      });
      document.querySelectorAll('[data-pie-slice-pop-out]').forEach(function (el) {
        if (el._psp) return;
        var c = setup(el);
        if (!c) return;
        ro.observe(el);
        io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.pieSlicePopOut = function (el, i) { if (el && el._psp) user(el._psp, i); };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-pie-slice-pop-out data-prefix="$" style="height: 240px">
  <span data-value="1200">Rent</span>
  <span data-value="520">Food</span>
  <span data-value="340">Travel</span>
  <span data-value="260">Bills</span>
</div>