Venn Diagram
Two or three see-through circles slide together from apart, their names fading in, and then the patch where they all overlap lights up with its own label.
DesignCode
Love itGood at itPaid for it
How to use it
- Paste the snippet just before
</head>. - Add
data-venn-diagramto a container with two or three children, one per circle. - Add
data-overlapto label the part where they all meet.
Options
Add any of these attributes to the same element.
data-overlap | Label for the lit overlap. |
data-colors | Comma-separated circle colours. Default purple, pink, blue. |
data-glow | Colour of the lit overlap. Default #7c3aed. |
data-duration | Time for the circles to slide in. Default 1100 ms. |
data-loop | Slide apart and together again. |
It scales to the width of its container; give that a max-width to size it.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Venn Diagram: two or three see-through circles slide together from
// apart, their names fading in, and then the patch where they all overlap
// lights up with its own label and a little flash.
// Usage:
// <div data-venn-diagram data-overlap="Sweet spot">
// <span>Design</span><span>Code</span>
// </div>
// Two children make two circles side by side; three make a triangle of
// circles, lighting the middle where all three meet.
// Optional: data-overlap (label for the lit overlap), data-colors (comma-
// separated circle colours), data-glow (colour of the lit overlap, default
// #7c3aed), data-duration (ms for the circles to slide in, default 1100),
// data-loop (slide apart and together again).
var DEFAULTS = { colors: ['#a855f7', '#ec4899', '#3b82f6'], glow: '#7c3aed', duration: 1100, hold: 2400, ink: '#1e1b2e' };
var NS = 'http://www.w3.org/2000/svg';
var count = 0;
var css =
'.vnd{display:flex;align-items:center;justify-content:center}' +
'.vnd-svg{display:block;width:100%;height:auto;max-height:100%}' +
'.vnd-label{font-weight:700}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
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;
}
// Centred text; splits onto two lines if it has a space and is long.
function text(parent, str, x, y, size, fill, wrapAt) {
var t = mk('text', { class: 'vnd-label', x: x, y: y, 'text-anchor': 'middle', 'dominant-baseline': 'central', 'font-size': size, fill: fill }, parent);
var words = str.split(' ');
if (str.length > (wrapAt || 10) && words.length > 1) {
// Break where the two lines come out most even.
var half = 1, best = Infinity;
for (var i = 1; i < words.length; i++) {
var m = Math.max(words.slice(0, i).join(' ').length, words.slice(i).join(' ').length);
if (m < best) { best = m; half = i; }
}
mk('tspan', { x: x, dy: '-0.6em' }, t).textContent = words.slice(0, half).join(' ');
mk('tspan', { x: x, dy: '1.2em' }, t).textContent = words.slice(half).join(' ');
} else t.textContent = str;
return t;
}
function build(el) {
var d = el.dataset;
var names = Array.from(el.children).map(function (c) { return c.textContent.trim(); }).slice(0, 3);
var three = names.length === 3;
var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }) : DEFAULTS.colors;
var id = 'vnd-' + ++count;
var W = 320, H = three ? 266 : 170, r = three ? 74 : 76, cx = W / 2, cy = three ? 148 : H / 2;
var spread = three ? 44 : 40, apart = three ? 28 : 38;
var dirs = three
? [[0, -1], [Math.cos(Math.PI / 6), Math.sin(Math.PI / 6)], [-Math.cos(Math.PI / 6), Math.sin(Math.PI / 6)]]
: [[-1, 0], [1, 0]];
el.setAttribute('role', 'img');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Venn diagram: ' + names.join(', ') + (d.overlap ? '; where they overlap: ' + d.overlap : ''));
el.textContent = '';
el.classList.add('vnd');
var svg = mk('svg', { class: 'vnd-svg', viewBox: '0 0 ' + W + ' ' + H, 'aria-hidden': 'true' }, el);
var defs = mk('defs', {}, svg);
var circles = [], labels = [];
dirs.forEach(function (dir, i) {
var x = cx + dir[0] * spread, y = cy + dir[1] * spread;
var clip = mk('clipPath', { id: id + '-' + i }, defs);
mk('circle', { cx: x, cy: y, r: r }, clip);
var g = mk('g', {}, svg);
mk('circle', { cx: x, cy: y, r: r, fill: colors[i % colors.length], 'fill-opacity': 0.28, stroke: colors[i % colors.length], 'stroke-width': 2 }, g);
circles.push({ g: g, dx: dir[0] * apart, dy: dir[1] * apart });
// Name in the part of the circle nobody else covers.
var off = three ? r * 0.5 : r * 0.5;
labels.push(text(svg, names[i], x + dir[0] * off, y + dir[1] * off, three ? 14 : 15, DEFAULTS.ink));
});
// The overlap: the last circle, clipped by each of the others.
var lens = mk('g', {}, svg);
var inner = lens;
for (var i = 0; i < dirs.length - 1; i++) inner = mk('g', { 'clip-path': 'url(#' + id + '-' + i + ')' }, inner);
var last = dirs.length - 1;
mk('circle', { cx: cx + dirs[last][0] * spread, cy: cy + dirs[last][1] * spread, r: r, fill: d.glow || DEFAULTS.glow }, inner);
var mid = d.overlap ? text(svg, d.overlap, cx, cy, three ? 12 : 13, '#fff', 7) : null;
return { circles: circles, labels: labels, lens: lens, mid: mid, loop: on(d.loop), done: false, visible: false };
}
function hide(s) {
s.circles.forEach(function (c) { anime.utils.set(c.g, { x: c.dx, y: c.dy, opacity: 0 }); });
anime.utils.set(s.labels, { opacity: 0 });
anime.utils.set(s.lens, { opacity: 0 });
if (s.mid) anime.utils.set(s.mid, { opacity: 0 });
}
function start(el) {
var s = el._vnd;
var dur = +el.dataset.duration || DEFAULTS.duration;
hide(s);
var tl = anime.createTimeline({
onComplete: function () {
if (s.tl !== tl) return;
if (s.loop) start(el); else s.done = true;
}
});
s.tl = tl;
s.circles.forEach(function (c, i) {
tl.add(c.g, { x: [c.dx, 0], y: [c.dy, 0], duration: dur, ease: 'inOut(3)' }, i * 60);
tl.add(c.g, { opacity: [0, 1], duration: dur * 0.4, ease: 'out(2)' }, i * 60);
tl.add(s.labels[i], { opacity: [0, 1], duration: 400 }, i * 60 + dur * 0.3);
});
var t = dur + s.circles.length * 60;
tl.add(s.lens, { opacity: [0, 1, 0.55, 1], duration: 900, ease: 'out(2)' }, t);
if (s.mid) tl.add(s.mid, { opacity: [0, 1], duration: 400, ease: 'out(2)' }, t + 250);
if (s.loop) {
var back = t + 900 + DEFAULTS.hold;
var fade = [s.lens].concat(s.labels);
if (s.mid) fade.push(s.mid);
tl.add(fade, { opacity: 0, duration: 350 }, back);
s.circles.forEach(function (c) {
tl.add(c.g, { x: c.dx, y: c.dy, opacity: 0, duration: 700, ease: 'in(2)' }, back + 200);
});
}
}
function init() {
var still = reduced();
document.querySelectorAll('[data-venn-diagram]').forEach(function (el) {
if (el._vnd) return;
var s = (el._vnd = build(el));
if (still) return;
hide(s);
new IntersectionObserver(function (entries) {
s.visible = entries[0].isIntersecting;
if (s.visible) { if (!s.tl) start(el); else if (!s.done) s.tl.resume(); }
else if (s.tl && !s.done) s.tl.pause();
}, { threshold: 0.2 }).observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-venn-diagram data-overlap="Sweet spot" style="max-width: 360px">
<span>Design</span>
<span>Code</span>
</div>