ui: better drop handling, create list of ids for hover and select

events
This commit is contained in:
nick-delirium 2025-05-05 16:06:21 +02:00
parent c82820dbbb
commit 1062f3c2e2
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
3 changed files with 72 additions and 33 deletions

View file

@ -2,41 +2,31 @@ import React from 'react';
import type { Data } from '../SankeyChart';
function DroppedSessionsList({
data,
colorMap,
onHover,
dropsByUrl,
onLeave,
}: {
data: Data;
colorMap: Map<string, string>;
onHover: (dataIndex: any[]) => void;
dropsByUrl: Record<string, { drop: number, ids: any[] }> | null;
onLeave: () => void;
}) {
const dropsByUrl: Record<string, number> = {};
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 (
<div className="bg-white rounded-lg border shadow p-4 h-fit min-w-[210px] w-1/3">
<h3 className="text-lg font-medium mb-3">Sessions Drop by Page</h3>
@ -46,6 +36,8 @@ function DroppedSessionsList({
<div
key={index}
className="py-1.5 px-2 hover:bg-gray-50 rounded transition-colors flex justify-between gap-2 relative"
onMouseEnter={() => onHover(item.ids)}
onMouseLeave={() => onLeave()}
>
<div
style={{
@ -59,7 +51,7 @@ function DroppedSessionsList({
}}
/>
<span className="truncate flex-1">{item.url}</span>
<span className="ml-2"> {item.count}</span>
<span className="ml-2"> {item.drop}</span>
<span className="text-gray-400">({item.percentage}%)</span>
</div>
))

View file

@ -16,13 +16,15 @@ const EChartsSunburst = (props: Props) => {
const { data, height = 240 } = props;
const chartRef = React.useRef<HTMLDListElement>(null);
const [colors, setColors] = React.useState<Map<string, string>>(new Map());
const [chartInst, setChartInst] = React.useState<echarts.ECharts | null>(null);
const [dropsByUrl, setDropsByUrl] = React.useState<any>(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 (
<div
style={{
@ -82,7 +103,7 @@ const EChartsSunburst = (props: Props) => {
style={containerStyle}
className="min-w-[600px] relative"
/>
<DroppedSessionsList colorMap={colors} data={data} />
<DroppedSessionsList dropsByUrl={dropsByUrl} onHover={onHover} onLeave={onLeave} colorMap={colors} data={data} />
</div>
);
};

View file

@ -14,6 +14,7 @@ const colorMap = new Map();
export function convertSankeyToSunburst(data: Data): {
tree: SunburstChild;
colors: Map<string, string>;
dropsByUrl: Record<string, { drop: number, ids: any[] }>;
} {
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<number, (typeof nodesCopy)[number]> = {};
const dropsByUrl: Record<string, { drop: number, ids: any[] }> = {};
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<string, number> = {};
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<string, string>) {
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 `
<div class="flex flex-col bg-white p-2 rounded shadow border">