OR-1 dataflow CPU sketch

feat: switch to ELK layout engine with visual polish

- Replace dagre with cytoscape-elk for proper layered graph layout
- Add post-layout edge routing that curves skip connections around
intermediate nodes with staggered offsets to prevent crossovers
- Fixed-size 40px circular nodes with thin coloured borders
- Thin 1px black edges with small arrows
- Add Playwright browser env vars to flake.nix for NixOS

Orual 232434e4 cb0eb08f

+167 -48
+19
dfgraph/frontend/package-lock.json
··· 10 10 "dependencies": { 11 11 "cytoscape": "^3.30.0", 12 12 "cytoscape-dagre": "^2.5.0", 13 + "cytoscape-elk": "^2.3.0", 13 14 "cytoscape-svg": "^0.4.0" 14 15 }, 15 16 "devDependencies": { ··· 471 472 "cytoscape": "^3.2.22" 472 473 } 473 474 }, 475 + "node_modules/cytoscape-elk": { 476 + "version": "2.3.0", 477 + "resolved": "https://registry.npmjs.org/cytoscape-elk/-/cytoscape-elk-2.3.0.tgz", 478 + "integrity": "sha512-1h2ZmPOy5HD2+mrfF3P2ICxfnDyPCWg/xLVs7fIjTOzdQu51ydrMtm6Sb7KnhFwLBzhGIVYI2Gbns0njggBarQ==", 479 + "license": "MIT", 480 + "dependencies": { 481 + "elkjs": "^0.9.3" 482 + }, 483 + "peerDependencies": { 484 + "cytoscape": "^3.2.0" 485 + } 486 + }, 474 487 "node_modules/cytoscape-svg": { 475 488 "version": "0.4.0", 476 489 "resolved": "https://registry.npmjs.org/cytoscape-svg/-/cytoscape-svg-0.4.0.tgz", ··· 489 502 "graphlib": "^2.1.8", 490 503 "lodash": "^4.17.15" 491 504 } 505 + }, 506 + "node_modules/elkjs": { 507 + "version": "0.9.3", 508 + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", 509 + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", 510 + "license": "EPL-2.0" 492 511 }, 493 512 "node_modules/esbuild": { 494 513 "version": "0.24.2",
+1
dfgraph/frontend/package.json
··· 10 10 "dependencies": { 11 11 "cytoscape": "^3.30.0", 12 12 "cytoscape-dagre": "^2.5.0", 13 + "cytoscape-elk": "^2.3.0", 13 14 "cytoscape-svg": "^0.4.0" 14 15 }, 15 16 "devDependencies": {
+4
dfgraph/frontend/src/cytoscape-elk.d.ts
··· 1 + declare module "cytoscape-elk" { 2 + const ext: cytoscape.Ext; 3 + export default ext; 4 + }
+28 -10
dfgraph/frontend/src/layout.ts
··· 1 1 export function logicalLayout(): object { 2 2 return { 3 - name: "dagre", 4 - rankDir: "TB", 5 - nodeSep: 60, 6 - rankSep: 80, 7 - edgeSep: 20, 3 + name: "elk", 4 + elk: { 5 + algorithm: "layered", 6 + "elk.direction": "DOWN", 7 + "elk.spacing.nodeNode": 40, 8 + "elk.layered.spacing.nodeNodeBetweenLayers": 45, 9 + "elk.spacing.edgeNode": 25, 10 + "elk.spacing.edgeNodeBetweenLayers": 20, 11 + "elk.spacing.edgeEdge": 12, 12 + "elk.spacing.edgeEdgeBetweenLayers": 12, 13 + "elk.edgeRouting": "ORTHOGONAL", 14 + "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP", 15 + "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", 16 + }, 8 17 animate: false, 9 18 }; 10 19 } 11 20 12 21 export function physicalLayout(): object { 13 22 return { 14 - name: "dagre", 15 - rankDir: "TB", 16 - nodeSep: 40, 17 - rankSep: 60, 18 - edgeSep: 15, 23 + name: "elk", 24 + elk: { 25 + algorithm: "layered", 26 + "elk.direction": "DOWN", 27 + "elk.spacing.nodeNode": 35, 28 + "elk.layered.spacing.nodeNodeBetweenLayers": 40, 29 + "elk.spacing.edgeNode": 20, 30 + "elk.spacing.edgeNodeBetweenLayers": 15, 31 + "elk.spacing.edgeEdge": 10, 32 + "elk.spacing.edgeEdgeBetweenLayers": 10, 33 + "elk.edgeRouting": "ORTHOGONAL", 34 + "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP", 35 + "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", 36 + }, 19 37 animate: false, 20 38 }; 21 39 }
+70 -6
dfgraph/frontend/src/main.ts
··· 1 1 import cytoscape from "cytoscape"; 2 - import dagre from "cytoscape-dagre"; 2 + import elk from "cytoscape-elk"; 3 3 import svg from "cytoscape-svg"; 4 4 import type { GraphUpdate, GraphNode, GraphEdge, GraphRegion } from "./types"; 5 5 import { stylesheet } from "./style"; 6 6 import { logicalLayout, physicalLayout } from "./layout"; 7 7 import { exportSvg, exportPng, copyPng } from "./export"; 8 8 9 - cytoscape.use(dagre); 9 + cytoscape.use(elk); 10 10 cytoscape.use(svg); 11 11 12 12 const ROUTING_CATEGORY = "routing"; ··· 15 15 container: document.getElementById("graph"), 16 16 style: stylesheet, 17 17 elements: [], 18 + maxZoom: 2.0, 19 + minZoom: 0.1, 18 20 }); 19 21 20 22 function buildLabel(node: GraphNode): string { ··· 217 219 let currentView: ViewMode = "logical"; 218 220 let latestUpdate: GraphUpdate | null = null; 219 221 222 + function routeEdges(): void { 223 + const nodes = cy.nodes().not(":parent"); 224 + const nodePositions: Array<{ x: number; y: number }> = []; 225 + nodes.forEach((n) => { 226 + nodePositions.push({ x: n.position("x"), y: n.position("y") }); 227 + }); 228 + 229 + let leftCount = 0; 230 + let rightCount = 0; 231 + 232 + cy.edges().forEach((edge) => { 233 + const sy = edge.source().position("y"); 234 + const ty = edge.target().position("y"); 235 + const sx = edge.source().position("x"); 236 + const tx = edge.target().position("x"); 237 + const span = Math.abs(ty - sy); 238 + 239 + if (span < 80) { 240 + edge.style({ "curve-style": "bezier" }); 241 + return; 242 + } 243 + 244 + const blocked = nodePositions.some((p) => { 245 + return ( 246 + p.y > Math.min(sy, ty) + 25 && 247 + p.y < Math.max(sy, ty) - 25 248 + ); 249 + }); 250 + 251 + if (!blocked) { 252 + edge.style({ "curve-style": "bezier" }); 253 + return; 254 + } 255 + 256 + const avgX = (sx + tx) / 2; 257 + const centerX = (Math.min(...nodePositions.map((p) => p.x)) + 258 + Math.max(...nodePositions.map((p) => p.x))) / 2; 259 + const goLeft = avgX >= centerX; 260 + 261 + const baseOffset = 30; 262 + const stagger = goLeft ? 12 * leftCount : 12 * rightCount; 263 + const offset = (goLeft ? -1 : 1) * (baseOffset + stagger); 264 + 265 + if (goLeft) leftCount++; 266 + else rightCount++; 267 + 268 + edge.style({ 269 + "curve-style": "unbundled-bezier", 270 + "control-point-distances": [offset * 0.6, offset, offset * 0.6], 271 + "control-point-weights": [0.2, 0.5, 0.8], 272 + }); 273 + }); 274 + } 275 + 220 276 function renderLogical(update: GraphUpdate): void { 221 277 cy.batch(() => { 222 278 cy.elements().remove(); 223 279 cy.add(buildElements(update)); 224 280 }); 225 - cy.layout(logicalLayout()).run(); 226 - cy.fit(undefined, 40); 281 + const layout = cy.layout(logicalLayout()); 282 + layout.on("layoutstop", () => { 283 + routeEdges(); 284 + cy.fit(undefined, 40); 285 + }); 286 + layout.run(); 227 287 } 228 288 229 289 function renderPhysical(update: GraphUpdate): void { ··· 231 291 cy.elements().remove(); 232 292 cy.add(buildPhysicalElements(update)); 233 293 }); 234 - cy.layout(physicalLayout()).run(); 235 - cy.fit(undefined, 40); 294 + const layout = cy.layout(physicalLayout()); 295 + layout.on("layoutstop", () => { 296 + routeEdges(); 297 + cy.fit(undefined, 40); 298 + }); 299 + layout.run(); 236 300 } 237 301 238 302 function renderUpdate(update: GraphUpdate): void {
+39 -32
dfgraph/frontend/src/style.ts
··· 5 5 selector: "node", 6 6 style: { 7 7 shape: "ellipse", 8 - width: "label", 9 - height: "label", 10 - padding: "12px", 8 + width: 40, 9 + height: 40, 11 10 "text-valign": "center", 12 11 "text-halign": "center", 13 12 label: "data(label)", 14 - "font-size": 11, 13 + "text-wrap": "wrap", 14 + "text-max-width": "36px", 15 + "font-size": 8, 15 16 "font-family": "monospace", 16 - color: "#fff", 17 + color: "#111", 17 18 "text-outline-width": 0, 18 - "background-color": "data(colour)", 19 - "border-width": 2, 19 + "background-color": "#fff", 20 + "background-opacity": 1, 21 + "border-width": 1.5, 20 22 "border-color": "data(colour)", 21 23 }, 22 24 }, ··· 24 26 selector: "$node > node", 25 27 style: { 26 28 shape: "roundrectangle", 29 + width: "label", 30 + height: "label", 27 31 "border-style": "dashed", 28 - "border-width": 2, 32 + "border-width": 1, 29 33 "border-color": "#888", 30 - "background-color": "rgba(200, 200, 200, 0.08)", 31 - padding: "24px", 34 + "background-color": "rgba(200, 200, 200, 0.06)", 35 + padding: "20px", 32 36 "text-valign": "top", 33 37 "text-halign": "center", 34 38 label: "data(label)", 35 - "font-size": 12, 39 + "font-size": 9, 36 40 color: "#666", 37 41 }, 38 42 }, 39 43 { 40 44 selector: "edge", 41 45 style: { 42 - "curve-style": "bezier", 46 + "curve-style": "unbundled-bezier", 43 47 "target-arrow-shape": "triangle", 44 - "target-arrow-color": "#999", 45 - "line-color": "#999", 46 - width: 2, 48 + "arrow-scale": 0.6, 49 + "target-arrow-color": "#333", 50 + "line-color": "#333", 51 + width: 1, 47 52 "target-label": "data(targetLabel)", 48 - "target-text-offset": 18, 49 - "target-text-margin-y": -10, 53 + "target-text-offset": 14, 54 + "target-text-margin-y": -6, 50 55 "source-label": "data(sourceLabel)", 51 - "source-text-offset": 18, 52 - "source-text-margin-y": -10, 53 - "font-size": 9, 56 + "source-text-offset": 14, 57 + "source-text-margin-y": -6, 58 + "font-size": 7, 54 59 "font-family": "monospace", 55 - color: "#666", 60 + color: "#333", 56 61 "text-background-color": "#fff", 57 - "text-background-opacity": 0.8, 58 - "text-background-padding": "2px", 62 + "text-background-opacity": 0.9, 63 + "text-background-padding": "1px", 59 64 }, 60 65 }, 61 66 { 62 67 selector: "node.error", 63 68 style: { 64 69 "border-style": "dashed", 65 - "border-width": 3, 70 + "border-width": 1.5, 66 71 "border-color": "#e53935", 67 72 }, 68 73 }, ··· 78 83 selector: "node.pe-cluster", 79 84 style: { 80 85 shape: "roundrectangle", 81 - "border-width": 2, 86 + width: "label", 87 + height: "label", 88 + "border-width": 1.5, 82 89 "border-color": "#5c6bc0", 83 - "background-color": "rgba(92, 107, 192, 0.06)", 84 - padding: "20px", 90 + "background-color": "rgba(92, 107, 192, 0.04)", 91 + padding: "16px", 85 92 "text-valign": "top", 86 93 "text-halign": "center", 87 94 label: "data(label)", 88 - "font-size": 13, 95 + "font-size": 9, 89 96 "font-weight": "bold", 90 97 color: "#5c6bc0", 91 98 }, ··· 93 100 { 94 101 selector: "edge.cross-pe", 95 102 style: { 96 - width: 3, 103 + width: 1.5, 97 104 "line-color": "#5c6bc0", 98 105 "target-arrow-color": "#5c6bc0", 99 106 }, ··· 101 108 { 102 109 selector: "edge.intra-pe", 103 110 style: { 104 - width: 1.5, 105 - "line-color": "#bbb", 106 - "target-arrow-color": "#bbb", 111 + width: 0.75, 112 + "line-color": "#999", 113 + "target-arrow-color": "#999", 107 114 }, 108 115 }, 109 116 ];
+6
flake.nix
··· 64 64 65 65 nodejs 66 66 nodePackages.npm 67 + playwright-driver.browsers 67 68 ]; 68 69 69 70 # Point nix-ld at the libs these wheels need ··· 78 79 pkgs.proj 79 80 pkgs.hdf5 80 81 ]; 82 + 83 + PLAYWRIGHT_BROWSERS_PATH = "${pkgs.playwright-driver.browsers}"; 84 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"; 85 + PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = true; 86 + PLAYWRIGHT_HOST_PLATFORM_OVERRIDE = "ubuntu-24.04"; 81 87 82 88 shellHook = '' 83 89 # Create/reuse a local venv so uv has somewhere to install to.