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
- Paste the snippet just before
</head>. - Add
data-pie-slice-pop-outto a container with a height, with one child per slice: its text is the name anddata-valuethe figure.
Options
Add any of these attributes to the same element.
data-colors | Comma-separated colours, used in turn. Default purple, pink, blue… |
data-color | On a slice: its colour. |
data-prefix / data-suffix | Text around the figures in the legend, e.g. $. |
data-legend | false hides the legend. |
data-offset | How far a slice slides out, as a share of the radius. Default 0.12. |
data-duration | Time for a slice to slide out. Default 450 ms. |
data-autoplay | Pop 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.
<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><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>