'use client' import { useMemo } from 'react' import { useTheme } from '@ciphera-net/ui' import { TreeStructure } from '@phosphor-icons/react' import { createTheme, ThemeProvider } from '@mui/material/styles' import { SankeyDataProvider, SankeyLinkPlot, SankeyNodePlot, SankeyNodeLabelPlot, SankeyTooltip, } from '@mui/x-charts-pro/SankeyChart' import { ChartsWrapper } from '@mui/x-charts-pro/ChartsWrapper' import { ChartsSurface } from '@mui/x-charts-pro/ChartsSurface' import type { PathTransition } from '@/lib/api/journeys' // ─── Types ────────────────────────────────────────────────────────── interface SankeyDiagramProps { transitions: PathTransition[] totalSessions: number depth: number onNodeClick?: (path: string) => void } // ─── Data transformation ──────────────────────────────────────────── const NODE_COLOR = '#FD5E0F' const EXIT_COLOR = '#595b63' function transformToSankeyData(transitions: PathTransition[], depth: number) { const numColumns = depth + 1 const nodeMap = new Map() const links: { source: string; target: string; value: number }[] = [] // Track flow in/out per node to compute exits const flowIn = new Map() const flowOut = new Map() for (const t of transitions) { if (t.step_index >= numColumns || t.step_index + 1 >= numColumns) continue const fromId = `${t.step_index}:${t.from_path}` const toId = `${t.step_index + 1}:${t.to_path}` if (!nodeMap.has(fromId)) { nodeMap.set(fromId, { id: fromId, label: t.from_path, color: NODE_COLOR }) } if (!nodeMap.has(toId)) { nodeMap.set(toId, { id: toId, label: t.to_path, color: NODE_COLOR }) } links.push({ source: fromId, target: toId, value: t.session_count }) flowOut.set(fromId, (flowOut.get(fromId) ?? 0) + t.session_count) flowIn.set(toId, (flowIn.get(toId) ?? 0) + t.session_count) } // Add exit nodes for flows that don't continue for (const [nodeId, node] of nodeMap) { const totalIn = flowIn.get(nodeId) ?? 0 const totalOut = flowOut.get(nodeId) ?? 0 const flow = Math.max(totalIn, totalOut) const exitCount = flow - totalOut if (exitCount > 0) { const col = parseInt(nodeId.split(':')[0], 10) if (col < numColumns - 1) { const exitId = `${col + 1}:(exit)` if (!nodeMap.has(exitId)) { nodeMap.set(exitId, { id: exitId, label: '(exit)', color: EXIT_COLOR }) } links.push({ source: nodeId, target: exitId, value: exitCount }) } } } return { nodes: Array.from(nodeMap.values()), links, } } const valueFormatter = (value: number, context: { type: string }) => { if (context.type === 'link') { return `${value.toLocaleString()} sessions` } return `${value.toLocaleString()} sessions total` } // ─── Component ────────────────────────────────────────────────────── export default function SankeyDiagram({ transitions, totalSessions, depth, onNodeClick, }: SankeyDiagramProps) { const { resolvedTheme } = useTheme() const isDark = resolvedTheme === 'dark' const muiTheme = useMemo( () => createTheme({ palette: { mode: isDark ? 'dark' : 'light' }, }), [isDark], ) const data = useMemo( () => transformToSankeyData(transitions, depth), [transitions, depth], ) if (!transitions.length || !data.links.length) { return (

No journey data yet

Navigation flows will appear here as visitors browse through your site.

) } return (
) }