ui: handle minor paths on frontend for path/sankey

This commit is contained in:
nick-delirium 2024-12-23 16:19:59 +01:00
parent bfb3557508
commit eb24893b2d
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
4 changed files with 150 additions and 5 deletions

View file

@ -33,6 +33,7 @@ import CustomMetricTableErrors from 'App/components/Dashboard/Widgets/CustomMetr
import ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard';
import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard';
import SankeyChart from 'Shared/Insights/SankeyChart';
import { filterMinorPaths } from 'Shared/Insights/SankeyChart/utils'
import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard';
import SessionsBy from 'Components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy';
import { useInView } from 'react-intersection-observer';
@ -120,7 +121,7 @@ function WidgetChart(props: Props) {
..._metric.series,
..._metric.excludes,
..._metric.startPoint,
hideExcess: _metric.hideExcess,
hideExcess: false,
});
const fetchMetricChartData = (
metric: any,
@ -473,11 +474,12 @@ function WidgetChart(props: Props) {
}
if (metricType === USER_PATH && data && data.links) {
// return <PathAnalysis data={data}/>;
const usedData = _metric.hideExcess ? filterMinorPaths(data) : data;
return (
<SankeyChart
height={props.isPreview ? 500 : 240}
data={data}
data={usedData}
iterations={_metric.hideExcess ? 0 : 128}
onChartClick={(filters: any) => {
dashboardStore.drillDownFilter.merge({ filters, page: 1 });
}}

View file

@ -44,6 +44,7 @@ function WidgetOptions() {
onClick={(e) => {
e.preventDefault();
metric.update({ hideExcess: !metric.hideExcess });
metric.updateKey('hasChanged', true);
}}
>
<Space>
@ -113,6 +114,7 @@ const SeriesTypeOptions = observer(({ metric }: { metric: any }) => {
})),
onClick: ({ key }: any) => {
metric.updateKey('metricOf', key);
metric.updateKey('hasChanged', true)
},
}}
>

View file

@ -29,9 +29,10 @@ interface Props {
nodeWidth?: number;
height?: number;
onChartClick?: (filters: any[]) => void;
iterations: number
}
const SankeyChart: React.FC<Props> = ({data, height = 240, onChartClick}: Props) => {
const SankeyChart: React.FC<Props> = ({data, height = 240, onChartClick, iterations}: Props) => {
const [highlightedLinks, setHighlightedLinks] = useState<string[]>([]);
const [hoveredLinks, setHoveredLinks] = useState<string[]>([]);
@ -115,7 +116,7 @@ const SankeyChart: React.FC<Props> = ({data, height = 240, onChartClick}: Props)
nodePadding={20}
sort={true}
nodeWidth={4}
iterations={128}
iterations={iterations}
// linkCurvature={0.9}
onClick={clickHandler}
link={({source, target, id, ...linkProps}, index) => (

View file

@ -0,0 +1,140 @@
interface Link {
eventType: 'string',
sessionsCount: number,
value: number,
avgTimeFromPrevious: any,
/**
* index in array of nodes
* */
source: number,
/**
* index in array of nodes
* */
target: number,
id: string,
}
interface DataNode {
name: string,
eventType: 'string',
avgTimeFromPrevious: any,
id: string,
}
export function filterMinorPaths(data: { links: Link[], nodes: DataNode[] }, startNode: number = 0): Data {
const original: { links: Link[], nodes: DataNode[] } = JSON.parse(JSON.stringify(data));
const eventType = data.nodes[startNode].eventType;
const sourceLinks: Map<number, Link[]> = new Map();
for (const link of original.links) {
if (!sourceLinks.has(link.source)) {
sourceLinks.set(link.source, []);
}
sourceLinks.get(link.source)!.push(link);
}
const visited: Set<number> = new Set([startNode]);
const queue: number[] = [startNode];
const newNodes: Node[] = [];
const oldToNewMap: Map<number, number> = new Map();
const otherIndexMap: Map<number, number> = new Map();
function getNewIndexForNode(oldIndex: number): number {
if (oldToNewMap.has(oldIndex)) {
return oldToNewMap.get(oldIndex)!;
}
const oldNode = original.nodes[oldIndex];
const newIndex = newNodes.length;
newNodes.push({ ...oldNode });
oldToNewMap.set(oldIndex, newIndex);
return newIndex;
}
function getOtherIndexForNode(oldIndex: number): number {
if (otherIndexMap.has(oldIndex)) {
return otherIndexMap.get(oldIndex)!;
}
const newIndex = newNodes.length;
newNodes.push({
name: 'Dropoff',
eventType: eventType,
avgTimeFromPrevious: null,
idd: `other_${oldIndex}`,
});
otherIndexMap.set(oldIndex, newIndex);
return newIndex;
}
const newLinks: Link[] = [];
while (queue.length) {
const current = queue.shift()!;
const outLinks = sourceLinks.get(current) || [];
if (!outLinks.length) continue;
const majorLink = outLinks.reduce((prev, curr) => (curr.value > prev.value ? curr : prev), outLinks[0]);
const minorSessionsSum = outLinks.reduce((sum, link) => link !== majorLink ? sum + (link.sessionsCount || 0) : sum, 0);
const minorValueSum = outLinks.reduce((sum, link) => link !== majorLink ? sum + (link.value || 0) : sum, 0);
if (majorLink) {
const newSource = getNewIndexForNode(majorLink.source);
const newTarget = getNewIndexForNode(majorLink.target);
newLinks.push({
...majorLink,
source: newSource,
target: newTarget,
});
if (!visited.has(majorLink.target)) {
visited.add(majorLink.target);
queue.push(majorLink.target);
}
}
if (minorValueSum > 0) {
const newSource = getNewIndexForNode(current);
const newTarget = getOtherIndexForNode(current);
newLinks.push({
eventType: eventType,
sessionsCount: minorSessionsSum,
value: minorValueSum,
avgTimeFromPrevious: null,
source: newSource,
target: newTarget,
id: `other-${current}`,
});
}
}
const dropoffIndices: number[] = [];
const normalIndices: number[] = [];
for (let i = 0; i < newNodes.length; i++) {
if (newNodes[i].name === 'Dropoff') {
dropoffIndices.push(i);
} else {
normalIndices.push(i);
}
}
const finalOrder = normalIndices.concat(dropoffIndices);
const indexMap: Map<number, number> = new Map();
finalOrder.forEach((oldIndex, sortedPos) => {
indexMap.set(oldIndex, sortedPos);
});
const sortedNodes = finalOrder.map((idx) => newNodes[idx]);
for (const link of newLinks) {
link.source = indexMap.get(link.source)!;
link.target = indexMap.get(link.target)!;
}
return {
...original,
nodes: sortedNodes,
links: newLinks,
};
}