-
Notifications
You must be signed in to change notification settings - Fork 0
Graph node placement algorithms #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,9 +4,13 @@ | |||||||||||
|
|
||||||||||||
| import cytoscape from 'cytoscape'; | ||||||||||||
| import dagre from 'cytoscape-dagre'; | ||||||||||||
| import fcose from 'cytoscape-fcose'; | ||||||||||||
| import coseBilkent from 'cytoscape-cose-bilkent'; | ||||||||||||
|
|
||||||||||||
| // Register dagre layout | ||||||||||||
| cytoscape.use(dagre); | ||||||||||||
| cytoscape.use(fcose); | ||||||||||||
| cytoscape.use(coseBilkent); | ||||||||||||
|
|
||||||||||||
| // Dark Forest color palette for nodes | ||||||||||||
| const NODE_COLORS = { | ||||||||||||
|
|
@@ -211,6 +215,15 @@ const LAYOUTS = { | |||||||||||
| fit: true, | ||||||||||||
| padding: 30, | ||||||||||||
| }, | ||||||||||||
| breadthfirst: { | ||||||||||||
| name: 'breadthfirst', | ||||||||||||
| directed: true, | ||||||||||||
| spacingFactor: 1.25, | ||||||||||||
| animate: true, | ||||||||||||
| animationDuration: 350, | ||||||||||||
| fit: true, | ||||||||||||
| padding: 30, | ||||||||||||
| }, | ||||||||||||
| circle: { | ||||||||||||
| name: 'circle', | ||||||||||||
| animate: true, | ||||||||||||
|
|
@@ -238,6 +251,50 @@ const LAYOUTS = { | |||||||||||
| concentric: (node) => node.data('caller_count') || 0, | ||||||||||||
| levelWidth: () => 3, | ||||||||||||
| }, | ||||||||||||
| cose: { | ||||||||||||
| // Built-in force-directed layout (good general-purpose) | ||||||||||||
| name: 'cose', | ||||||||||||
| animate: true, | ||||||||||||
| animationDuration: 500, | ||||||||||||
| fit: true, | ||||||||||||
| padding: 30, | ||||||||||||
| randomize: true, | ||||||||||||
| // Tuning for directed call graphs (reduce hairballs a bit) | ||||||||||||
| nodeOverlap: 4, | ||||||||||||
| idealEdgeLength: 90, | ||||||||||||
| nodeRepulsion: 4500, | ||||||||||||
| gravity: 0.25, | ||||||||||||
| numIter: 800, | ||||||||||||
| }, | ||||||||||||
| fcose: { | ||||||||||||
| // Faster COSE variant for larger graphs | ||||||||||||
| name: 'fcose', | ||||||||||||
| animate: true, | ||||||||||||
| animationDuration: 600, | ||||||||||||
| fit: true, | ||||||||||||
| padding: 30, | ||||||||||||
| randomize: true, | ||||||||||||
| quality: 'default', | ||||||||||||
| nodeSeparation: 80, | ||||||||||||
| idealEdgeLength: 90, | ||||||||||||
| nodeRepulsion: 6500, | ||||||||||||
| edgeElasticity: 0.45, | ||||||||||||
| gravity: 0.25, | ||||||||||||
| numIter: 900, | ||||||||||||
| }, | ||||||||||||
| 'cose-bilkent': { | ||||||||||||
| // Popular spring-embedder implementation (often nicer spacing than COSE) | ||||||||||||
| name: 'cose-bilkent', | ||||||||||||
| animate: true, | ||||||||||||
| animationDuration: 600, | ||||||||||||
| fit: true, | ||||||||||||
| padding: 30, | ||||||||||||
| randomize: true, | ||||||||||||
| idealEdgeLength: 100, | ||||||||||||
| nodeRepulsion: 9000, | ||||||||||||
| gravity: 0.25, | ||||||||||||
| numIter: 1200, | ||||||||||||
| }, | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
|
|
@@ -248,6 +305,7 @@ export class GraphManager { | |||||||||||
| this.containerId = containerId; | ||||||||||||
| this.cy = null; | ||||||||||||
| this.currentLayout = 'dagre'; | ||||||||||||
| this.currentLayoutInstance = null; | ||||||||||||
| this.nodes = new Map(); // Track nodes by hash | ||||||||||||
| this.onNodeSelect = null; | ||||||||||||
| this.onNodeHover = null; | ||||||||||||
|
|
@@ -440,14 +498,35 @@ export class GraphManager { | |||||||||||
| * @param {string} layoutName - Layout name (dagre, circle, grid, concentric, clustered) | ||||||||||||
|
||||||||||||
| * @param {string} layoutName - Layout name (dagre, circle, grid, concentric, clustered) | |
| * @param {string} layoutName - Layout name (dagre, circle, grid, concentric, clustered, breadthfirst, cose, fcose, cose-bilkent) |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The empty catch block silently ignores all errors when stopping the layout. While this might be intentional to prevent errors from blocking layout transitions, consider logging the error to the console for debugging purposes. This would help identify if there are issues with the layout stop operation without breaking the user experience.
| } catch { | |
| // ignore | |
| } catch (error) { | |
| // Log the error for debugging but do not block layout transitions | |
| console.error('Failed to stop current layout:', error); |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a shallow copy of the layout configuration object is good practice, but the copy doesn't handle nested objects deeply. While this works for the current implementation since most properties are primitives, if future layouts include nested configuration objects (like the concentric function which is a reference type), this could lead to unintended mutations of the original LAYOUTS object. Consider documenting this limitation or using a deep clone if nested objects become common.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on line 10 says "Register dagre layout" but the following lines also register fcose and coseBilkent layouts. The comment should be updated to reflect that multiple layout extensions are being registered, for example: "Register layout extensions" or list all of them.