From b10abd38fc698ecea86384f009dab05591657184 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sun, 15 Mar 2026 12:56:59 +0100 Subject: [PATCH] feat: show exit count when selecting a page, fix scroll fade overlay --- components/journeys/ColumnJourney.tsx | 100 ++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 15 deletions(-) diff --git a/components/journeys/ColumnJourney.tsx b/components/journeys/ColumnJourney.tsx index 77e5771..c9c6e22 100644 --- a/components/journeys/ColumnJourney.tsx +++ b/components/journeys/ColumnJourney.tsx @@ -262,6 +262,13 @@ function JourneyColumn({ // ─── Connection Lines ─────────────────────────────────────────────── +interface ExitLabel { + x: number + y: number + count: number + color: string +} + function ConnectionLines({ containerRef, selections, @@ -274,12 +281,14 @@ function ConnectionLines({ transitions: PathTransition[] }) { const [lines, setLines] = useState<(LineDef & { color: string })[]>([]) + const [exits, setExits] = useState([]) const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) useLayoutEffect(() => { const container = containerRef.current if (!container || selections.size === 0) { setLines([]) + setExits([]) return } @@ -290,10 +299,10 @@ function ConnectionLines({ }) const newLines: (LineDef & { color: string })[] = [] + const newExits: ExitLabel[] = [] for (const [colIdx, selectedPath] of selections) { const nextCol = columns[colIdx + 1] - if (!nextCol) continue const sourceEl = container.querySelector( `[data-col="${colIdx}"][data-path="${CSS.escape(selectedPath)}"]` @@ -311,28 +320,76 @@ function ConnectionLines({ const color = colorForColumn(colIdx) - for (const t of relevantTransitions) { - const destEl = container.querySelector( - `[data-col="${colIdx + 1}"][data-path="${CSS.escape(t.to_path)}"]` - ) as HTMLElement | null - if (!destEl) continue + // Find total sessions for this page + const col = columns[colIdx] + const page = col?.pages.find((p) => p.path === selectedPath) + const pageCount = page?.sessionCount ?? 0 + const outboundCount = relevantTransitions.reduce((sum, t) => sum + t.session_count, 0) + const exitCount = pageCount - outboundCount - const destRect = destEl.getBoundingClientRect() - const destY = - destRect.top + destRect.height / 2 - containerRect.top + container.scrollTop - const destX = destRect.left - containerRect.left + container.scrollLeft + if (nextCol) { + const maxCount = Math.max( + ...relevantTransitions.map((rt) => rt.session_count), + exitCount > 0 ? exitCount : 0 + ) - const maxCount = Math.max(...relevantTransitions.map((rt) => rt.session_count)) - const weight = Math.max(1, Math.min(4, (t.session_count / maxCount) * 4)) + for (const t of relevantTransitions) { + const destEl = container.querySelector( + `[data-col="${colIdx + 1}"][data-path="${CSS.escape(t.to_path)}"]` + ) as HTMLElement | null + if (!destEl) continue - newLines.push({ sourceY, destY, sourceX, destX, weight, color }) + const destRect = destEl.getBoundingClientRect() + const destY = + destRect.top + destRect.height / 2 - containerRect.top + container.scrollTop + const destX = destRect.left - containerRect.left + container.scrollLeft + + const weight = maxCount > 0 + ? Math.max(1, Math.min(4, (t.session_count / maxCount) * 4)) + : 1 + + newLines.push({ sourceY, destY, sourceX, destX, weight, color }) + } + } + + // Show exit if any visitors dropped off + if (exitCount > 0) { + // Position the exit label below the last destination or below the source + const lastDestY = newLines.length > 0 + ? Math.max(...newLines.filter((l) => l.sourceX === sourceX).map((l) => l.destY)) + : sourceY + + const exitY = lastDestY + 30 + const exitX = nextCol + ? ((): number => { + // Find the left edge of the next column + const nextColEl = container.querySelector(`[data-col="${colIdx + 1}"]`) as HTMLElement | null + if (nextColEl) { + const nextRect = nextColEl.getBoundingClientRect() + return nextRect.left - containerRect.left + container.scrollLeft + } + return sourceX + 100 + })() + : sourceX + 100 + + newLines.push({ + sourceY, + destY: exitY, + sourceX, + destX: exitX, + weight: 1, + color: '#52525b', // EXIT_GREY + }) + + newExits.push({ x: exitX, y: exitY, count: exitCount, color: '#52525b' }) } } setLines(newLines) + setExits(newExits) }, [selections, columns, transitions, containerRef]) - if (lines.length === 0) return null + if (lines.length === 0 && exits.length === 0) return null return ( ) })} + {exits.map((exit, i) => ( + + + (exit) {exit.count} + + + ))} ) } @@ -472,7 +542,7 @@ export default function ColumnJourney({ {/* Scroll fade indicator */} {canScrollRight && ( -
+
)}
)