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:
Lum1104
2026-03-14 18:35:52 +08:00
Unverified
parent a243a07d6f
commit dd252eddc0
4 changed files with 75 additions and 13 deletions
+2 -1
View File
@@ -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"
+14 -12
View File
@@ -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);
+44
View File
@@ -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 };
}
+15
View File
@@ -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