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,
+  });
 };

Reply via email to