From 1062f3c2e2fa7187f4a3d9ab773358a230c71db5 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Mon, 5 May 2025 16:06:21 +0200 Subject: [PATCH] ui: better drop handling, create list of ids for hover and select events --- .../Charts/SunburstChart/DroppedSessions.tsx | 42 ++++++++----------- .../Charts/SunburstChart/Sunburst.tsx | 27 ++++++++++-- .../Charts/SunburstChart/sunburstUtils.ts | 36 +++++++++++++--- 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/frontend/app/components/Charts/SunburstChart/DroppedSessions.tsx b/frontend/app/components/Charts/SunburstChart/DroppedSessions.tsx index 08474e55e..b1f58f99b 100644 --- a/frontend/app/components/Charts/SunburstChart/DroppedSessions.tsx +++ b/frontend/app/components/Charts/SunburstChart/DroppedSessions.tsx @@ -2,41 +2,31 @@ import React from 'react'; import type { Data } from '../SankeyChart'; function DroppedSessionsList({ - data, colorMap, + onHover, + dropsByUrl, + onLeave, }: { - data: Data; colorMap: Map; + onHover: (dataIndex: any[]) => void; + dropsByUrl: Record | null; + onLeave: () => void; }) { - const dropsByUrl: Record = {}; - - data.links.forEach((link) => { - const targetNode = data.nodes.find((node) => node.id === link.target); - const sourceNode = data.nodes.find((node) => node.id === link.source); - if (!targetNode || !sourceNode) return; - - const isDrop = targetNode.eventType === 'DROP'; - if (!isDrop) return; - - const sourceUrl = sourceNode.name; - console.log(link, sourceUrl, dropsByUrl); - if (sourceUrl) { - dropsByUrl[sourceUrl] = (dropsByUrl[sourceUrl] || 0) + link.sessionsCount; - } - }); - + console.log(colorMap, dropsByUrl) + if (!dropsByUrl) return null; const totalDropSessions = Object.values(dropsByUrl).reduce( - (sum, count) => sum + count, + (sum, { drop }) => sum + drop, 0, ); const sortedDrops = Object.entries(dropsByUrl) - .map(([url, count]) => ({ + .map(([url, { drop, ids }]) => ({ url, - count, - percentage: Math.round((count / totalDropSessions) * 100), + drop, + ids, + percentage: Math.round((drop / totalDropSessions) * 100), })) - .sort((a, b) => b.count - a.count); + .sort((a, b) => b.drop - a.drop); return (

Sessions Drop by Page

@@ -46,6 +36,8 @@ function DroppedSessionsList({
onHover(item.ids)} + onMouseLeave={() => onLeave()} >
{item.url} - {item.count} + {item.drop} ({item.percentage}%)
)) diff --git a/frontend/app/components/Charts/SunburstChart/Sunburst.tsx b/frontend/app/components/Charts/SunburstChart/Sunburst.tsx index e7df0f4c9..0fc8255fe 100644 --- a/frontend/app/components/Charts/SunburstChart/Sunburst.tsx +++ b/frontend/app/components/Charts/SunburstChart/Sunburst.tsx @@ -16,13 +16,15 @@ const EChartsSunburst = (props: Props) => { const { data, height = 240 } = props; const chartRef = React.useRef(null); const [colors, setColors] = React.useState>(new Map()); + const [chartInst, setChartInst] = React.useState(null); + const [dropsByUrl, setDropsByUrl] = React.useState(null); React.useEffect(() => { if (!chartRef.current || data.nodes.length === 0 || data.links.length === 0) return; const chart = echarts.init(chartRef.current); - const { tree, colors } = convertSankeyToSunburst(data); + const { tree, colors, dropsByUrl } = convertSankeyToSunburst(data); const singleRoot = data.nodes.reduce((acc, node) => { if (node.depth === 0) { @@ -30,11 +32,12 @@ const EChartsSunburst = (props: Props) => { } return acc; }, 0) === 1; + const finalData = singleRoot ? tree.children : [tree] const options = { ...defaultOptions, series: { type: 'sunburst', - data: singleRoot ? tree.children : [tree], + data: finalData, radius: [30, '90%'], itemStyle: { borderRadius: 6, @@ -50,13 +53,18 @@ const EChartsSunburst = (props: Props) => { }, }, }; + console.log(finalData) chart.setOption(options); const ro = new ResizeObserver(() => chart.resize()); ro.observe(chartRef.current); setColors(colors); + setChartInst(chart); + setDropsByUrl(dropsByUrl); return () => { chart.dispose(); ro.disconnect(); + setChartInst(null); + setDropsByUrl(null); }; }, [data, height]); @@ -65,6 +73,19 @@ const EChartsSunburst = (props: Props) => { height, flex: 1, }; + + const onHover = (dataIndex: any[]) => { + chartInst?.dispatchAction({ + type: 'highlight', + dataIndex, + }) + } + const onLeave = () => { + chartInst?.dispatchAction({ + type: 'downplay', + }) + } + return (
{ style={containerStyle} className="min-w-[600px] relative" /> - +
); }; diff --git a/frontend/app/components/Charts/SunburstChart/sunburstUtils.ts b/frontend/app/components/Charts/SunburstChart/sunburstUtils.ts index ca6ca99e7..df50fbad7 100644 --- a/frontend/app/components/Charts/SunburstChart/sunburstUtils.ts +++ b/frontend/app/components/Charts/SunburstChart/sunburstUtils.ts @@ -14,6 +14,7 @@ const colorMap = new Map(); export function convertSankeyToSunburst(data: Data): { tree: SunburstChild; colors: Map; + dropsByUrl: Record; } { const dataLinks = data.links.filter((link) => { const sourceNode = data.nodes.find((node) => node.id === link.source); @@ -34,8 +35,26 @@ export function convertSankeyToSunburst(data: Data): { })); const nodesById: Record = {}; + const dropsByUrl: Record = {}; + dataLinks.forEach((link) => { + const targetNode = nodesCopy.find((node) => node.id === link.target); + const sourceNode = nodesCopy.find((node) => node.id === link.source); + if (!targetNode || !sourceNode) return; + + const isDrop = targetNode.eventType === 'DROP'; + if (!isDrop) return; + + const sourceUrl = sourceNode.name; + if (sourceUrl) { + if (dropsByUrl[sourceUrl]) { + dropsByUrl[sourceUrl].drop = dropsByUrl[sourceUrl].drop + link.sessionsCount; + } else { + dropsByUrl[sourceUrl] = { drop: link.sessionsCount, ids: [] }; + } + } + }); nodesCopy.forEach((node) => { - nodesById[node.id as number] = node; + nodesById[node.id as number] = { ...node, dataIndex: node.id }; }); dataLinks.forEach((link) => { @@ -67,7 +86,6 @@ export function convertSankeyToSunburst(data: Data): { const rootNode = nodesById[0]; const nameCount: Record = {}; - let dataIndex = 0; function buildSunburstNode(node: SunburstChild): SunburstChild | null { if (!node) return null; // eventType === DROP @@ -76,22 +94,29 @@ export function convertSankeyToSunburst(data: Data): { // colorMap.set('DROP', 'black') return null; } + let color = colorMap.get(node.name); if (!color) { color = randomColor(colorMap.size); colorMap.set(node.name, color); } let nodeName; + if (node.name.includes('feature/sess')) { + console.log(node) + } if (nameCount[node.name]) { - nodeName = `${node.name}_$$$_${nameCount[node.name]++}`; + nodeName = `${node.name}_OPENREPLAY_NODE_${nameCount[node.name]++}`; } else { nodeName = node.name; nameCount[node.name] = 1; } + if (dropsByUrl[node.name]) { + dropsByUrl[node.name].ids.push(node.dataIndex); + } const result: SunburstChild = { name: nodeName, value: node.value || 0, - dataIndex: dataIndex++, + dataIndex: node.dataIndex, itemStyle: { color, }, @@ -109,6 +134,7 @@ export function convertSankeyToSunburst(data: Data): { return { tree: buildSunburstNode(rootNode) as SunburstChild, colors: colorMap, + dropsByUrl, }; } @@ -125,7 +151,7 @@ export function sunburstTooltip(colorMap: Map) { if ('name' in params.data) { const color = colorMap.get(params.data.name); const clearName = params.data.name - ? params.data.name.split('_$$$_')[0] + ? params.data.name.split('_OPENREPLAY_NODE_')[0] : 'Total'; return `