mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(dashboard): add dagre auto-layout for hierarchical graph visualization
Replace grid-based node positioning with dagre-powered hierarchical layout that computes positions based on edge direction (top-to-bottom). This produces clean, readable graphs instead of the previous chaotic grid pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,10 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^2.0.4",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@understand-anything/core": "workspace:*",
|
||||
"@xyflow/react": "^12.0.0",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"zustand": "^5.0.0"
|
||||
|
||||
@@ -13,6 +13,7 @@ import "@xyflow/react/dist/style.css";
|
||||
import CustomNode from "./CustomNode";
|
||||
import type { CustomFlowNode } from "./CustomNode";
|
||||
import { useDashboardStore } from "../store";
|
||||
import { applyDagreLayout } from "../utils/layout.ts";
|
||||
|
||||
const nodeTypes = { custom: CustomNode };
|
||||
|
||||
@@ -22,15 +23,13 @@ export default function GraphView() {
|
||||
const searchResults = useDashboardStore((s) => s.searchResults);
|
||||
const selectNode = useDashboardStore((s) => s.selectNode);
|
||||
|
||||
const initialNodes = useMemo<CustomFlowNode[]>(() => {
|
||||
if (!graph) return [];
|
||||
return graph.nodes.map((node, i) => ({
|
||||
const { initialNodes, initialEdges } = useMemo(() => {
|
||||
if (!graph) return { initialNodes: [] as CustomFlowNode[], initialEdges: [] as Edge[] };
|
||||
|
||||
const flowNodes: CustomFlowNode[] = graph.nodes.map((node) => ({
|
||||
id: node.id,
|
||||
type: "custom" as const,
|
||||
position: {
|
||||
x: (i % 3) * 300 + 50,
|
||||
y: Math.floor(i / 3) * 200 + 50,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: node.name,
|
||||
nodeType: node.type,
|
||||
@@ -40,11 +39,8 @@ export default function GraphView() {
|
||||
isSelected: selectedNodeId === node.id,
|
||||
},
|
||||
}));
|
||||
}, [graph, searchResults, selectedNodeId]);
|
||||
|
||||
const initialEdges = useMemo<Edge[]>(() => {
|
||||
if (!graph) return [];
|
||||
return graph.edges.map((edge, i) => ({
|
||||
const flowEdges: Edge[] = graph.edges.map((edge, i) => ({
|
||||
id: `e-${i}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
@@ -53,7 +49,13 @@ export default function GraphView() {
|
||||
style: { stroke: "#6b7280", strokeWidth: 1.5 },
|
||||
labelStyle: { fill: "#9ca3af", fontSize: 10 },
|
||||
}));
|
||||
}, [graph]);
|
||||
|
||||
const laid = applyDagreLayout(flowNodes, flowEdges);
|
||||
return {
|
||||
initialNodes: laid.nodes as CustomFlowNode[],
|
||||
initialEdges: laid.edges,
|
||||
};
|
||||
}, [graph, searchResults, selectedNodeId]);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import dagre from "@dagrejs/dagre";
|
||||
import type { Node, Edge } from "@xyflow/react";
|
||||
|
||||
const NODE_WIDTH = 280;
|
||||
const NODE_HEIGHT = 120;
|
||||
|
||||
export function applyDagreLayout(
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
direction: "TB" | "LR" = "TB",
|
||||
): { nodes: Node[]; edges: Edge[] } {
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
g.setGraph({
|
||||
rankdir: direction,
|
||||
nodesep: 60,
|
||||
ranksep: 80,
|
||||
marginx: 20,
|
||||
marginy: 20,
|
||||
});
|
||||
|
||||
nodes.forEach((node) => {
|
||||
g.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||
});
|
||||
|
||||
edges.forEach((edge) => {
|
||||
g.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
dagre.layout(g);
|
||||
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
const pos = g.node(node.id);
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: pos.x - NODE_WIDTH / 2,
|
||||
y: pos.y - NODE_HEIGHT / 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return { nodes: layoutedNodes, edges };
|
||||
}
|
||||
Generated
+15
@@ -45,6 +45,9 @@ importers:
|
||||
|
||||
packages/dashboard:
|
||||
dependencies:
|
||||
'@dagrejs/dagre':
|
||||
specifier: ^2.0.4
|
||||
version: 2.0.4
|
||||
'@monaco-editor/react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -171,6 +174,12 @@ packages:
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@dagrejs/dagre@2.0.4':
|
||||
resolution: {integrity: sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA==}
|
||||
|
||||
'@dagrejs/graphlib@3.0.4':
|
||||
resolution: {integrity: sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1510,6 +1519,12 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@dagrejs/dagre@2.0.4':
|
||||
dependencies:
|
||||
'@dagrejs/graphlib': 3.0.4
|
||||
|
||||
'@dagrejs/graphlib@3.0.4': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
optional: true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user