I am currently using cytoscapejs for my UI to render a graph. I have a big graph that I want to render on-demand or incrementally to avoid performance bottleneck issues. My server sends the graphs in chunks as and when requested.
What I want to do:
- Fetch the first chunk from the backend and render it with
dagre
layout or any layout that is easier for the user to understand. We can call this graph asG0
- Fetch the next chunk when the user requests neighbouring subgraph. (Assume that the size of the subgraph is fixed as of now) The fetched graph can be referred as
G1
- Render
G1
with respect toG0
such that the overall result is consistent and looks like as if bothG1
andG0
were rendered together.
What is happening:
When I render the G1
graph using the answer given on this page: Cytoscape: apply layout only to newly added nodes
the new nodes overlap the nodes of G0
. In other words, the layout
function is not taking the previously rendered graph G0
into consideration.
What have I tried:
I looked around and came across this library: https://github.com/iVis-at-Bilkent/cytoscape.js-layout-utilities?tab=readme-ov-file which has been developed for the exact same purpose (incrementing layout adjustments). But when I tried this library (by referencing demo.html, I did not get the expected results. The nodes of G1
still overlap G0
nodes. Here is the snippets of my code:
- The
api
initialisation:
api = cy.layoutUtilities({
desiredAspectRatio: 1.0,
polyominoGridSizeFactor: 1.0,
utilityFunction: 1,
componentSpacing: 80
})
- Rendering the initial chunks (3 chunks):
async function loadInitialChunks() {
for (let i = 0; i < 3; i++) {
// this function fetches an object containing two arrays
// {nodes, edges} and stores it in a variable `preloadedchunks`
await getChunk(chunkIndex++, chunkSize);
}
renderChunks();
}
function renderChunks() {
preloadedChunks.forEach(chunk => {
chunk.nodes.forEach(node => {
cy.add({
group: 'nodes',
data: node.data,
position: node.position
});
});
cy.add(chunk.edges);
});
// This code has been referred from the demo.html file I mentioned above
// More specifically, this code is present at line 299 in demo.html
var subgraphs = []
cy.$().forEach(chunk => {
var subgraph = {}
subgraph.nodes = []
subgraph.edges = []
chunk.nodes().forEach(node => {
var boundingBox = node.boundingBox();
subgraph.nodes.push({
x: boundingBox.x1,
y: boundingBox.y1,
width: boundingBox.w,
height: boundingBox.h
})
});
chunk.edges().forEach(edge => {
subgraph.edges.push({
startX: edge.sourceEndpoint().x,
startY: edge.sourceEndpoint().y,
endX: edge.targetEndpoint().x,
endY: edge.targetEndpoint().y
})
});
subgraphs.push(subgraph);
})
var result = api.packComponents(subgraphs, true);
cy.$().forEach((component, index) => {
component.nodes().layout({
name: 'fcose',
animate: 'true',
fit: false,
transform: (node) => {
let position = {};
position.x = node.position('x') + result.shifts[index].dx;
position.y = node.position('y') + result.shifts[index].dy;
return position;
}
}).run();
});
let currentZoom = cy.zoom();
let currentPan = cy.pan();
const layout = cy.layout({
name: 'fcose',
avoidOverlap: true,
nodeDimensionsIncludeLabels: true
});
layout.run();
}
- Requesting additional chunks:
function renderAdditionalChunks(chunk, chunkIndex) {
try {
// This approach is given at line #398 in the demo.html file
var newNodes = cy.collection();
chunk.nodes.forEach(node => {
if (!loadedNodes.has(node.data.id)) {
node.data.chunkId = chunkIndex
var newEles = cy.add({
group: 'nodes',
data: node.data,
});
newNodes.merge(newEles.nodes());
loadedNodes.add(node.data.id);
}
});
chunk.edges.forEach(edge => {
edge.data.chunkId = chunkIndex
cy.add({
group: 'edges',
data: edge.data,
});
});
let elementF = cy.elements("node[chunkId=" + chunkIndex + "],edge[chunkId=" + chunkIndex + "]");
console.log("Elements filtered out: ", elementF)
elementF.layout({
name: 'fcose',
// fit: false,
animate: true,
avoidOverlap: true,
nodeDimensionsIncludeLabels: true
}).run();
api.placeNewNodes(newNodes);
cy.zoom(currentZoom);
cy.pan(currentPan);
} catch (error) {
console.log(chunk)
console.error('Error:', error);
}
}
It would be really great if anyone can help me figure out what am I missing here or if there are any other better alternatives to do so. Thanks.