This is an automated email from the ASF dual-hosted git repository. skrawcz pushed a commit to branch stefan/remove_elkjs_update_backend in repository https://gitbox.apache.org/repos/asf/hamilton.git
commit 5921124f2de41372a7f32a1d55a1932f40f0d40c Author: Stefan Krawczyk <[email protected]> AuthorDate: Sun Oct 5 10:17:28 2025 -0700 Removes elkjs and adds dagre Need to test this more -- but basic graphs work. --- ui/frontend/package-lock.json | 34 +++- ui/frontend/package.json | 3 +- .../src/components/dashboard/Visualize/layout.ts | 176 ++++++++------------- 3 files changed, 93 insertions(+), 120 deletions(-) diff --git a/ui/frontend/package-lock.json b/ui/frontend/package-lock.json index 555be3b4..c7441d48 100644 --- a/ui/frontend/package-lock.json +++ b/ui/frontend/package-lock.json @@ -26,9 +26,9 @@ "chart.js": "^4.3.0", "chartjs-adapter-date-fns": "^3.0.0", "chartjs-plugin-zoom": "^2.0.1", + "dagre": "^0.8.5", "date-fns": "^2.30.0", "dayjs": "^1.11.9", - "elkjs": "^0.8.2", "fuse.js": "^6.6.2", "headlessui": "^0.0.0", "http-proxy-middleware": "^2.0.9", @@ -60,6 +60,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@types/dagre": "^0.7.52", "@types/react-syntax-highlighter": "^15.5.6", "@typescript-eslint/eslint-plugin": "^5.47.1", "@typescript-eslint/parser": "^5.47.1", @@ -4319,6 +4320,13 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/dagre": { + "version": "0.7.53", + "resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.53.tgz", + "integrity": "sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -7268,6 +7276,16 @@ "node": ">=12" } }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -7838,11 +7856,6 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz", "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==" }, - "node_modules/elkjs": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.8.2.tgz", - "integrity": "sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==" - }, "node_modules/emittery": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", @@ -9592,6 +9605,15 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", diff --git a/ui/frontend/package.json b/ui/frontend/package.json index c9080f86..e907cf48 100644 --- a/ui/frontend/package.json +++ b/ui/frontend/package.json @@ -22,8 +22,8 @@ "chartjs-adapter-date-fns": "^3.0.0", "chartjs-plugin-zoom": "^2.0.1", "date-fns": "^2.30.0", + "dagre": "^0.8.5", "dayjs": "^1.11.9", - "elkjs": "^0.8.2", "fuse.js": "^6.6.2", "headlessui": "^0.0.0", "http-proxy-middleware": "^2.0.9", @@ -89,6 +89,7 @@ ] }, "devDependencies": { + "@types/dagre": "^0.7.52", "@types/react-syntax-highlighter": "^15.5.6", "@typescript-eslint/eslint-plugin": "^5.47.1", "@typescript-eslint/parser": "^5.47.1", diff --git a/ui/frontend/src/components/dashboard/Visualize/layout.ts b/ui/frontend/src/components/dashboard/Visualize/layout.ts index e2cc0c48..9b5bd6d6 100644 --- a/ui/frontend/src/components/dashboard/Visualize/layout.ts +++ b/ui/frontend/src/components/dashboard/Visualize/layout.ts @@ -17,35 +17,17 @@ import { VizEdge, VizNode } from "./types"; -import ELK, { ElkNode } from "elkjs/lib/elk.bundled.js"; +import dagre from "dagre"; -const elk = new ELK(); +// Dagre graph instance +const dagreGraph = new dagre.graphlib.Graph({ compound: true }); -const convertGraphFromElk = ( - root: ElkNode, - nodeNameMap: Map<string, VizNode> -) => { - const output = [] as VizNode[]; - const queue = [root]; - while (queue.length > 0) { - const node = queue.shift() as ElkNode; - const vizNode = nodeNameMap.get(node.id); - if (vizNode !== undefined) { - (vizNode.position = { - x: node.x as number, // + PARENT_PADDING.left, - y: node.y as number, // + PARENT_PADDING.top, - }), - (vizNode.data.dimensions = { - width: node.width as number, // + PARENT_PADDING.left + PARENT_PADDING.right, - height: node.height as number, - // PARENT_PADDING.bottom + - // PARENT_PADDING.top, - }); - output.push(vizNode); - } - queue.push(...(node.children || [])); - } - return output; +const dagreOptions = { + rankdir: "TB", // Top to bottom layout (can be changed to "LR" for left-to-right) + nodesep: 80, // Node separation + ranksep: 100, // Rank separation + marginx: 25, + marginy: 25, }; export const getLayoutedElements = ( @@ -54,97 +36,65 @@ export const getLayoutedElements = ( nodeDimensions: Map<string, { width: number; height: number }>, vertical: boolean ) => { - // Organize every node by its parents - const nodesByParent = new Map<string, VizNode[]>([["root", []]]); + const direction = vertical ? "TB" : "LR"; + + // Configure dagre graph + dagreGraph.setDefaultEdgeLabel(() => ({})); + dagreGraph.setGraph({ + ...dagreOptions, + rankdir: direction, + }); + + // Clear previous graph state nodes.forEach((node) => { - const parent = node.parentNode || "root"; - if (!nodesByParent.has(parent)) { - nodesByParent.set(parent, []); + if (dagreGraph.hasNode(node.id)) { + dagreGraph.removeNode(node.id); } - nodesByParent.get(parent)?.push(node); }); - const buildGraph = (rootNode: VizNode): ElkNode => { - const children = nodesByParent.get(rootNode.id) || []; - const subGraph = children.map((child) => buildGraph(child)); - // let { width: minWidth, height: minHeight } = nodeDimensions.get( - // rootNode.id - // ) || { minWidth: 0, minHeight: 0 }; - // minWidth = minWidth || 0; - // minHeight = minHeight || 0; - const graph = { - id: rootNode.id, - children: subGraph, - targetPosition: "right", // TODO -- use the direction above - sourcePosition: "left", - layoutOptions: { - "elk.padding": "[top=40,left=25,bottom=25,right=25]", - // "elk.hierarchyHandling": "INCLUDE_CHILDREN", - // "elk.nodeSize.constraints": "MINIMUM_SIZE", - // "elk.nodeSize.minimum": `(${minHeight-2},${minWidth-2})`, - }, - // ? isHorizontal - // ? "right" - // : "bottom" - // : "right", - // width: 1000, - // height: 100, - width: rootNode - ? nodeDimensions.get(rootNode.id)?.width || 10 - : undefined, - height: rootNode - ? nodeDimensions.get(rootNode.id)?.height || 10 - : undefined, - }; - return graph; - }; + // Add nodes to dagre graph with their dimensions + nodes.forEach((node) => { + const dimensions = nodeDimensions.get(node.id) || { width: 150, height: 100 }; + dagreGraph.setNode(node.id, { + width: dimensions.width, + height: dimensions.height, + }); + + // Handle parent-child relationships for compound graphs + if (node.parentNode) { + dagreGraph.setParent(node.id, node.parentNode); + } + }); + + // Add edges to dagre graph + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); - const elkOptions = { - // "nodeLabels.padding": "[top=10,left=10,bottom=10,right=10]", - "elk.hierarchyHandling": "INCLUDE_CHILDREN", - // "elk.padding" : "[top=25,left=25,bottom=25,right=25]", - // "elk.spacing.individual": "true", - // "spacing.nodeNodeBetweenLayers": "150", - "elk.algorithm": "layered", - // "org.eclipse.elk.layered.layering.strategy": "STRETCH_WIDfasefasefsTH", - // "elk.spacing.nodeNode": "20", - // "elk.direction": direction == "LR" ? "RIGHT" : "DOWN", - // "elk.layered.spacing.nodeNodeBetweenLayers": "100", - // "elk.spacing.nodeNode": "80", - // // "elk.algorithm": "mrtree", - }; + // Calculate layout + dagre.layout(dagreGraph); - const nodeNameMap = new Map(nodes.map((node) => [node.id, node])); - const vizEdgeNameMap = new Map(edges.map((edge) => [edge.id, edge])); + // Apply layout to nodes + const layoutedNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + const dimensions = nodeDimensions.get(node.id) || { width: 150, height: 100 }; - const subGraph = (nodesByParent.get("root") || []).map((node) => - buildGraph(node) - ); - const graph = { - id: "root", - layoutOptions: elkOptions, - children: subGraph, - width: 1000, - height: 200, - edges: edges.map((edge) => ({ - ...edge, - sources: [edge.source], - targets: [edge.target], - })), - }; - return elk - .layout(graph, { - layoutOptions: { - "org.eclipse.elk.direction": vertical ? "DOWN" : "RIGHT", - }, - }) - .then((layoutedGraph) => ({ - nodes: convertGraphFromElk(layoutedGraph, nodeNameMap), - edges: (layoutedGraph.edges || []).map((edge) => { - const edgeId = edge.id; - const originalEdge = vizEdgeNameMap.get(edgeId) as VizEdge; - return originalEdge; - }) as VizEdge[], - })) - .catch(console.error); + // Update node position and dimensions + node.position = { + x: nodeWithPosition.x - dimensions.width / 2, // Center the node + y: nodeWithPosition.y - dimensions.height / 2, // Center the node + }; + node.data.dimensions = { + width: dimensions.width, + height: dimensions.height, + }; + + return node; + }); + + // Return layouted elements (no async needed for dagre) + return Promise.resolve({ + nodes: layoutedNodes, + edges: edges, + }); };
