Transition map: cull invisible connectors, stable beads, lane labels
- Cull connections whose thin middle would render below ~0.8px (MIN_WMID); drop external source/exit nodes whose connectors are all culled, while site page nodes always stay - Bead simulation persists across data reloads: emitters keyed per edge direction, beads tracked by progress, so unrelated count changes no longer reshuffle bead positions - Bead speed relative to span length: constant 1.5s traversal per edge - Top lane labeled with a house icon; all lane labels left-aligned just past the source pill (half height on near-vertical branch lanes), with guides running to the lane end so long slugs are never truncated
This commit is contained in:
+15
-7
@@ -250,7 +250,8 @@ younger than that, bucket size included) so the chart never collapses to a
|
||||
tiny sliver when the site is young. Below the charts: a **transition map** (all pages from
|
||||
`/_api/pages` — top-level menu items on a large-radius circular arc whose
|
||||
bottom point is the last item (each earlier item a bit higher), connected
|
||||
by an unlabeled top lane, each item's
|
||||
by a top lane labeled 🏠︎ beside the home pill (50% thicker than
|
||||
the branch lanes, its label font and guide offset scaled along), each item's
|
||||
subtree fanning out below it in menu order along a large-radius circular
|
||||
arc that leaves heading
|
||||
straight down and gradually bends right, index pages without views omitted
|
||||
@@ -258,17 +259,24 @@ and their children promoted in their place. The submenu structure is drawn
|
||||
as wide branch lanes: one per path prefix with at least two visible
|
||||
nodes, running behind the branch's node pills as circle arcs concentric
|
||||
with the fan (parent levels one radius step outward, so all lanes of a
|
||||
group share exactly one form), each labeled with its branch slug along the
|
||||
first inter-node gap — so the lanes reflect the path
|
||||
group share exactly one form), each labeled with its branch slug
|
||||
left-aligned just past the first pill and allowed to run along the lane to
|
||||
its end, disappearing under later pills when long — so the lanes reflect
|
||||
the path
|
||||
structure even where index pages are omitted — opposite transition
|
||||
directions joined into organic
|
||||
tapered connections whose middle width grows logarithmically with the
|
||||
count (a single count renders as a ~1 px line, uncapped), connections
|
||||
count (uncapped), connections
|
||||
carrying less than 1% of the total traffic
|
||||
pruned; beads are simulated one by one in JS (requestAnimationFrame) and
|
||||
flow along each edge, emitted at a rate linearly proportional
|
||||
pruned, as are those whose thin middle would render below ~0.8 px —
|
||||
fainter strands are invisible and only their wide end flares would show; beads are simulated one by one in JS (requestAnimationFrame) and
|
||||
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 show as a node row above the
|
||||
offset onto parallel lanes. 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
|
||||
referer origin, then any other `utm_*` tag, so UTM-tagged visits are grouped
|
||||
under their campaign/source value rather than the referer domain. A UTM
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
TNODE_W,
|
||||
TNODE_H,
|
||||
BEAD_R,
|
||||
BEAD_SPEED,
|
||||
buildTransitionGraph,
|
||||
} from './analytics/transitions.js'
|
||||
|
||||
@@ -46,65 +45,97 @@ const graph = computed(() =>
|
||||
|
||||
// Bead animation: every bead is simulated independently in JS. Each flow
|
||||
// (one per edge direction) emits a bead every `interval` seconds; beads
|
||||
// travel at BEAD_SPEED along the segment and are dropped at the end.
|
||||
// cross their segment in a constant TRAVERSAL_S seconds (speed relative
|
||||
// to span length) and are dropped at the end.
|
||||
// There is deliberately no cap on beads in flight.
|
||||
// Emitters persist across data reloads, keyed by flow.key: an unchanged
|
||||
// link keeps its emission phase and in-flight beads (tracked by progress,
|
||||
// not absolute time), so a count change elsewhere never reshuffles them.
|
||||
const beads = shallowRef([])
|
||||
let rafId = 0
|
||||
const emitters = new Map() // flow.key -> { flow, interval, next, alive }
|
||||
const live = [] // { e, p } — beads in flight, p = progress 0..1
|
||||
let lastTick = 0
|
||||
|
||||
const MAX_BEAD_RATE = 120 // upper bound on total beads per second
|
||||
const TRAVERSAL_S = 1.5 // seconds to cross any segment, end to end
|
||||
|
||||
const startBeads = (flows) => {
|
||||
cancelAnimationFrame(rafId)
|
||||
beads.value = []
|
||||
if (!flows?.length) return
|
||||
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||
|
||||
const syncBeads = (flows) => {
|
||||
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
if (!flows?.length || reduced) {
|
||||
emitters.clear()
|
||||
live.length = 0
|
||||
beads.value = []
|
||||
return
|
||||
}
|
||||
// Cap the total bead emission rate so a busy range cannot spawn enough
|
||||
// beads to kill the page. Existing per-range time scaling is preserved;
|
||||
// this is only a proportional emergency throttle when the limit is hit.
|
||||
const totalRate = flows.reduce((s, f) => s + 1 / f.interval, 0)
|
||||
const scale = totalRate > MAX_BEAD_RATE ? MAX_BEAD_RATE / totalRate : 1
|
||||
|
||||
const live = [] // { flow, t0 } — one entry per bead in flight
|
||||
const now = performance.now()
|
||||
const emitters = flows.map((flow) => {
|
||||
const seen = new Set()
|
||||
for (const flow of flows) {
|
||||
seen.add(flow.key)
|
||||
const interval = (flow.interval / scale) * 1000
|
||||
// Pre-fill the traversal with evenly spaced beads (random phase), so
|
||||
// the flow appears already running instead of starting empty.
|
||||
const e = emitters.get(flow.key)
|
||||
if (e) {
|
||||
e.flow = flow // pick up new geometry/rate, keep the phase
|
||||
e.interval = interval
|
||||
continue
|
||||
}
|
||||
// New emitter: pre-fill the traversal with evenly spaced beads (random
|
||||
// phase), so the flow appears already running instead of empty.
|
||||
const phase = Math.random() * interval
|
||||
for (let t = now - (flow.len / BEAD_SPEED) * 1000 + phase; t <= now; t += interval) {
|
||||
live.push({ flow, t0: t })
|
||||
const dp = interval / 1000 / TRAVERSAL_S
|
||||
const ne = { flow, interval, next: now + phase, alive: true }
|
||||
for (let p = 1 - phase / 1000 / TRAVERSAL_S; p > 0; p -= dp) {
|
||||
live.push({ e: ne, p })
|
||||
}
|
||||
return { flow, interval, next: now + phase }
|
||||
})
|
||||
|
||||
const tick = (t) => {
|
||||
for (const e of emitters) {
|
||||
while (e.next <= t) {
|
||||
live.push({ flow: e.flow, t0: e.next })
|
||||
e.next += e.interval
|
||||
}
|
||||
}
|
||||
const out = []
|
||||
for (let i = live.length - 1; i >= 0; i--) {
|
||||
const b = live[i]
|
||||
const p = ((t - b.t0) / 1000) * BEAD_SPEED / b.flow.len
|
||||
if (p >= 1) {
|
||||
live.splice(i, 1)
|
||||
continue
|
||||
}
|
||||
out.push({
|
||||
x: b.flow.x1 + (b.flow.x2 - b.flow.x1) * p,
|
||||
y: b.flow.y1 + (b.flow.y2 - b.flow.y1) * p,
|
||||
})
|
||||
}
|
||||
beads.value = out
|
||||
rafId = requestAnimationFrame(tick)
|
||||
emitters.set(flow.key, ne)
|
||||
}
|
||||
for (const [key, e] of emitters) {
|
||||
if (!seen.has(key)) {
|
||||
e.alive = false
|
||||
emitters.delete(key)
|
||||
}
|
||||
}
|
||||
for (let i = live.length - 1; i >= 0; i--) {
|
||||
if (!live[i].e.alive) live.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const tick = (t) => {
|
||||
const dt = lastTick ? (t - lastTick) / 1000 : 0
|
||||
lastTick = t
|
||||
for (const e of emitters.values()) {
|
||||
while (e.next <= t) {
|
||||
live.push({ e, p: 0 })
|
||||
e.next += e.interval
|
||||
}
|
||||
}
|
||||
const out = []
|
||||
for (let i = live.length - 1; i >= 0; i--) {
|
||||
const b = live[i]
|
||||
b.p += dt / TRAVERSAL_S
|
||||
if (b.p >= 1) {
|
||||
live.splice(i, 1)
|
||||
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 })
|
||||
}
|
||||
beads.value = out
|
||||
rafId = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
watch(() => graph.value?.flows, startBeads, { immediate: true })
|
||||
watch(() => graph.value?.flows, syncBeads, { immediate: true })
|
||||
onMounted(() => {
|
||||
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
rafId = requestAnimationFrame(tick)
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => cancelAnimationFrame(rafId))
|
||||
|
||||
// The svg never renders larger than its natural size (1 viewBox unit = 1
|
||||
@@ -161,7 +192,7 @@ const countLabel = (n) =>
|
||||
:id="`tarc${i}`" :d="a.d" :class="['tarc', a.top && 'tarc-top']" />
|
||||
<template v-for="(a, i) in graph.arcs" :key="'t' + i">
|
||||
<path v-if="a.ld" :id="`tarcl${i}`" :d="a.ld" fill="none" stroke="none" />
|
||||
<text v-if="a.ld" class="tarclabel"><textPath :href="`#tarcl${i}`" startOffset="50%">{{ a.label }}</textPath></text>
|
||||
<text v-if="a.ld" class="tarclabel" :class="{ 'tarclabel-top': a.top }"><textPath :href="`#tarcl${i}`" startOffset="0">{{ a.label }}</textPath></text>
|
||||
</template>
|
||||
<path v-for="(e, i) in graph.edges" :key="'e' + i"
|
||||
:d="e.d" :class="['tconn', e.external && 'tconn-exit']">
|
||||
@@ -237,10 +268,16 @@ const countLabel = (n) =>
|
||||
opacity: 0.25;
|
||||
}
|
||||
.tmap .tarc-top { stroke-width: 24; }
|
||||
/* Lane labels are left-aligned: each guide arc starts just past the source
|
||||
pill's edge, the earliest point where the text is visible. */
|
||||
.tmap .tarclabel {
|
||||
fill: var(--muted);
|
||||
font-size: calc(13px / var(--u, 1));
|
||||
text-anchor: middle;
|
||||
text-anchor: start;
|
||||
}
|
||||
/* The top lane is 50% thicker; its 🏠︎ label scales along. */
|
||||
.tmap .tarclabel-top {
|
||||
font-size: calc(19.5px / var(--u, 1));
|
||||
}
|
||||
.tmap .tnode {
|
||||
fill: var(--accent);
|
||||
|
||||
@@ -124,20 +124,22 @@ const pillTangent = (s, margin = 0) => {
|
||||
|
||||
// Edge width (half-width of the thin middle) grows logarithmically with
|
||||
// the count. The constants are scaled down by ~10× so busy ranges (day,
|
||||
// year) do not overwhelm the graph with fat connectors. A single recorded
|
||||
// transition still renders as a faint ~0.4 px line. Connections carrying
|
||||
// less than PRUNE_FRACTION of the total traffic are not drawn at all (this
|
||||
// also keeps the graph under ~100 connections).
|
||||
// year) do not overwhelm the graph with fat connectors. Connections whose
|
||||
// thin middle would render below MIN_WMID are culled entirely: fainter
|
||||
// strands are practically invisible and only their wide end flares would
|
||||
// show. Connections carrying less than PRUNE_FRACTION of the total traffic
|
||||
// are likewise not drawn (this also keeps the graph under ~100 connections).
|
||||
const WMID_MIN = 0.2
|
||||
const WIDTH_GROWTH = 0.15
|
||||
const PRUNE_FRACTION = 0.01
|
||||
// ~0.8 px full width at natural size (1 viewBox unit = 1 px).
|
||||
const MIN_WMID = 0.4
|
||||
|
||||
// Beads: each edge direction emits beads at count * BEAD_RATE beads per
|
||||
// second (linear in the count). The rate is reduced ~10× across all time
|
||||
// scales to keep the animation lightweight. The component simulates every
|
||||
// bead independently in JS at BEAD_SPEED along the edge, with no limit on
|
||||
// beads in flight.
|
||||
export const BEAD_SPEED = 180 // svg units per second
|
||||
// bead independently in JS with a constant traversal time per edge (speed
|
||||
// relative to span length), with no limit on beads in flight.
|
||||
export const BEAD_R = 3.2
|
||||
const BEAD_RATE = 0.012 // beads per second per recorded transition
|
||||
const FLOW_OFFSET = 3 // lane offset to the right of the travel direction
|
||||
@@ -379,15 +381,23 @@ function layoutGroups(root) {
|
||||
// INDENT larger per parent level — concentric circles, so all lanes
|
||||
// share exactly one form. Lanes span their branch's nodes plus a
|
||||
// little extra tucked under the first/last pill (so the line caps are
|
||||
// never visible) and run behind the pills. A separate short arc
|
||||
// across the first inter-node gap carries the branch slug as a label,
|
||||
// replacing per-node path crumbs. Hidden (unplaced) index pages still
|
||||
// never visible) and run behind the pills. A label arc carries the
|
||||
// branch slug, left-aligned just past the first pill and free to run
|
||||
// to the lane's end — longer text simply passes under later pills,
|
||||
// which are drawn on top. Hidden (unplaced) index pages still
|
||||
// define a lane: it follows their promoted children, so lanes reflect
|
||||
// the path structure rather than page existence.
|
||||
const INDENT = 20 // lane spacing (radius) per nesting level (> lane width)
|
||||
const END_TUCK = 22 // arc units tucked under the first/last pill
|
||||
const GAP_TRIM = 32 // label arc clearance from the pills
|
||||
const LABEL_CHARS = 8 // ~13px glyphs fitting the gap
|
||||
// Labels are left-aligned on their guide: the guide starts just past the
|
||||
// source pill's edge, the earliest point where the text is visible.
|
||||
const LABEL_PAD = 6
|
||||
// The label guide rides GUIDE_OFF outward of the lane centerline: the
|
||||
// text's alphabetic baseline sits on the guide, so this puts the
|
||||
// glyph middle (not the baseline) on the lane center at any zoom —
|
||||
// dominant-baseline tricks are em-based and break under downscale.
|
||||
const GUIDE_OFF = 3.5
|
||||
const branches = []
|
||||
groups.forEach((members, gi) => {
|
||||
const g = groupRoots[gi]
|
||||
@@ -427,26 +437,36 @@ function layoutGroups(root) {
|
||||
return `M ${x0.toFixed(2)} ${y0.toFixed(2)} A ${r.toFixed(2)} ${r.toFixed(2)} 0 0 0 ${x1.toFixed(2)} ${y1.toFixed(2)}`
|
||||
}
|
||||
const d = arc(th(first) + END_TUCK / R, th(last) - END_TUCK / R, R)
|
||||
// The label guide rides GUIDE_OFF outward of the lane centerline: the
|
||||
// text's alphabetic baseline sits on the guide, so this puts the
|
||||
// glyph middle (not the baseline) on the lane center at any zoom —
|
||||
// dominant-baseline tricks are em-based and break under downscale.
|
||||
const GUIDE_OFF = 3.5
|
||||
const ld = arc(th(first) - GAP_TRIM / R, th(first + 1) + GAP_TRIM / R, R + GUIDE_OFF)
|
||||
// Start just past the first pill: lanes leave the source node nearly
|
||||
// vertically, so the pill's extent along the arc is its half height.
|
||||
// The guide runs to the lane's end so long slugs are never cut off.
|
||||
const ld = arc(th(first) - (TNODE_H / 2 + LABEL_PAD) / R,
|
||||
th(last) - END_TUCK / R, R + GUIDE_OFF)
|
||||
arcLeft = Math.min(arcLeft, pt(th(first) + END_TUCK / R, R)[0])
|
||||
const label = name.length > LABEL_CHARS ? `${name.slice(0, LABEL_CHARS - 1)}…` : name
|
||||
return { d, ld, label }
|
||||
return { d, ld, label: name }
|
||||
})
|
||||
// Top lane: an unlabeled arc along the top row's own circle, connecting
|
||||
// Top lane: an arc along the top row's own circle, connecting
|
||||
// the top nodes of all groups and tucked under the first and last of
|
||||
// them (the arc bottoms at the last item, so it continues rightward
|
||||
// under its pill). Drawn 50% thicker than branch lanes.
|
||||
// under its pill). Drawn 50% thicker than branch lanes. A 🏠︎ label
|
||||
// marks the lane right after the home pill, on a guide arc like
|
||||
// the branch labels but with the offset and clearance scaled up by the
|
||||
// same 50% to keep the glyph centered on the wider lane.
|
||||
if (span) {
|
||||
const d = `M ${(-half - END_TUCK).toFixed(2)} ${topY(-half - END_TUCK).toFixed(2)} `
|
||||
+ `A ${R_T.toFixed(2)} ${R_T.toFixed(2)} 0 0 0 ${(half + END_TUCK).toFixed(2)} ${topY(half + END_TUCK).toFixed(2)}`
|
||||
const rG = R_T + GUIDE_OFF * 1.5
|
||||
const ptG = (x) => [x, topD - R_T + Math.sqrt(rG * rG - (x - half) ** 2)]
|
||||
// Left-aligned like the branch labels: the guide starts just past the
|
||||
// home pill's edge (scaled with the lane thickness).
|
||||
const g0 = -half + TNODE_W / 2 + LABEL_PAD * 1.5
|
||||
const g1 = SLOT - half - TNODE_W / 2 - GAP_TRIM * 1.5
|
||||
const [gx0, gy0] = ptG(g0)
|
||||
const [gx1, gy1] = ptG(g1)
|
||||
arcs.unshift({
|
||||
d: `M ${(-half - END_TUCK).toFixed(2)} ${topY(-half - END_TUCK).toFixed(2)} `
|
||||
+ `A ${R_T.toFixed(2)} ${R_T.toFixed(2)} 0 0 0 ${(half + END_TUCK).toFixed(2)} ${topY(half + END_TUCK).toFixed(2)}`,
|
||||
ld: null,
|
||||
label: null,
|
||||
d,
|
||||
ld: `M ${gx0.toFixed(2)} ${gy0.toFixed(2)} A ${rG.toFixed(2)} ${rG.toFixed(2)} 0 0 0 ${gx1.toFixed(2)} ${gy1.toFixed(2)}`,
|
||||
label: '🏠︎',
|
||||
top: true,
|
||||
})
|
||||
}
|
||||
@@ -636,16 +656,18 @@ function buildFlows(a, b, ab, ba, visualScale = 1) {
|
||||
}
|
||||
}
|
||||
const flows = []
|
||||
if (ab) flows.push(flow(ab, t0, t1))
|
||||
if (ba) flows.push(flow(ba, t1, t0))
|
||||
// Stable key per edge direction so the component's bead simulation can
|
||||
// match flows across data reloads and keep bead phases/positions.
|
||||
if (ab) flows.push({ ...flow(ab, t0, t1), key: `${a.path} ${b.path}` })
|
||||
if (ba) flows.push({ ...flow(ba, t1, t0), key: `${b.path} ${a.path}` })
|
||||
return flows
|
||||
}
|
||||
|
||||
/**
|
||||
* Half-width for a connection middle: logarithmic in the count, anchored
|
||||
* so a single count lands exactly at WMID_MIN (~1 px line), uncapped.
|
||||
* Absolute on purpose — cool routes stay visible regardless of how hot
|
||||
* the hottest connection is.
|
||||
* at WMID_MIN, uncapped. Absolute on purpose — cool routes stay visible
|
||||
* regardless of how hot the hottest connection is. Callers cull results
|
||||
* below MIN_WMID.
|
||||
*/
|
||||
const scaledWidth = (count) => {
|
||||
if (count <= 0) return 0
|
||||
@@ -671,7 +693,7 @@ function buildInternalEdges(pairs, byPath, visualScale = 1) {
|
||||
const b = byPath.get(pt)
|
||||
if (a.hidden || b.hidden) continue // unplaced index pages are omitted
|
||||
const wMid = scaledWidth((ab + ba) * visualScale)
|
||||
if (wMid <= 0) continue
|
||||
if (wMid < MIN_WMID) continue
|
||||
edges.push(buildRibbon(a, b, ab, ba, wMid))
|
||||
flows.push(...buildFlows(a, b, ab, ba, visualScale))
|
||||
}
|
||||
@@ -764,7 +786,8 @@ function buildExternal({ sources, exits }, byPath, innerBounds, visualScale = 1)
|
||||
const width = (count) => scaledWidth(count * visualScale)
|
||||
|
||||
// Incoming: one source node per identified source, in a row centered
|
||||
// above the map, with an edge to each page that source led to.
|
||||
// above the map, with an edge to each page that source led to. A source
|
||||
// whose connectors are all culled (below MIN_WMID) is dropped itself.
|
||||
const bySource = new Map() // source -> pairs, sorted by total incoming count
|
||||
for (const p of liveSources.filter((p) => p.in >= minCount)) {
|
||||
const g = bySource.get(p.source) || []
|
||||
@@ -781,6 +804,8 @@ function buildExternal({ sources, exits }, byPath, innerBounds, visualScale = 1)
|
||||
}))
|
||||
.sort((a, b) => b.total - a.total)
|
||||
.slice(0, MAX_EXT_IN)
|
||||
.filter(({ ps }) =>
|
||||
ps.some((p) => !byPath.get(p.page).hidden && width(p.in) >= MIN_WMID))
|
||||
if (origins.length) {
|
||||
const cx = (innerBounds.x0 + innerBounds.x1) / 2
|
||||
const y = innerBounds.y0 - TNODE_BOUND - EXT_GAP
|
||||
@@ -802,7 +827,7 @@ function buildExternal({ sources, exits }, byPath, innerBounds, visualScale = 1)
|
||||
const page = byPath.get(p.page)
|
||||
if (page.hidden) continue
|
||||
const wMid = width(p.in)
|
||||
if (wMid <= 0) continue
|
||||
if (wMid < MIN_WMID) continue
|
||||
edges.push(buildRibbon(xn, page, p.in, 0, wMid, true))
|
||||
flows.push(...buildFlows(xn, page, p.in, 0, visualScale))
|
||||
}
|
||||
@@ -813,7 +838,8 @@ function buildExternal({ sources, exits }, byPath, innerBounds, visualScale = 1)
|
||||
// the same domain stay distinct), showing the total count across all
|
||||
// pages linking to it, in a row centered below the map (hottest
|
||||
// first), mirroring the source row above. Each (URL, page) pair
|
||||
// contributes an edge from that page.
|
||||
// contributes an edge from that page. An exit whose connectors are all
|
||||
// culled (below MIN_WMID) is dropped itself.
|
||||
const byExt = new Map() // full URL -> { ext, out, pairs }
|
||||
for (const p of liveExits.filter((p) => p.out >= minCount)) {
|
||||
const g = byExt.get(p.ext) || { ext: p.ext, out: 0, pairs: [] }
|
||||
@@ -824,6 +850,8 @@ function buildExternal({ sources, exits }, byPath, innerBounds, visualScale = 1)
|
||||
const targets = [...byExt.values()]
|
||||
.sort((a, b) => b.out - a.out)
|
||||
.slice(0, MAX_EXT_OUT)
|
||||
.filter(({ pairs }) =>
|
||||
pairs.some((p) => !byPath.get(p.page).hidden && width(p.out) >= MIN_WMID))
|
||||
if (targets.length) {
|
||||
const cx = (innerBounds.x0 + innerBounds.x1) / 2
|
||||
const y = innerBounds.y1 + TNODE_BOUND + EXT_GAP
|
||||
@@ -844,7 +872,7 @@ function buildExternal({ sources, exits }, byPath, innerBounds, visualScale = 1)
|
||||
const page = byPath.get(p.page)
|
||||
if (page.hidden) continue
|
||||
const wMid = width(p.out)
|
||||
if (wMid <= 0) continue
|
||||
if (wMid < MIN_WMID) continue
|
||||
edges.push(buildRibbon(page, xn, p.out, 0, wMid, true))
|
||||
flows.push(...buildFlows(page, xn, p.out, 0, visualScale))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user