Request Flow Diagram
A request travels through your system. A glowing packet hops from service to service, each one lighting up with its timing as the request arrives, then the response runs all the way back in green and the first box shows 200 OK. The boxes snake into rows when they don’t fit on one line.
App
CDN
Gateway
Auth
Orders
Database
How to use it
- Paste the snippet just before
</head>. - Add
data-request-flow-diagramto a container with one child per service, in the order the request reaches them. - Give a service an emoji with
data-iconand a timing withdata-time.
Options
Add any of these attributes to the same element.
data-icon | On a service: an emoji shown above its name. |
data-time | On a service: a badge shown when the request reaches it, such as 12 ms. |
data-color | Colour of the request. Default #a855f7. |
data-return-color | Colour of the response. Default #10b981. |
data-status | Badge on the first box when the response lands. Default 200 OK. |
data-one-way | Stop at the last service, with no return trip. |
data-step | Time for each hop. Default 700 ms. |
data-loop | Send the request again and again. |
The boxes are sized in em, so set font-size on the container to scale them.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Request Flow Diagram: a request travels through a system. A glowing
// packet runs from service to service, each one lighting up (with its
// timing) as the request reaches it, then the response runs all the way
// back in a second colour and the first box shows the status.
// Usage:
// <div data-request-flow-diagram data-loop>
// <span data-icon="๐ฑ">App</span>
// <span data-icon="๐ช" data-time="3 ms">Gateway</span>
// <span data-icon="โ๏ธ" data-time="18 ms">Orders</span>
// <span data-icon="๐๏ธ" data-time="9 ms">Database</span>
// </div>
// One child per service, in the order the request reaches them. The boxes
// snake into rows when they don't fit on one line.
// Optional: data-icon and data-time on a service; data-color (request,
// default #a855f7), data-return-color (response, default #10b981),
// data-status (shown on the first box when the response lands, default
// "200 OK"), data-one-way (no return trip), data-step (ms per hop,
// default 700), data-loop (run the request again and again).
var DEFAULTS = { color: '#a855f7', returnColor: '#10b981', status: '200 OK', step: 700 };
var NS = 'http://www.w3.org/2000/svg';
var css =
'.rqf{position:relative;width:100%;--rqf-c:#a855f7;--rqf-r:#10b981}' +
'.rqf-svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}' +
'.rqf-node{position:absolute;box-sizing:border-box;width:6.4em;padding:.7em .3em .6em;border-radius:.9em;background:#fff;border:2px solid #e6dcf5;text-align:center;line-height:1.15;box-shadow:0 4px 12px rgba(70,30,130,.08);transition:border-color .3s,box-shadow .3s}' +
'.rqf-icon{display:block;font-size:1.7em;line-height:1.1;margin-bottom:.2em}' +
'.rqf-name{display:block;font-weight:700;font-size:.92em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'.rqf-node.rqf-on{border-color:var(--rqf-c);box-shadow:0 0 0 4px color-mix(in srgb,var(--rqf-c) 18%,transparent),0 6px 18px rgba(70,30,130,.18)}' +
'.rqf-node.rqf-back{border-color:var(--rqf-r);box-shadow:0 0 0 4px color-mix(in srgb,var(--rqf-r) 18%,transparent),0 6px 18px rgba(20,90,60,.15)}' +
'.rqf-badge{position:absolute;left:50%;top:0;translate:-50% -60%;padding:.15em .55em;border-radius:99px;background:var(--rqf-c);color:#fff;font-size:.78em;font-weight:700;white-space:nowrap;opacity:0}' +
'.rqf-badge.rqf-ok{background:var(--rqf-r)}' +
'.rqf-packet{filter:drop-shadow(0 0 4px currentColor)}';
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 color = d.color || DEFAULTS.color, back = d.returnColor || DEFAULTS.returnColor;
var s = (el._rqf = { nodes: [], links: [], visible: false, running: false });
el.classList.add('rqf');
el.style.setProperty('--rqf-c', color);
el.style.setProperty('--rqf-r', back);
var svg = mk('svg', { class: 'rqf-svg', 'aria-hidden': 'true' });
s.linkLayer = mk('g', { fill: 'none', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }, svg);
s.packet = mk('circle', { class: 'rqf-packet', r: 5, fill: color, color: color, opacity: 0 }, svg);
Array.from(el.children).forEach(function (k) {
var node = document.createElement('div');
node.className = 'rqf-node';
if (k.getAttribute('data-icon')) {
var ic = document.createElement('span');
ic.className = 'rqf-icon';
ic.textContent = k.getAttribute('data-icon');
ic.setAttribute('aria-hidden', 'true');
node.appendChild(ic);
}
var name = document.createElement('span');
name.className = 'rqf-name';
name.textContent = k.textContent.trim();
node.appendChild(name);
var badge = document.createElement('span');
badge.className = 'rqf-badge';
badge.setAttribute('aria-hidden', 'true');
badge.textContent = k.getAttribute('data-time') || '';
node.appendChild(badge);
el.replaceChild(node, k);
s.nodes.push({ el: node, icon: node.querySelector('.rqf-icon'), badge: badge, time: k.getAttribute('data-time') || '', name: name.textContent });
});
for (var i = 0; i < s.nodes.length - 1; i++) {
s.links.push({
base: mk('path', { stroke: '#cbbde6', 'stroke-width': 2, 'stroke-dasharray': '4 5' }, s.linkLayer),
fwd: mk('path', { stroke: color, 'stroke-width': 3 }, s.linkLayer),
back: mk('path', { stroke: back, 'stroke-width': 3 }, s.linkLayer),
});
}
el.insertBefore(svg, el.firstChild);
el.setAttribute('role', 'img');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Request flow: ' + s.nodes.map(function (n) { return n.name; }).join(' to ') + (d.oneWay != null ? '' : ', and the response back'));
layout(el);
new ResizeObserver(function () { layout(el); }).observe(el);
}
// Lay the boxes out in rows that snake back and forth, then route the links.
function layout(el) {
var s = el._rqf, n = s.nodes.length;
var W = el.clientWidth;
if (!W || !n) return;
var nw = s.nodes[0].el.offsetWidth, nh = 0;
s.nodes.forEach(function (x) { nh = Math.max(nh, x.el.offsetHeight); });
var gap = Math.max(18, nw * 0.42);
var cols = Math.max(1, Math.min(n, Math.floor((W + gap) / (nw + gap))));
var rows = Math.ceil(n / cols);
cols = Math.ceil(n / rows);
var gx = cols > 1 ? Math.min(nw * 1.1, (W - cols * nw) / (cols - 1)) : 0;
var gy = Math.max(26, nh * 0.55);
var left = (W - (cols * nw + (cols - 1) * gx)) / 2;
var top = 14;
s.nodes.forEach(function (x, i) {
var r = Math.floor(i / cols), c = i % cols;
if (r % 2) c = cols - 1 - c;
x.x = left + c * (nw + gx);
x.y = top + r * (nh + gy);
x.w = nw; x.h = nh; x.row = r;
x.el.style.left = x.x + 'px';
x.el.style.top = x.y + 'px';
x.el.style.height = nh + 'px';
});
el.style.height = top + rows * nh + (rows - 1) * gy + 6 + 'px';
s.links.forEach(function (l, i) {
var a = s.nodes[i], b = s.nodes[i + 1], p;
if (a.row === b.row) {
var dir = b.x > a.x ? 1 : -1, y = a.y + a.h / 2;
p = 'M' + (dir > 0 ? a.x + a.w + 3 : a.x - 3) + ',' + y + ' L' + (dir > 0 ? b.x - 3 : b.x + b.w + 3) + ',' + y;
} else {
var x = a.x + a.w / 2;
p = 'M' + x + ',' + (a.y + a.h + 3) + ' L' + x + ',' + (b.y - 3);
}
l.base.setAttribute('d', p);
l.fwd.setAttribute('d', p);
l.back.setAttribute('d', p);
l.len = l.fwd.getTotalLength();
});
}
function travel(el, link, forward, cb) {
var s = el._rqf, step = +el.dataset.step || DEFAULTS.step;
var path = forward ? link.fwd : link.back, len = link.len, o = { t: 0 };
anime.utils.set(path, { opacity: 1 });
anime.animate(anime.svg.createDrawable(path), { draw: forward ? ['0 0', '0 1'] : ['1 1', '0 1'], duration: step * 0.7, ease: 'inOut(2)' });
anime.animate(o, {
t: 1, duration: step * 0.7, ease: 'inOut(2)',
onUpdate: function () {
var p = path.getPointAtLength(len * (forward ? o.t : 1 - o.t));
s.packet.setAttribute('cx', p.x);
s.packet.setAttribute('cy', p.y);
},
onComplete: cb,
});
}
function light(el, node, cls, badge) {
node.el.classList.add(cls);
anime.animate(node.el, { scale: [1, 1.08, 1], duration: 420, ease: 'out(2)' });
if (node.icon) anime.animate(node.icon, { y: [0, -5, 0], duration: 420, ease: 'out(2)' });
if (badge) {
node.badge.textContent = badge;
anime.animate(node.badge, { opacity: [0, 1], y: [6, 0], duration: 300, ease: 'out(2)' });
}
}
function run(el) {
var s = el._rqf, d = el.dataset, n = s.nodes.length, step = +d.step || DEFAULTS.step;
var color = d.color || DEFAULTS.color, back = d.returnColor || DEFAULTS.returnColor;
s.running = true;
s.packet.setAttribute('fill', color);
s.packet.setAttribute('color', color);
var first = s.nodes[0];
s.packet.setAttribute('cx', first.x + first.w / 2);
s.packet.setAttribute('cy', first.y + first.h / 2);
anime.animate(s.packet, { opacity: [0, 1], r: [0, 5], duration: 250 });
function forward(i) {
light(el, s.nodes[i], 'rqf-on', i > 0 ? s.nodes[i].time : '');
if (i === n - 1) return setTimeout(turn, step * 0.5);
setTimeout(function () { travel(el, s.links[i], true, function () { forward(i + 1); }); }, step * 0.25);
}
function turn() {
if (d.oneWay != null) return setTimeout(finish, step);
s.packet.setAttribute('fill', back);
s.packet.setAttribute('color', back);
anime.animate(s.packet, { r: [5, 8, 5], duration: 300 });
s.nodes[n - 1].el.classList.add('rqf-back');
backward(n - 2);
}
function backward(i) {
travel(el, s.links[i], false, function () {
var node = s.nodes[i];
node.el.classList.add('rqf-back');
if (i === 0) {
node.badge.classList.add('rqf-ok');
light(el, node, 'rqf-back', d.status || DEFAULTS.status);
anime.animate(s.packet, { opacity: 0, r: 12, duration: 400 });
return setTimeout(finish, step * 2.2);
}
anime.animate(node.el, { scale: [1, 1.04, 1], duration: 300 });
backward(i - 1);
});
}
function finish() {
anime.animate(s.packet, { opacity: 0, duration: 200 });
if (d.loop == null) { s.running = false; return; }
s.links.forEach(function (l) { anime.animate([l.fwd, l.back], { opacity: 0, duration: 500 }); });
s.nodes.forEach(function (x) {
x.el.classList.remove('rqf-on', 'rqf-back');
anime.animate(x.badge, { opacity: 0, duration: 300 });
});
setTimeout(function () {
s.nodes[0].badge.classList.remove('rqf-ok');
s.running = false;
if (s.visible) run(el);
}, 800);
}
forward(0);
}
function intro(el) {
var s = el._rqf;
anime.animate(s.nodes.map(function (x) { return x.el; }), { opacity: [0, 1], scale: [0.6, 1], duration: 500, delay: anime.stagger(90), ease: 'outBack(2)' });
s.links.forEach(function (l, i) {
anime.animate(anime.svg.createDrawable(l.base), { draw: ['0 0', '0 1'], duration: 300, delay: 150 + i * 90, ease: 'out(2)' });
});
setTimeout(function () { run(el); }, 400 + s.nodes.length * 90);
}
function init() {
var still = reduced();
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var el = e.target, s = el._rqf;
s.visible = e.isIntersecting;
if (!s.visible) return;
if (!s.introduced) { s.introduced = true; intro(el); return; }
if (!s.running) run(el);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-request-flow-diagram]').forEach(function (el) {
if (el._rqf) return;
setup(el);
var s = el._rqf;
s.links.forEach(function (l) { anime.utils.set([l.fwd, l.back], { opacity: 0 }); });
if (still) {
s.nodes.forEach(function (x) { x.el.classList.add('rqf-on'); });
s.links.forEach(function (l) { anime.utils.set(l.fwd, { opacity: 1 }); });
return;
}
s.nodes.forEach(function (x) { anime.utils.set(x.el, { opacity: 0 }); });
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-request-flow-diagram data-loop>
<span data-icon="๐ฑ">App</span>
<span data-icon="๐ช" data-time="2 ms">Gateway</span>
<span data-icon="โ๏ธ" data-time="24 ms">Orders</span>
<span data-icon="๐๏ธ" data-time="8 ms">Database</span>
</div>