Skip to content

Commit 165edc0

Browse files
committed
refactor(pathways): simplify graph to clean top-down tree layout
- Simplified dagre layout to pure top-down tree (TB) - Increased spacing: 60px horizontal, 120px vertical - Removed complex connected component handling - Use fitView for better initial centering - Cleaner visual hierarchy: Resume → Jobs → Secondary Jobs
1 parent b12ff91 commit 165edc0

File tree

2 files changed

+32
-168
lines changed

2 files changed

+32
-168
lines changed

apps/registry/app/[username]/jobs-graph/utils/graphLayout.js

Lines changed: 26 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -2,105 +2,41 @@ import dagre from '@dagrejs/dagre';
22

33
// Node dimensions - must match actual rendered sizes
44
const NODE_WIDTH = 200;
5-
const NODE_HEIGHT = 100;
5+
const NODE_HEIGHT = 120;
66
const RESUME_WIDTH = 140;
7-
const RESUME_HEIGHT = 70;
7+
const RESUME_HEIGHT = 80;
8+
9+
// Spacing - generous to avoid overlap
10+
const HORIZONTAL_SPACING = 60; // Space between sibling nodes
11+
const VERTICAL_SPACING = 120; // Space between levels (ranks)
812

913
/**
10-
* Applies Dagre graph layout algorithm to React Flow nodes and edges
11-
* Handles disconnected components by laying them out in a grid
14+
* Applies a clean top-down tree layout
15+
* Resume at top, primary jobs below, secondary jobs below those
1216
* @param {Array} nodes - React Flow nodes
1317
* @param {Array} edges - React Flow edges
14-
* @param {string} direction - Layout direction ('TB', 'LR', etc.)
18+
* @param {string} direction - Layout direction ('TB' = top-bottom)
1519
* @returns {Object} Layouted nodes and edges
1620
*/
1721
export const getLayoutedElements = (nodes, edges, direction = 'TB') => {
1822
if (nodes.length === 0) return { nodes: [], edges };
1923

20-
// Find connected components
21-
const components = findConnectedComponents(nodes, edges);
22-
23-
// Layout each component separately
24-
const layoutedComponents = components.map((component) =>
25-
layoutComponent(component.nodes, component.edges, direction)
26-
);
27-
28-
// Arrange components in a compact grid layout
29-
const finalNodes = arrangeComponents(layoutedComponents);
30-
31-
return { nodes: finalNodes, edges };
32-
};
33-
34-
/**
35-
* Find connected components in the graph
36-
*/
37-
function findConnectedComponents(nodes, edges) {
38-
const visited = new Set();
39-
const components = [];
40-
41-
// Build adjacency list (undirected)
42-
const adj = new Map();
43-
nodes.forEach((n) => adj.set(n.id, new Set()));
44-
edges.forEach((e) => {
45-
if (adj.has(e.source) && adj.has(e.target)) {
46-
adj.get(e.source).add(e.target);
47-
adj.get(e.target).add(e.source);
48-
}
49-
});
50-
51-
// DFS to find components
52-
function dfs(nodeId, component) {
53-
if (visited.has(nodeId)) return;
54-
visited.add(nodeId);
55-
component.add(nodeId);
56-
for (const neighbor of adj.get(nodeId) || []) {
57-
dfs(neighbor, component);
58-
}
59-
}
60-
61-
for (const node of nodes) {
62-
if (!visited.has(node.id)) {
63-
const component = new Set();
64-
dfs(node.id, component);
65-
const componentNodes = nodes.filter((n) => component.has(n.id));
66-
const componentEdges = edges.filter(
67-
(e) => component.has(e.source) && component.has(e.target)
68-
);
69-
components.push({ nodes: componentNodes, edges: componentEdges });
70-
}
71-
}
72-
73-
// Sort components: main tree (with resume) first, then by size
74-
components.sort((a, b) => {
75-
const aHasResume = a.nodes.some((n) => n.data?.isResume);
76-
const bHasResume = b.nodes.some((n) => n.data?.isResume);
77-
if (aHasResume && !bHasResume) return -1;
78-
if (!aHasResume && bHasResume) return 1;
79-
return b.nodes.length - a.nodes.length;
80-
});
81-
82-
return components;
83-
}
84-
85-
/**
86-
* Layout a single connected component using dagre
87-
*/
88-
function layoutComponent(nodes, edges, direction) {
8924
const dagreGraph = new dagre.graphlib.Graph();
9025
dagreGraph.setDefaultEdgeLabel(() => ({}));
9126

27+
// Configure for a clean top-down tree
9228
dagreGraph.setGraph({
9329
rankdir: direction,
9430
align: 'UL',
95-
nodesep: 20, // Horizontal spacing between nodes
96-
ranksep: 60, // Vertical spacing between ranks
97-
edgesep: 10,
98-
marginx: 20,
99-
marginy: 20,
100-
acyclicer: 'greedy',
101-
ranker: 'tight-tree', // More compact than network-simplex
31+
nodesep: HORIZONTAL_SPACING,
32+
ranksep: VERTICAL_SPACING,
33+
edgesep: 20,
34+
marginx: 50,
35+
marginy: 50,
36+
ranker: 'tight-tree',
10237
});
10338

39+
// Add nodes to dagre
10440
nodes.forEach((node) => {
10541
const isResume = node.data?.isResume;
10642
dagreGraph.setNode(node.id, {
@@ -109,94 +45,29 @@ function layoutComponent(nodes, edges, direction) {
10945
});
11046
});
11147

48+
// Add edges to dagre
11249
edges.forEach((edge) => {
11350
dagreGraph.setEdge(edge.source, edge.target);
11451
});
11552

53+
// Run layout
11654
dagre.layout(dagreGraph);
11755

118-
// Calculate bounding box
119-
let minX = Infinity,
120-
minY = Infinity,
121-
maxX = -Infinity,
122-
maxY = -Infinity;
123-
56+
// Apply positions
12457
const layoutedNodes = nodes.map((node) => {
12558
const pos = dagreGraph.node(node.id);
12659
const isResume = node.data?.isResume;
12760
const width = isResume ? RESUME_WIDTH : NODE_WIDTH;
12861
const height = isResume ? RESUME_HEIGHT : NODE_HEIGHT;
12962

130-
// Dagre returns center position, convert to top-left
131-
const x = pos.x - width / 2;
132-
const y = pos.y - height / 2;
133-
134-
minX = Math.min(minX, x);
135-
minY = Math.min(minY, y);
136-
maxX = Math.max(maxX, x + width);
137-
maxY = Math.max(maxY, y + height);
138-
13963
return {
14064
...node,
141-
position: { x, y },
142-
};
143-
});
144-
145-
return {
146-
nodes: layoutedNodes,
147-
width: maxX - minX,
148-
height: maxY - minY,
149-
minX,
150-
minY,
151-
};
152-
}
153-
154-
/**
155-
* Arrange multiple components in a grid to avoid overlap
156-
*/
157-
function arrangeComponents(components) {
158-
if (components.length === 0) return [];
159-
if (components.length === 1) {
160-
// Single component - normalize to origin
161-
const comp = components[0];
162-
return comp.nodes.map((n) => ({
163-
...n,
16465
position: {
165-
x: n.position.x - comp.minX,
166-
y: n.position.y - comp.minY,
66+
x: pos.x - width / 2,
67+
y: pos.y - height / 2,
16768
},
168-
}));
169-
}
170-
171-
const allNodes = [];
172-
let currentX = 0;
173-
let currentY = 0;
174-
let rowHeight = 0;
175-
const maxRowWidth = 2000; // Max width before wrapping to next row
176-
const gap = 80; // Gap between components
177-
178-
for (const comp of components) {
179-
// Check if we need to wrap to next row
180-
if (currentX + comp.width > maxRowWidth && currentX > 0) {
181-
currentX = 0;
182-
currentY += rowHeight + gap;
183-
rowHeight = 0;
184-
}
185-
186-
// Add nodes with offset
187-
for (const node of comp.nodes) {
188-
allNodes.push({
189-
...node,
190-
position: {
191-
x: node.position.x - comp.minX + currentX,
192-
y: node.position.y - comp.minY + currentY,
193-
},
194-
});
195-
}
196-
197-
currentX += comp.width + gap;
198-
rowHeight = Math.max(rowHeight, comp.height);
199-
}
69+
};
70+
});
20071

201-
return allNodes;
202-
}
72+
return { nodes: layoutedNodes, edges };
73+
};

apps/registry/app/pathways/components/PathwaysGraph.js

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -116,26 +116,19 @@ export default function PathwaysGraph() {
116116
onEdgesChange={onEdgesChange}
117117
onNodeClick={handleNodeClick}
118118
onMoveEnd={handleMoveEnd}
119-
fitView={false}
119+
fitView
120+
fitViewOptions={{ padding: 0.2, maxZoom: 1.5 }}
120121
minZoom={0.05}
121122
maxZoom={4}
122-
defaultViewport={initialViewport || { x: 0, y: 0, zoom: 1.2 }}
123+
defaultViewport={initialViewport || { x: 0, y: 0, zoom: 1 }}
123124
onInit={(instance) => {
124125
setReactFlowInstance(instance);
125-
// If we have saved viewport, use it; otherwise center on resume
126+
// If we have saved viewport, use it; otherwise fit view
126127
if (initialViewport) {
127128
instance.setViewport(initialViewport, { duration: 0 });
128129
} else {
129-
setTimeout(() => {
130-
const resumeNode = nodes.find((node) => node.data?.isResume);
131-
if (resumeNode) {
132-
instance.setCenter(
133-
resumeNode.position.x,
134-
resumeNode.position.y,
135-
{ zoom: 1.2, duration: 800 }
136-
);
137-
}
138-
}, 100);
130+
// Fit view to show all nodes
131+
setTimeout(() => instance.fitView({ padding: 0.2 }), 100);
139132
}
140133
}}
141134
proOptions={{ hideAttribution: true }}

0 commit comments

Comments
 (0)