This commit is contained in:
Kaushik Narayan R 2025-01-08 10:21:41 -07:00
parent 4baedc3e60
commit e15575ce80

View File

@ -271,92 +271,97 @@ const Graph = () => {
type getLayoutedElementsOpts = { type getLayoutedElementsOpts = {
direction: rankdirType; direction: rankdirType;
}; };
const getLayoutedElements = ( const getLayoutedElements = useCallback(
nodes: Node[], (nodes: Node[], edges: Edge[], options: getLayoutedElementsOpts) => {
edges: Edge[], const g = new Dagre.graphlib.Graph();
options: getLayoutedElementsOpts g.setDefaultEdgeLabel(() => ({}));
) => { g.setGraph({
const g = new Dagre.graphlib.Graph(); rankdir: options.direction,
g.setDefaultEdgeLabel(() => ({})); nodesep: 100,
g.setGraph({ edgesep: 100,
rankdir: options.direction, ranksep: 100,
nodesep: 100,
edgesep: 100,
ranksep: 100,
});
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
const connectedNodesID = new Set(
edges.flatMap((edge) => [edge.source, edge.target])
);
const connectedNodes = nodes.filter((node) =>
connectedNodesID.has(node.id)
);
const unconnectedNodes = nodes.filter(
(node) => !connectedNodesID.has(node.id)
);
nodes.forEach((node) => {
g.setNode(node.id, {
...node,
width: node.measured?.width ?? 0,
height: node.measured?.height ?? 0,
}); });
});
Dagre.layout(g); edges.forEach((edge) => g.setEdge(edge.source, edge.target));
let finalLayout: { nodes: Node[]; edges: Edge[] } = { const connectedNodesID = new Set(
nodes: [], edges.flatMap((edge) => [edge.source, edge.target])
edges: [], );
}; const connectedNodes = nodes.filter((node) =>
connectedNodesID.has(node.id)
);
const unconnectedNodes = nodes.filter(
(node) => !connectedNodesID.has(node.id)
);
finalLayout.edges = [...edges]; nodes.forEach((node) => {
finalLayout.nodes.push( g.setNode(node.id, {
...connectedNodes.map((node) => { ...node,
const position = g.node(node.id); width: node.measured?.width ?? 0,
// We are shifting the dagre node position (anchor=center center) to the top left height: node.measured?.height ?? 0,
// so it matches the React Flow node anchor point (top left). });
const x = position.x - (node.measured?.width ?? 0) / 2; });
const y = position.y - (node.measured?.height ?? 0) / 2;
return { ...node, position: { x, y } }; Dagre.layout(g);
})
);
finalLayout.nodes.push( let finalLayout: { nodes: Node[]; edges: Edge[] } = {
...unconnectedNodes.map((node, idx) => { nodes: [],
const position = { edges: [],
x: };
nodeOffsets.unconnected.origin.x +
Math.floor(idx / 20) * nodeOffsets.unconnected.scaling.x,
y:
nodeOffsets.unconnected.origin.y +
Math.floor(idx % 20) * nodeOffsets.unconnected.scaling.y,
};
const x = position.x - (node.measured?.width ?? 0) / 2;
const y = position.y - (node.measured?.height ?? 0) / 2;
return { ...node, position: { x, y } }; finalLayout.edges = [...edges];
}) finalLayout.nodes.push(
); ...connectedNodes.map((node) => {
const position = g.node(node.id);
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
const x = position.x - (node.measured?.width ?? 0) / 2;
const y = position.y - (node.measured?.height ?? 0) / 2;
console.debug("layout generated"); return { ...node, position: { x, y } };
return finalLayout; })
}; );
const arrangeLayout = (direction: rankdirType) => { finalLayout.nodes.push(
const layouted = getLayoutedElements(playlistNodes, linkEdges, { ...unconnectedNodes.map((node, idx) => {
direction, const position = {
}); x:
nodeOffsets.unconnected.origin.x +
Math.floor(idx / 20) * nodeOffsets.unconnected.scaling.x,
y:
nodeOffsets.unconnected.origin.y +
Math.floor(idx % 20) * nodeOffsets.unconnected.scaling.y,
};
const x = position.x - (node.measured?.width ?? 0) / 2;
const y = position.y - (node.measured?.height ?? 0) / 2;
setPlaylistNodes([...layouted.nodes]); return { ...node, position: { x, y } };
setLinkEdges([...layouted.edges]); })
);
setTimeout(flowInstance.fitView); console.debug("layout generated");
console.debug("layout applied"); return finalLayout;
}; },
[]
);
const arrangeLayout = useCallback(
(direction: rankdirType) => {
// TODO: race condition
// states not updated in time inside other functions that call this before they call this
// fix that
const layouted = getLayoutedElements(playlistNodes, linkEdges, {
direction,
});
setPlaylistNodes([...layouted.nodes]);
setLinkEdges([...layouted.edges]);
setTimeout(flowInstance.fitView);
console.debug("layout applied");
},
[playlistNodes, linkEdges, flowInstance, getLayoutedElements]
);
const fetchGraph = useCallback(async () => { const fetchGraph = useCallback(async () => {
const resp = await APIWrapper({ apiFn: apiFetchGraph, refreshAuth }); const resp = await APIWrapper({ apiFn: apiFetchGraph, refreshAuth });
@ -364,7 +369,7 @@ const Graph = () => {
`graph fetched with ${resp?.data.playlists?.length} nodes and ${resp?.data.links?.length} edges` `graph fetched with ${resp?.data.playlists?.length} nodes and ${resp?.data.links?.length} edges`
); );
// place playlist nodes // place playlist nodes
setPlaylistNodes( const newNodes =
resp?.data.playlists?.map((pl, idx) => { resp?.data.playlists?.map((pl, idx) => {
return { return {
id: `${pl.playlistID}`, id: `${pl.playlistID}`,
@ -383,18 +388,18 @@ const Graph = () => {
}, },
}, },
}; };
}) ?? [] }) ?? [];
); setPlaylistNodes(newNodes);
// connect links // connect links
setLinkEdges( const newEdges =
resp?.data.links?.map((link, idx) => { resp?.data.links?.map((link, idx) => {
return { return {
id: `${link.from}->${link.to}`, id: `${link.from}->${link.to}`,
source: link.from, source: link.from,
target: link.to, target: link.to,
}; };
}) ?? [] }) ?? [];
); setLinkEdges(newEdges);
showInfoToastNotification("Graph updated."); showInfoToastNotification("Graph updated.");
}, [refreshAuth]); }, [refreshAuth]);
@ -403,7 +408,7 @@ const Graph = () => {
apiFn: apiUpdateUserData, apiFn: apiUpdateUserData,
refreshAuth, refreshAuth,
}); });
showInfoToastNotification("Spotify synced."); showInfoToastNotification(resp?.data.message);
if (resp?.data.removedLinks) if (resp?.data.removedLinks)
showWarnToastNotification( showWarnToastNotification(
"Some links with deleted playlists were removed." "Some links with deleted playlists were removed."
@ -413,7 +418,6 @@ const Graph = () => {
const refreshGraph = async () => { const refreshGraph = async () => {
await fetchGraph(); await fetchGraph();
arrangeLayout("TB");
}; };
useEffect(() => { useEffect(() => {