diff --git a/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx b/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx index 651a496f4..c08f94785 100644 --- a/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx +++ b/frontend/app/components/shared/Insights/SankeyChart/CustomLink.tsx @@ -3,11 +3,23 @@ import { Layer, Rectangle } from 'recharts'; function CustomLink(props: any) { const [fill, setFill] = React.useState('url(#linkGradient)'); - const { payload, sourceX, targetX, sourceY, targetY, sourceControlX, targetControlX, linkWidth, index, activeLink } = + const { + hoveredLinks, + activeLinks, + payload, + sourceX, + targetX, + sourceY, + targetY, + sourceControlX, + targetControlX, + linkWidth, + index, + activeLink + } = props; - const activeSource = activeLink?.payload.source; - const activeTarget = activeLink?.payload.target; - const isActive = activeSource?.name === payload.source.name && activeTarget?.name === payload.target.name; + const isActive = activeLinks.length > 1 && activeLinks.includes(payload.id); + const isHover = hoveredLinks.length > 1 && hoveredLinks.includes(payload.id); const onClick = () => { if (props.onClick) { @@ -34,7 +46,7 @@ function CustomLink(props: any) { ${sourceX},${sourceY - linkWidth / 2} Z `} - fill={isActive ? 'rgba(57, 78, 255, 0.5)' : fill} + fill={isActive ? 'rgba(57, 78, 255, 1)' : (isHover ? 'rgba(57, 78, 255, 0.5)' : fill)} strokeWidth='1' strokeOpacity={props.strokeOpacity} onMouseEnter={() => { diff --git a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx index 5c0fc3ec4..07261578d 100644 --- a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx +++ b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx @@ -27,40 +27,91 @@ interface Props { data: Data; nodeWidth?: number; height?: number; + onChartClick?: (filters: any[]) => void; } const SankeyChart: React.FC = ({ data, - height = 240 + height = 240, + onChartClick }: Props) => { const [highlightedLinks, setHighlightedLinks] = useState([]); + const [hoveredLinks, setHoveredLinks] = useState([]); + + function buildReversedAdjacencyList(nodes, links) { + const adjList = Array(nodes.length).fill(null).map(() => []); + + for (const link of links) { + adjList[link.target].push(link.source); + } + + return adjList; + } + + function dfs(adjList, start, target, visited, path) { + if (start === target) return [...path, start]; + + if (visited[start]) return null; + + visited[start] = true; + + for (const neighbor of adjList[start]) { + const newPath = dfs(adjList, neighbor, target, visited, [...path, start]); + if (newPath) return newPath; + } + + return null; + } + + function findPathFromLinkId(linkId) { + const startNodeIndex = data.links.findIndex(link => link.id === linkId); + + if (startNodeIndex === -1) { + return null; + } + + const adjList = buildReversedAdjacencyList(data.nodes, data.links); + const visited = Array(data.nodes.length).fill(false); + return dfs(adjList, startNodeIndex, 0, visited, []); + } const handleLinkMouseEnter = (linkData: any) => { const { payload } = linkData; - const fullPathArray: Node[] = []; - console.log('linkData', linkData.index); - - // Add the source node of the current link - fullPathArray.push(payload.source); - fullPathArray.push(payload.target); + const pathFromLinkId = findPathFromLinkId(payload.id); + setHoveredLinks(pathFromLinkId); - if (payload.source.sourceLinks.length > 0) { - let prevLink = data.links[payload.source.sourceLinks[0]]; - // fullPathArray.unshift(prevLink); - fullPathArray.unshift(data.nodes[prevLink.source]); + }; - if (prevLink.source) { - let prevLinkPrev = data.links[prevLink.source]; - // fullPathArray.unshift(prevLinkPrev); - fullPathArray.unshift(data.nodes[prevLinkPrev.source]); - } + const clickHandler = () => { + setHighlightedLinks(hoveredLinks); + + const targetLink = data.links[hoveredLinks[0]]; + const sourceLink = data.links[hoveredLinks[hoveredLinks.length - 1]]; + const targetNode = data.nodes[targetLink.source]; + const sourceNode = data.nodes[sourceLink.target]; + + const filters = []; + if (sourceNode) { + filters.push({ + operator: 'is', + type: sourceNode.eventType, + value: [sourceNode.name], + isEvent: true + }); } - setHighlightedLinks(fullPathArray); + if (targetNode) { + filters.push({ + operator: 'is', + type: targetNode.eventType, + value: [targetNode.name], + isEvent: true + }); + } - console.log('fullPathArray', fullPathArray.map(node => node)); + onChartClick?.(filters); }; @@ -76,15 +127,15 @@ const SankeyChart: React.FC = ({ iterations={128} node={} sort={true} - onClick={(data) => { - - }} + onClick={clickHandler} link={({ source, target, ...linkProps }, index) => ( data.links[linkId].id)} + activeLinks={highlightedLinks.map(linkId => data.links[linkId].id)} strokeOpacity={highlightedLinks.includes(index) ? 1 : 0.2} onMouseEnter={() => handleLinkMouseEnter(linkProps)} - onMouseLeave={() => setHighlightedLinks([])} + onMouseLeave={() => setHoveredLinks([])} /> )} margin={{ right: 200 }} diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 4f9bc078a..daf46e59c 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -261,8 +261,13 @@ export default class Widget { const _data: any = { ...data }; if (this.metricType === USER_PATH) { + _data['nodes'] = data.nodes.map((s: any) => ({ + ...s, + idd: Math.random().toString(36).substring(7), + })); _data['links'] = data.links.map((s: any) => ({ - ...s + ...s, + id: Math.random().toString(36).substring(7), // value: Math.round(s.value), }));