feat: show exit count when selecting a page, fix scroll fade overlay
This commit is contained in:
@@ -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<ExitLabel[]>([])
|
||||
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 (
|
||||
<svg
|
||||
@@ -354,6 +411,19 @@ function ConnectionLines({
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{exits.map((exit, i) => (
|
||||
<g key={`exit-${i}`}>
|
||||
<text
|
||||
x={exit.x + 4}
|
||||
y={exit.y + 4}
|
||||
fontSize={11}
|
||||
fontFamily="system-ui, -apple-system, sans-serif"
|
||||
fill={exit.color}
|
||||
>
|
||||
(exit) {exit.count}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -472,7 +542,7 @@ export default function ColumnJourney({
|
||||
</div>
|
||||
{/* Scroll fade indicator */}
|
||||
{canScrollRight && (
|
||||
<div className="absolute top-0 right-0 bottom-0 w-16 pointer-events-none bg-gradient-to-l from-white dark:from-neutral-900 to-transparent" />
|
||||
<div className="absolute top-0 right-0 bottom-0 w-10 pointer-events-none bg-gradient-to-l from-white/90 dark:from-neutral-900/90 to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user