graph arranging stuff

This commit is contained in:
Kaushik Narayan R 2024-12-31 00:22:00 -07:00
parent ad25ce7f05
commit aa5579d855
4 changed files with 116 additions and 64 deletions

View File

@ -18,6 +18,7 @@ import Navbar from './components/Navbar';
import AllRoutes from './routes/AllRoutes'; import AllRoutes from './routes/AllRoutes';
import { showErrorToastNotification, showInfoToastNotification, showWarnToastNotification } from './components/ToastNotification'; import { showErrorToastNotification, showInfoToastNotification, showWarnToastNotification } from './components/ToastNotification';
import { apiAuthCheck, apiAuthRefresh } from './api/auth'; import { apiAuthCheck, apiAuthRefresh } from './api/auth';
import { ReactFlowProvider } from '@xyflow/react';
// Contexts // Contexts
export const WidthContext = createContext(); export const WidthContext = createContext();
@ -106,23 +107,25 @@ function App() {
<WidthContext.Provider value={width}> <WidthContext.Provider value={width}>
<AuthContext.Provider value={auth}> <AuthContext.Provider value={auth}>
<RefreshAuthContext.Provider value={refreshAuth}> <RefreshAuthContext.Provider value={refreshAuth}>
<div className={styles.app_wrapper}> <ReactFlowProvider>
<BrowserRouter> <div className={styles.app_wrapper}>
<ScrollToTop /> <BrowserRouter>
<Navbar /> <ScrollToTop />
<div className={styles.page_wrapper}> <Navbar />
<AllRoutes /> <div className={styles.page_wrapper}>
</div> <AllRoutes />
</BrowserRouter> </div>
<ToastContainer </BrowserRouter>
id={"notif-container"} <ToastContainer
position={"bottom-center"} id={"notif-container"}
theme={"dark"} position={"bottom-center"}
stacked theme={"dark"}
newestOnTop stacked
draggable newestOnTop
/> draggable
</div> />
</div>
</ReactFlowProvider>
</RefreshAuthContext.Provider> </RefreshAuthContext.Provider>
</AuthContext.Provider> </AuthContext.Provider>
</WidthContext.Provider> </WidthContext.Provider>

View File

@ -0,0 +1,9 @@
.btn_wrapper {
padding: var(--mb-2);
width: 100%;
cursor: pointer;
text-decoration: none;
color: var(--text);
box-shadow: 8px 8px var(--bg);
background-color: var(--bgLinkInactive);
}

View File

@ -1,11 +1,17 @@
import React from 'react' import React from 'react';
import styles from "./Button.module.css"; import styles from "./Button.module.css";
const Button = ({ child }) => { function Button({ children, onClickMethod }) {
const clickHandler = (e) => {
e.preventDefault();
onClickMethod();
}
return ( return (
<> <button type="button"
{child} className={styles.btn_wrapper}
</> onClick={clickHandler}>
{children}
</button>
) )
} }

View File

@ -6,18 +6,20 @@ import {
useNodesState, useNodesState,
useEdgesState, useEdgesState,
addEdge, addEdge,
Panel, useReactFlow,
MarkerType,
} from '@xyflow/react'; } from '@xyflow/react';
import Dagre from '@dagrejs/dagre'; import Dagre from '@dagrejs/dagre';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import styles from './Graph.module.css'; import styles from './Graph.module.css';
import { showErrorToastNotification, showInfoToastNotification, showSuccessToastNotification } from '../../components/ToastNotification'; import { showErrorToastNotification, showInfoToastNotification } from '../../components/ToastNotification';
import { apiFetchGraph } from '../../api/operations'; import { apiFetchGraph } from '../../api/operations';
import { RefreshAuthContext } from "../../App"; import { RefreshAuthContext } from "../../App";
import Button from '../../components/Button';
const initialNodes = []; const initialNodes = [];
// const initialNodes = [ // const initialNodes = [
@ -32,68 +34,84 @@ const initialEdges = [];
const nodeOffsets = { const nodeOffsets = {
connected: { connected: {
origin: {
x: 1000,
y: 0
},
scaling: {
x: 270,
y: 90
}
},
unconnected: {
origin: { origin: {
x: 0, x: 0,
y: 0 y: 0
}, },
scaling: { scaling: {
x: 180, x: 240,
y: 60 y: 80
}
},
unconnected: {
origin: {
x: 800,
y: 0
},
scaling: {
x: 160,
y: 40
} }
} }
} }
/** @type {import('@xyflow/react').DefaultEdgeOptions} */
const edgeOptions = { const edgeOptions = {
animated: true, animated: true,
style: { style: {
stroke: 'white', stroke: "white",
strokeWidth: 2,
}, },
markerEnd: {
type: MarkerType.ArrowClosed,
color: "white",
width: 40,
height: 40,
}
}; };
const proOptions = { hideAttribution: true }; const proOptions = { hideAttribution: true };
const Graph = () => { const Graph = () => {
const refreshAuth = useContext(RefreshAuthContext); const refreshAuth = useContext(RefreshAuthContext);
const flowInstance = useReactFlow();
const [playlistNodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [playlistNodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [linkEdges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [linkEdges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onFlowInit = (instance) => {
console.debug("flow loaded");
}
const onConnect = useCallback((params) => { const onConnect = useCallback((params) => {
setEdges((eds) => addEdge(params, eds)); setEdges((eds) => addEdge(params, eds));
console.debug("new connection");
console.debug(params);
}, [setEdges]); }, [setEdges]);
const getLayoutedElements = (nodes, edges, options = { direction: "TB" }) => { const getLayoutedElements = (nodes, edges, options = { direction: "TB" }) => {
const g = new Dagre.graphlib.Graph() const g = new Dagre.graphlib.Graph()
g.setDefaultEdgeLabel(() => ({})); g.setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: options.direction }); g.setGraph({ rankdir: options.direction, nodesep: 200, edgesep: 200, ranksep: 200 });
edges.forEach((edge) => g.setEdge(edge.source, edge.target)); edges.forEach((edge) => g.setEdge(edge.source, edge.target));
// const connectedNodes = new Set(edges.flatMap(edge => [edge.source, edge.target])); const connectedNodesID = new Set(edges.flatMap(edge => [edge.source, edge.target]));
// const unconnectedNodes = nodes.filter(node => !connectedNodes.has(node.id)); const connectedNodes = nodes.filter(node => connectedNodesID.has(node.id));
const unconnectedNodes = nodes.filter(node => !connectedNodesID.has(node.id));
nodes.forEach((node) => { nodes.forEach((node) => {
// if (connectedNodes.has(node.id)) {
g.setNode(node.id, { g.setNode(node.id, {
...node, ...node,
width: node.measured?.width ?? 0, width: node.measured?.width ?? 0,
height: node.measured?.height ?? 0, height: node.measured?.height ?? 0,
}) })
// }
}); });
Dagre.layout(g); Dagre.layout(g);
return { let finalLayout = { edges };
nodes: nodes.map((node) => { finalLayout.nodes = [
...connectedNodes.map((node) => {
const position = g.node(node.id); const position = g.node(node.id);
// We are shifting the dagre node position (anchor=center center) to the top left // 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). // so it matches the React Flow node anchor point (top left).
@ -102,10 +120,31 @@ const Graph = () => {
return { ...node, position: { x, y } }; return { ...node, position: { x, y } };
}), }),
edges, ...unconnectedNodes.map((node, idx) => {
}; 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;
return { ...node, position: { x, y } };
})
];
console.debug("layout generated");
return finalLayout;
}; };
const arrangeLayout = (direction) => {
const layouted = getLayoutedElements(playlistNodes, linkEdges, { direction });
setNodes([...layouted.nodes]);
setEdges([...layouted.edges]);
setTimeout(flowInstance.fitView);
console.debug("layout applied");
}
useEffect(() => { useEffect(() => {
const fetchGraph = async () => { const fetchGraph = async () => {
@ -120,13 +159,13 @@ const Graph = () => {
return { return {
id: `${pl.playlistID}`, id: `${pl.playlistID}`,
position: { position: {
x: nodeOffsets.unconnected.origin.x + Math.floor(idx / 5) * nodeOffsets.unconnected.scaling.x, x: nodeOffsets.unconnected.origin.x + Math.floor(idx / 15) * nodeOffsets.unconnected.scaling.x,
y: nodeOffsets.unconnected.origin.y + Math.floor(idx % 5) * nodeOffsets.unconnected.scaling.y, y: nodeOffsets.unconnected.origin.y + Math.floor(idx % 15) * nodeOffsets.unconnected.scaling.y,
}, },
data: { data: {
label: pl.playlistName, label: pl.playlistName,
meta: { metadata: {
name: pl.playlistName pl
} }
} }
} }
@ -152,16 +191,8 @@ const Graph = () => {
showErrorToastNotification(resp.data.message); showErrorToastNotification(resp.data.message);
return; return;
} }
fetchGraph(); fetchGraph();
}, []); }, [refreshAuth, setEdges, setNodes]);
const arrangeLayout = (direction) => {
const layouted = getLayoutedElements(playlistNodes, linkEdges, { direction });
setNodes([...layouted.nodes]);
setEdges([...layouted.edges]);
}
return ( return (
<div className={styles.graph_wrapper}> <div className={styles.graph_wrapper}>
@ -169,21 +200,24 @@ const Graph = () => {
nodes={playlistNodes} nodes={playlistNodes}
edges={linkEdges} edges={linkEdges}
defaultEdgeOptions={edgeOptions} defaultEdgeOptions={edgeOptions}
connectionLineType="smoothstep"
fitView fitView
proOptions={proOptions} proOptions={proOptions}
colorMode={"light"}
onInit={onFlowInit}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
> >
<Controls /> <Controls />
<Background variant='dots' gap={36} size={3} /> <Background variant='dots' gap={36} size={3} />
<Panel position="top-right"> {/* <Panel position="top-right"> */}
<button onClick={() => arrangeLayout('TB')}>Arrange vertically</button> {/* <button onClick={() => arrangeLayout('TB')}>Arrange vertically</button> */}
<button onClick={() => arrangeLayout('LR')}>Arrange horizontally</button> {/* <button onClick={() => arrangeLayout('LR')}>Arrange horizontally</button> */}
</Panel> {/* </Panel> */}
</ReactFlow> </ReactFlow>
<div className={styles.operations_wrapper}> <div className={styles.operations_wrapper}>
test <Button onClickMethod={() => arrangeLayout('TB')}>Arrange</Button>
</div> </div>
</div> </div>
) )