ui: handle minor paths on frontend for path/sankey
This commit is contained in:
parent
bfb3557508
commit
eb24893b2d
4 changed files with 150 additions and 5 deletions
|
|
@ -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 });
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
140
frontend/app/components/shared/Insights/SankeyChart/utils.ts
Normal file
140
frontend/app/components/shared/Insights/SankeyChart/utils.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue