Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ <h3 class="panel-title">Custom Query</h3>
<line x1="18" y1="10" x2="12" y2="6"/>
</svg>
</button>
<button class="toolbar-btn" data-layout="breadthfirst" title="Breadth-first (Directed Tree)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="6" cy="6" r="2"/>
<circle cx="18" cy="6" r="2"/>
<circle cx="6" cy="18" r="2"/>
<circle cx="18" cy="18" r="2"/>
<line x1="8" y1="6" x2="16" y2="6"/>
<line x1="6" y1="8" x2="6" y2="16"/>
<line x1="18" y1="8" x2="18" y2="16"/>
<line x1="8" y1="18" x2="16" y2="18"/>
</svg>
</button>
<button class="toolbar-btn" data-layout="circle" title="Circular">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
Expand All @@ -147,6 +159,38 @@ <h3 class="panel-title">Custom Query</h3>
<rect x="3" y="14" width="7" height="7"/>
</svg>
</button>
<button class="toolbar-btn" data-layout="cose" title="Force-directed (COSE)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="2"/>
<circle cx="4.5" cy="8" r="1.75"/>
<circle cx="19.5" cy="8" r="1.75"/>
<circle cx="6" cy="19" r="1.75"/>
<circle cx="18" cy="19" r="1.75"/>
<line x1="10.5" y1="11.2" x2="6.2" y2="8.8"/>
<line x1="13.5" y1="11.2" x2="17.8" y2="8.8"/>
<line x1="11.2" y1="13.5" x2="7.2" y2="17.6"/>
<line x1="12.8" y1="13.5" x2="16.8" y2="17.6"/>
</svg>
</button>
<button class="toolbar-btn" data-layout="fcose" title="Fast Force-directed (fCOSE)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="7" cy="7" r="2"/>
<circle cx="17" cy="7" r="2"/>
<circle cx="7" cy="17" r="2"/>
<circle cx="17" cy="17" r="2"/>
<line x1="9" y1="7" x2="15" y2="7"/>
<line x1="9" y1="17" x2="15" y2="17"/>
<line x1="7" y1="9" x2="7" y2="15"/>
<line x1="17" y1="9" x2="17" y2="15"/>
<path d="M12 4v4M12 16v4" />
</svg>
</button>
<button class="toolbar-btn" data-layout="cose-bilkent" title="Spring Layout (CoSE-Bilkent)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 4c0 2 4 2 4 4s-4 2-4 4 4 2 4 4-4 2-4 4" />
<path d="M13 4c0 2 4 2 4 4s-4 2-4 4 4 2 4 4-4 2-4 4" />
</svg>
</button>
<button class="toolbar-btn" data-layout="clustered" title="Clustered Circles">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="7" cy="7" r="5"/>
Expand Down
56 changes: 56 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
},
"dependencies": {
"cytoscape": "^3.30.4",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-dagre": "^2.5.0",
"cytoscape-fcose": "^2.2.0",
"sql.js": "^1.11.0"
},
"devDependencies": {
Expand Down
83 changes: 81 additions & 2 deletions src/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Dec 20, 2025

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.

Suggested change
// Register dagre layout
// Register layout extensions (dagre, fcose, coseBilkent)

Copilot uses AI. Check for mistakes.
cytoscape.use(dagre);
cytoscape.use(fcose);
cytoscape.use(coseBilkent);

// Dark Forest color palette for nodes
const NODE_COLORS = {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
};

/**
Expand All @@ -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;
Expand Down Expand Up @@ -440,14 +498,35 @@ export class GraphManager {
* @param {string} layoutName - Layout name (dagre, circle, grid, concentric, clustered)
Copy link

Copilot AI Dec 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc comment for runLayout is outdated. It lists the old layout options (dagre, circle, grid, concentric, clustered) but doesn't include the newly added layouts (breadthfirst, cose, fcose, cose-bilkent). The documentation should be updated to reflect all available layout options.

Suggested change
* @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 uses AI. Check for mistakes.
*/
runLayout(layoutName = this.currentLayout) {
// Stop any running layout before starting a new one
if (this.currentLayoutInstance && typeof this.currentLayoutInstance.stop === 'function') {
try {
this.currentLayoutInstance.stop();
} catch {
// ignore
Comment on lines +505 to +506
Copy link

Copilot AI Dec 20, 2025

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.

Suggested change
} catch {
// ignore
} catch (error) {
// Log the error for debugging but do not block layout transitions
console.error('Failed to stop current layout:', error);

Copilot uses AI. Check for mistakes.
}
}

if (layoutName === 'clustered') {
this.runClusteredLayout();
this.currentLayout = layoutName;
this.currentLayoutInstance = null;
return;
}
const layout = LAYOUTS[layoutName] || LAYOUTS.dagre;

const layout = { ...(LAYOUTS[layoutName] || LAYOUTS.dagre) };
Copy link

Copilot AI Dec 20, 2025

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.

Copilot uses AI. Check for mistakes.

// Breadthfirst looks best when we explicitly set roots (entry points)
if (layout.name === 'breadthfirst') {
const roots = this.cy.nodes().roots();
if (roots && roots.length > 0) {
layout.roots = roots;
}
}

this.currentLayout = layoutName;
this.cy.layout(layout).run();
this.currentLayoutInstance = this.cy.layout(layout);
this.currentLayoutInstance.run();
}

/**
Expand Down