You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hamilton.apache.org by sk...@apache.org on 2025/10/05 17:19:13 UTC
(hamilton) 03/04: Removes elkjs and adds dagre
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 <st...@cs.stanford.edu>
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,
+ });
};