import { Column, Focusable, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen'; import classNames from 'classnames'; import { useMemo, useState } from 'react'; import { firstBy } from 'thenby'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useEscapeKey, useMessages, useResultQuery } from '@/components/hooks'; import { File } from '@/components/icons'; import { Lightning } from '@/components/svg'; import { objectToArray } from '@/lib/data'; import { formatLongNumber } from '@/lib/format'; import styles from './Journey.module.css'; const NODE_HEIGHT = 60; const NODE_GAP = 10; const LINE_WIDTH = 3; export interface JourneyProps { websiteId: string; startDate: Date; endDate: Date; steps: number; startStep?: string; endStep?: string; view: string; } const EVENT_TYPES = { views: 1, events: 2, }; export function Journey({ websiteId, steps, startStep, endStep, view }: JourneyProps) { const [selectedNode, setSelectedNode] = useState(null); const [activeNode, setActiveNode] = useState(null); const { formatMessage, labels } = useMessages(); const { data, error, isLoading } = useResultQuery('journey', { websiteId, steps, startStep, endStep, view, eventType: EVENT_TYPES[view], }); useEscapeKey(() => setSelectedNode(null)); const columns = useMemo(() => { if (!data) { return []; } const selectedPaths = selectedNode?.paths ?? []; const activePaths = activeNode?.paths ?? []; const columns = []; for (let columnIndex = 0; columnIndex < +steps; columnIndex++) { const nodes = {}; data.forEach(({ items, count }: any, nodeIndex: any) => { const name = items[columnIndex]; if (name) { const selected = !!selectedPaths.find(({ items }) => items[columnIndex] === name); const active = selected && !!activePaths.find(({ items }) => items[columnIndex] === name); if (!nodes[name]) { const paths = data.filter(({ items }) => items[columnIndex] === name); nodes[name] = { name, count, totalCount: count, nodeIndex, columnIndex, selected, active, paths, pathMap: paths.map(({ items, count }) => ({ [`${columnIndex}:${items.join(':')}`]: count, })), }; } else { nodes[name].totalCount += count; } } }); columns.push({ nodes: objectToArray(nodes).sort(firstBy('total', -1)), }); } columns.forEach((column, columnIndex) => { const nodes = column.nodes.map( ( currentNode: { totalCount: number; name: string; selected: boolean }, currentNodeIndex: any, ) => { const previousNodes = columns[columnIndex - 1]?.nodes; let selectedCount = previousNodes ? 0 : currentNode.totalCount; let activeCount = selectedCount; const lines = previousNodes?.reduce((arr: any[][], previousNode: any, previousNodeIndex: number) => { const fromCount = selectedNode?.paths.reduce((sum, path) => { if ( previousNode.name === path.items[columnIndex - 1] && currentNode.name === path.items[columnIndex] ) { sum += path.count; } return sum; }, 0); if (currentNode.selected && previousNode.selected && fromCount) { arr.push([previousNodeIndex, currentNodeIndex]); selectedCount += fromCount; if (previousNode.active) { activeCount += fromCount; } } return arr; }, []) || []; return { ...currentNode, selectedCount, activeCount, lines }; }, ); const visitorCount = nodes.reduce( (sum: number, { selected, selectedCount, active, activeCount, totalCount }) => { if (!selectedNode) { sum += totalCount; } else if (!activeNode && selectedNode && selected) { sum += selectedCount; } else if (activeNode && active) { sum += activeCount; } return sum; }, 0, ); const previousTotal = columns[columnIndex - 1]?.visitorCount ?? 0; const dropOff = previousTotal > 0 ? ((visitorCount - previousTotal) / previousTotal) * 100 : 0; Object.assign(column, { nodes, visitorCount, dropOff }); }); return columns; }, [data, selectedNode, activeNode]); const handleClick = (name: string, columnIndex: number, paths: any[]) => { if (name !== selectedNode?.name || columnIndex !== selectedNode?.columnIndex) { setSelectedNode({ name, columnIndex, paths }); } else { setSelectedNode(null); } setActiveNode(null); }; return (
{columns.map(({ visitorCount, nodes }, columnIndex) => { return (
{columnIndex + 1}
{formatLongNumber(visitorCount)} {formatMessage(labels.visitors)}
{nodes.map( ({ name, totalCount, selected, active, paths, activeCount, selectedCount, lines, }) => { const nodeCount = selected ? active ? activeCount : selectedCount : totalCount; const remaining = columnIndex > 0 ? Math.round((nodeCount / columns[columnIndex - 1]?.visitorCount) * 100) : 0; const dropped = 100 - remaining; return (
selected && setActiveNode({ name, columnIndex, paths }) } onMouseLeave={() => selected && setActiveNode(null)} >
handleClick(name, columnIndex, paths)} > {name.startsWith('/') ? : } {name}
{formatLongNumber(nodeCount)}
{`${dropped}% ${formatMessage(labels.dropoff)}`} {`${remaining}% ${formatMessage(labels.conversion)}`}
{columnIndex < columns.length && lines.map(([fromIndex, nodeIndex], i) => { const height = (Math.abs(nodeIndex - fromIndex) + 1) * (NODE_HEIGHT + NODE_GAP) - NODE_GAP; const midHeight = (Math.abs(nodeIndex - fromIndex) - 1) * (NODE_HEIGHT + NODE_GAP) + NODE_GAP + LINE_WIDTH; const nodeName = columns[columnIndex - 1]?.nodes[fromIndex].name; return (
path.items[columnIndex] === name && path.items[columnIndex - 1] === nodeName, ), [styles.up]: fromIndex < nodeIndex, [styles.down]: fromIndex > nodeIndex, [styles.flat]: fromIndex === nodeIndex, })} style={{ height }} >
); })}
); }, )}
); })}
); }