Transition map beads: connector-colored elongated pulses
Beads now take their connector's color — accent for internal links, plain text color for external ones (matching the tconn-exit styling) — and render as an elongated pulse along the direction of travel instead of a plain dot: a rounded capsule with a gradient-faded tail, rotated to the edge's angle (cached per flow). Flow objects carry an 'external' tag from buildFlows; the gradients live in the svg defs with stop colors resolved from CSS so theme variables apply.
This commit is contained in:
1 parent
fb2914d853
commit
3c9ef04ffd
3 files changed
+52
-12
No files matched your search
+4
-1
@@ -465,7 +465,10 @@ flow along each edge, persisting across data reloads (emitters are keyed
|
||||
per edge direction and beads tracked by progress, so an unrelated count
|
||||
change never reshuffles them), emitted at a rate linearly proportional
|
||||
to the directional count with no in-flight limit, opposing directions
|
||||
offset onto parallel lanes. External sources and exits whose connectors are
|
||||
offset onto parallel lanes; each bead renders as an elongated pulse
|
||||
(gradient-faded tail) along its direction of travel, colored like its
|
||||
connector — accent for internal edges, plain text color for external
|
||||
ones. External sources and exits whose connectors are
|
||||
all culled by the width threshold are dropped from their rows themselves
|
||||
(the site's own page nodes always stay, connected or not). External sources show as a node row above the
|
||||
map: each visit is attributed to `utm_campaign`, then `utm_source`, then the
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
TNODE_W,
|
||||
TNODE_H,
|
||||
BEAD_R,
|
||||
BEAD_LEN,
|
||||
buildTransitionGraph,
|
||||
} from './analytics/transitions.js'
|
||||
|
||||
@@ -145,7 +146,17 @@ const tick = (t) => {
|
||||
continue
|
||||
}
|
||||
const f = b.e.flow
|
||||
out.push({ x: f.x1 + (f.x2 - f.x1) * b.p, y: f.y1 + (f.y2 - f.y1) * b.p })
|
||||
// The pulse is elongated along the direction of travel (constant per
|
||||
// flow) and colored like its connector (external edges use --text).
|
||||
if (f.deg === undefined) {
|
||||
f.deg = (Math.atan2(f.y2 - f.y1, f.x2 - f.x1) * 180) / Math.PI
|
||||
}
|
||||
out.push({
|
||||
x: f.x1 + (f.x2 - f.x1) * b.p,
|
||||
y: f.y1 + (f.y2 - f.y1) * b.p,
|
||||
deg: f.deg,
|
||||
ext: f.external,
|
||||
})
|
||||
}
|
||||
beads.value = out
|
||||
rafId = requestAnimationFrame(tick)
|
||||
@@ -184,6 +195,21 @@ const countLabel = (n) =>
|
||||
<section v-if="graph">
|
||||
<svg class="tmap" :style="{ maxWidth: `${graph.bounds.x1 - graph.bounds.x0}px` }" :viewBox="`${graph.bounds.x0} ${graph.bounds.y0} ${graph.bounds.x1 - graph.bounds.x0} ${graph.bounds.y1 - graph.bounds.y0}`"
|
||||
role="img" aria-label="map of transitions between pages">
|
||||
<defs>
|
||||
<!-- Pulse gradient: opaque head at the bead position (local origin),
|
||||
fading tail trailing behind it (stop-color comes from CSS so
|
||||
theme vars resolve). -->
|
||||
<linearGradient id="tbead-grad">
|
||||
<stop offset="0" class="tbead-stop" stop-opacity="0" />
|
||||
<stop offset="0.55" class="tbead-stop" stop-opacity="0.5" />
|
||||
<stop offset="1" class="tbead-stop" stop-opacity="1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="tbead-grad-exit">
|
||||
<stop offset="0" class="tbead-stop-exit" stop-opacity="0" />
|
||||
<stop offset="0.55" class="tbead-stop-exit" stop-opacity="0.5" />
|
||||
<stop offset="1" class="tbead-stop-exit" stop-opacity="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path v-for="(a, i) in graph.arcs" :key="'a' + i"
|
||||
:id="`tarc${i}`" :d="a.d" :class="['tarc', a.top && 'tarc-top']" />
|
||||
<template v-for="(a, i) in graph.arcs" :key="'t' + i">
|
||||
@@ -194,8 +220,11 @@ const countLabel = (n) =>
|
||||
:d="e.d" :class="['tconn', e.external && 'tconn-exit']">
|
||||
<title>{{ e.title }}</title>
|
||||
</path>
|
||||
<circle v-for="(b, i) in beads" :key="'b' + i"
|
||||
:cx="b.x" :cy="b.y" :r="BEAD_R" class="tbead" />
|
||||
<rect v-for="(b, i) in beads" :key="'b' + i"
|
||||
:x="-BEAD_LEN" :y="-BEAD_R" :width="BEAD_LEN" :height="2 * BEAD_R" :rx="BEAD_R"
|
||||
:fill="b.ext ? 'url(#tbead-grad-exit)' : 'url(#tbead-grad)'"
|
||||
:class="['tbead', b.ext && 'tbead-exit']"
|
||||
:transform="`translate(${b.x} ${b.y}) rotate(${b.deg})`" />
|
||||
<g v-for="(x, i) in graph.extNodes" :key="'x' + i">
|
||||
<clipPath :id="`xclip${i}`">
|
||||
<rect :x="x.x - TNODE_W/2 + 6" :y="x.y - TNODE_H/2" :width="TNODE_W - 12"
|
||||
@@ -259,10 +288,13 @@ const countLabel = (n) =>
|
||||
fill: var(--text);
|
||||
}
|
||||
.tmap .tbead {
|
||||
fill: var(--accent);
|
||||
opacity: 0.85;
|
||||
filter: drop-shadow(0 0 2.5px var(--accent));
|
||||
}
|
||||
.tmap .tbead-exit {
|
||||
filter: drop-shadow(0 0 2.5px var(--text));
|
||||
}
|
||||
.tmap .tbead-stop { stop-color: var(--accent); }
|
||||
.tmap .tbead-stop-exit { stop-color: var(--text); }
|
||||
.tmap .txnode {
|
||||
/* External source/exit pills: plain white on every theme, with a hairline
|
||||
so the pill stays visible on a white page. */
|
||||
|
||||
@@ -149,8 +149,10 @@ const MIN_WMID = 0.4
|
||||
// from real time to keep the animation lightweight. The component
|
||||
// simulates every bead independently in JS with a constant traversal
|
||||
// time per edge (speed relative to span length), with no limit on beads
|
||||
// in flight.
|
||||
// in flight. A bead renders as an elongated pulse trailing its direction
|
||||
// of travel: BEAD_R is its half-thickness, BEAD_LEN its full length.
|
||||
export const BEAD_R = 3.2
|
||||
export const BEAD_LEN = BEAD_R * 5.5
|
||||
const BEAD_RATE = 0.0084 // beads per second per hit/day
|
||||
const FLOW_OFFSET = 3 // lane offset to the right of the travel direction
|
||||
|
||||
@@ -638,10 +640,12 @@ function buildRibbon(a, b, ab, ba, wMid, external = false) {
|
||||
* at the surround), plus the emission interval (seconds between beads,
|
||||
* inverse of the daily hit rate * BEAD_RATE). Each segment is offset to the
|
||||
* right-hand side of its travel direction, so opposing flows on the same
|
||||
* edge run on parallel lanes instead of colliding. The component turns
|
||||
* these into independently simulated beads.
|
||||
* edge run on parallel lanes instead of colliding. `external` tags flows on
|
||||
* external-link edges so the component can render their beads in the
|
||||
* connector color (the exit/source connectors use --text, not --accent).
|
||||
* The component turns these into independently simulated beads.
|
||||
*/
|
||||
function buildFlows(a, b, ab, ba, dayScale = 1) {
|
||||
function buildFlows(a, b, ab, ba, dayScale = 1, external = false) {
|
||||
const len = Math.hypot(b.x - a.x, b.y - a.y) || 1
|
||||
const ux = (b.x - a.x) / len
|
||||
const uy = (b.y - a.y) / len
|
||||
@@ -665,6 +669,7 @@ function buildFlows(a, b, ab, ba, dayScale = 1) {
|
||||
y2: a.y + toT * uy + s * ry,
|
||||
len: span,
|
||||
interval: 1 / (count * BEAD_RATE * dayScale),
|
||||
external,
|
||||
}
|
||||
}
|
||||
const flows = []
|
||||
@@ -894,7 +899,7 @@ function buildExternal({ sources, exits }, byPath, innerBounds, dayScale = 1) {
|
||||
const wMid = width(p.in)
|
||||
if (wMid < MIN_WMID) continue
|
||||
edges.push(buildRibbon(xn, page, p.in, 0, wMid, true))
|
||||
flows.push(...buildFlows(xn, page, p.in, 0, dayScale))
|
||||
flows.push(...buildFlows(xn, page, p.in, 0, dayScale, true))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -944,7 +949,7 @@ function buildExternal({ sources, exits }, byPath, innerBounds, dayScale = 1) {
|
||||
const wMid = width(p.out)
|
||||
if (wMid < MIN_WMID) continue
|
||||
edges.push(buildRibbon(page, xn, p.out, 0, wMid, true))
|
||||
flows.push(...buildFlows(page, xn, p.out, 0, dayScale))
|
||||
flows.push(...buildFlows(page, xn, p.out, 0, dayScale, true))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user