Rollup — correlated tracks
Variant: Correlated tracks
A rollup drawn the way a well log is read: one composite track over the three it summarises, all revealed by a single linear pen so the same depth is the same instant on every track — the correlation is the reason to stack them, and a pen per track would break it. The composite is the worst of the three at each x, and it has its own dashed threshold. Small wobbles pass through it honestly (the queue never crosses the line), but when the composite dips past the line, a dashed tie drops from the dip to the track that caused it in 240ms of ease-out, and the source’s name is written on the rollup 72ms behind, so the eye travels down to the cause and back. The rollup’s own dot is clay exactly while it is past the line, cut on 72ms fades so it reads as state. Under reduced motion the full log is on the page and the two attributions fade in order. Use it for incident timelines, SLO dashboards, and any composite metric that should never be read without its sources.
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Rollup — correlated tracks</title>
<style>
:root {
/* Neutrals — fixed, never remixed. */
--ivory: #faf9f5;
--paper: #ffffff;
--oat: #e3dacc;
--slate: #141413;
/* Knobs — the remix API. A knob exists iff declared here and used below. */
--hue: 38.8;
--lift: 0;
--chroma: 1;
--round: 1;
--speed: 1;
/* Accents — derived from the knobs with the family's offsets baked in,
so one hue roll moves them together. Defaults reproduce the original hex. */
--clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
--clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
--olive: oklch(
calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
);
--rust: oklch(
calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
);
/* Clock — every duration in the document runs off --t. 1% is 72ms.
The log is read 5%–75%, so x in the 200-unit plot is 5% + 0.35% x. */
--cycle: 7.2s;
--t: calc(var(--cycle) / var(--speed));
/* The clock every animation reads; reduced motion doubles it in one
place, so no selector can fall out of step with the rest. */
--_t: var(--t);
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: var(--slate);
}
.panel {
width: min(280px, 90vw);
padding: 12px 14px 14px;
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(10px * var(--round));
}
.cap {
margin-bottom: 10px;
font-size: 11px;
line-height: 1.35;
letter-spacing: 0.02em;
text-wrap: balance;
color: color-mix(in srgb, var(--slate) 55%, transparent);
}
.log {
display: grid;
grid-template-columns: 46px 1fr;
gap: 8px;
animation: rig var(--_t) infinite;
}
.names {
display: grid;
grid-template-rows: repeat(4, 30px);
align-items: center;
font-size: 11px;
color: color-mix(in srgb, var(--slate) 60%, transparent);
}
.names .top {
display: flex;
align-items: center;
gap: 5px;
font-weight: 600;
color: var(--slate);
}
/* The rollup's own status: clay exactly while the composite is past
the line, cut in on 72ms fades so it reads as a state, not a pulse. */
.dot {
position: relative;
width: 7px;
height: 7px;
flex: none;
}
.dot i,
.dot b {
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--olive);
}
.dot b {
background: var(--clay);
opacity: 0;
animation: alarm var(--_t) infinite;
}
.plot {
position: relative;
height: 120px;
}
.plot svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
}
.plot path,
.plot line {
fill: none;
vector-effect: non-scaling-stroke;
}
.rule {
stroke: var(--oat);
stroke-width: 1;
}
.limit {
stroke: color-mix(in srgb, var(--clay) 60%, var(--oat));
stroke-width: 1;
stroke-dasharray: 3 3;
}
.trace {
stroke: color-mix(in srgb, var(--slate) 42%, transparent);
stroke-width: 1.2;
stroke-linejoin: round;
}
.trace.composite {
stroke: var(--slate);
stroke-width: 1.6;
}
/* The pen. Every track is revealed by one clip on one linear clock, so
the same depth is the same instant on all four — the whole point of
reading them side by side. */
.reveal {
position: absolute;
inset: 0;
clip-path: inset(-2px 100% -2px 0);
animation: reveal var(--_t) linear infinite;
}
.cursor {
position: absolute;
inset: 0;
transform: translateX(0);
animation: cursor var(--_t) linear infinite;
}
.cursor::before {
content: '';
position: absolute;
top: -4px;
bottom: -4px;
left: 0;
width: 1px;
background: color-mix(in srgb, var(--slate) 30%, transparent);
}
/* Bands where a track is past its line. Inside the reveal, so they
grow with the pen rather than announcing an incident early. */
.band {
position: absolute;
height: 25%;
background: color-mix(in srgb, var(--clay) 16%, transparent);
}
.band.api {
left: 23.85%;
width: 9.85%;
}
.band.db {
left: 63.1%;
width: 14.5%;
}
/* The correlation: a dashed tie from the rollup's dip down to the
track that caused it, and the source's name on the rollup itself. */
.tie {
position: absolute;
width: 0;
border-left: 1px dashed var(--clay-d);
transform: scaleY(0);
transform-origin: top;
}
.tie.api {
left: 23.85%;
top: 13.3%;
height: 25%;
animation: tie-api var(--_t) infinite;
}
.tie.db {
left: 63.1%;
top: 13.3%;
height: 50%;
animation: tie-db var(--_t) infinite;
}
.tag {
position: absolute;
top: 17px;
font-size: 11px;
line-height: 12px;
font-weight: 600;
color: var(--clay-d);
opacity: 0;
}
.tag.api {
right: calc(76.15% + 4px);
animation: tag-api var(--_t) infinite;
}
.tag.db {
right: calc(36.9% + 4px);
animation: tag-db var(--_t) infinite;
}
@keyframes rig {
0% {
opacity: 0;
animation-timing-function: var(--ease-out);
}
3%,
90% {
opacity: 1;
animation-timing-function: var(--ease-out);
}
93%,
100% {
opacity: 0;
}
}
@keyframes reveal {
0%,
5% {
clip-path: inset(-2px 100% -2px 0);
}
75%,
95% {
clip-path: inset(-2px 0 -2px 0);
}
96%,
100% {
clip-path: inset(-2px 100% -2px 0);
}
}
@keyframes cursor {
0%,
5% {
transform: translateX(0);
}
75%,
95% {
transform: translateX(100%);
}
96%,
100% {
transform: translateX(0);
}
}
/* api crosses the line at x 47.7 (21.7%) and clears at 67.4 (28.6%);
db crosses at 126.2 (49.2%) and clears at 155.2 (59.3%). */
@keyframes alarm {
0%,
21.7% {
opacity: 0;
}
22.7%,
28.6% {
opacity: 1;
}
29.6%,
49.2% {
opacity: 0;
}
50.2%,
59.3% {
opacity: 1;
}
60.3%,
100% {
opacity: 0;
}
}
/* The tie drops 240ms of ease-out from the crossing; the name follows
72ms behind it, so the eye travels down to the source and back. */
@keyframes tie-api {
0%,
21.7% {
transform: scaleY(0);
animation-timing-function: var(--ease-out);
}
25%,
95% {
transform: scaleY(1);
}
96%,
100% {
transform: scaleY(0);
}
}
@keyframes tie-db {
0%,
49.2% {
transform: scaleY(0);
animation-timing-function: var(--ease-out);
}
52.5%,
95% {
transform: scaleY(1);
}
96%,
100% {
transform: scaleY(0);
}
}
@keyframes tag-api {
0%,
22.7% {
opacity: 0;
transform: translateY(3px);
animation-timing-function: var(--ease-out);
}
26%,
95% {
opacity: 1;
transform: translateY(0);
}
96%,
100% {
opacity: 0;
}
}
@keyframes tag-db {
0%,
50.2% {
opacity: 0;
transform: translateY(3px);
animation-timing-function: var(--ease-out);
}
53.5%,
95% {
opacity: 1;
transform: translateY(0);
}
96%,
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
/* No pen and no cursor: the whole log is on the page, and the two
attributions fade in, in order, at twice the period, so the
correlation is still taught one incident at a time. */
.reveal,
.cursor {
animation: none;
clip-path: none;
}
.cursor {
display: none;
}
:root {
--_t: calc(var(--t) * 2);
}
.tie,
.tag {
transform: none !important;
}
.tie.api {
animation-name: tag-api;
}
.tie.db {
animation-name: tag-db;
}
}
</style>
</head>
<body>
<div
class="panel"
role="img"
aria-label="A well-log style strip: a rollup track above three service tracks, all drawn by one cursor sweeping left to right. Wherever the rollup dips past its threshold, a dashed line drops to the track that caused it and the source's name, api and then db, is written on the rollup. A small wobble on the queue track passes through without crossing the line"
>
<p class="cap">rollup = worst of three</p>
<div class="log">
<div class="names">
<span class="top"
><span class="dot"><i></i><b></b></span>rollup</span
>
<span>api</span>
<span>db</span>
<span>queue</span>
</div>
<div class="plot">
<svg viewBox="0 0 200 120" preserveAspectRatio="none" aria-hidden="true">
<line class="rule" x1="0" y1="30" x2="200" y2="30" />
<line class="rule" x1="0" y1="60" x2="200" y2="60" />
<line class="rule" x1="0" y1="90" x2="200" y2="90" />
<line class="limit" x1="0" y1="16" x2="200" y2="16" />
<line class="limit" x1="0" y1="46" x2="200" y2="46" />
<line class="limit" x1="0" y1="76" x2="200" y2="76" />
<line class="limit" x1="0" y1="106" x2="200" y2="106" />
</svg>
<div class="reveal">
<span class="band api" style="top: 0"></span>
<span class="band api" style="top: 25%"></span>
<span class="band db" style="top: 0"></span>
<span class="band db" style="top: 50%"></span>
<svg viewBox="0 0 200 120" preserveAspectRatio="none" aria-hidden="true">
<path
class="trace composite"
d="M0 10.1 L4 10.2 L8 9.8 L12 10.4 L16 10.0 L20 10.5 L24 9.8 L28 10.1 L32 10.3 L36 9.3 L40 10.6 L44 10.2 L48 17.0 L52 24.1 L56 24.2 L60 24.3 L62 24.5 L64 21.4 L68 15.1 L72 9.9 L76 10.6 L80 10.3 L84 9.6 L86 10.3 L88 10.3 L92 12.1 L96 14.3 L100 12.3 L104 10.0 L106 10.3 L108 10.5 L112 10.4 L116 10.5 L118 10.1 L120 11.2 L124 14.5 L128 17.7 L132 21.2 L136 21.1 L140 21.5 L144 21.3 L146 21.2 L148 20.0 L152 18.1 L156 15.6 L160 13.8 L164 11.3 L168 10.6 L172 9.9 L176 10.2 L180 10.0 L184 9.6 L188 10.5 L192 10.5 L196 10.2 L200 9.1"
/>
<path
class="trace"
d="M0 39.0 L4 39.1 L8 39.8 L12 40.4 L16 39.1 L20 39.6 L24 39.4 L28 39.5 L32 40.3 L36 39.2 L40 40.4 L44 39.0 L48 47.0 L52 54.1 L56 54.2 L60 54.3 L62 54.5 L64 51.4 L68 45.1 L72 39.9 L76 40.5 L80 39.3 L84 39.6 L86 39.1 L88 39.2 L92 40.5 L96 40.4 L100 40.1 L104 39.8 L106 40.2 L108 40.0 L112 40.0 L116 39.5 L118 39.0 L120 40.2 L124 40.0 L128 39.4 L132 39.9 L136 39.4 L140 39.2 L144 40.3 L146 40.2 L148 39.5 L152 39.4 L156 39.8 L160 39.9 L164 39.2 L168 40.6 L172 39.9 L176 39.5 L180 39.9 L184 39.6 L188 39.1 L192 40.5 L196 40.2 L200 39.1"
/>
<path
class="trace"
d="M0 70.1 L4 70.2 L8 69.2 L12 69.1 L16 70.0 L20 69.0 L24 69.8 L28 70.1 L32 69.7 L36 69.2 L40 70.6 L44 69.1 L48 69.9 L52 70.3 L56 69.7 L60 69.8 L62 70.4 L64 69.3 L68 70.4 L72 69.7 L76 70.6 L80 69.6 L84 69.4 L86 70.3 L88 69.6 L92 69.3 L96 69.2 L100 70.0 L104 69.3 L106 69.6 L108 70.5 L112 69.3 L116 69.3 L118 70.1 L120 71.2 L124 74.5 L128 77.7 L132 81.2 L136 81.1 L140 81.5 L144 81.3 L146 81.2 L148 80.0 L152 78.1 L156 75.6 L160 73.8 L164 71.3 L168 70.5 L172 69.8 L176 69.1 L180 70.0 L184 69.3 L188 70.5 L192 70.0 L196 70.2 L200 69.1"
/>
<path
class="trace"
d="M0 99.9 L4 99.2 L8 99.5 L12 99.9 L16 99.8 L20 100.5 L24 99.2 L28 99.8 L32 100.2 L36 99.3 L40 99.6 L44 100.2 L48 100.5 L52 99.2 L56 99.2 L60 99.7 L62 99.7 L64 100.2 L68 100.3 L72 99.3 L76 99.0 L80 100.3 L84 99.0 L86 99.5 L88 100.3 L92 102.1 L96 104.3 L100 102.3 L104 100.0 L106 100.3 L108 100.0 L112 100.4 L116 100.5 L118 99.9 L120 99.7 L124 100.1 L128 99.8 L132 100.0 L136 99.5 L140 99.1 L144 100.3 L146 99.4 L148 99.2 L152 100.0 L156 99.9 L160 99.3 L164 99.2 L168 100.3 L172 99.1 L176 100.2 L180 99.3 L184 99.4 L188 99.3 L192 100.5 L196 99.5 L200 99.0"
/>
</svg>
</div>
<span class="tie api"></span>
<span class="tie db"></span>
<span class="tag api">api</span>
<span class="tag db">db</span>
<div class="cursor"></div>
</div>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.