cytoscape / cytoscape.js

Graph theory (network) library for visualisation and analysis
https://js.cytoscape.org
MIT License
10.09k stars 1.64k forks source link

headless option doesn't provide same boundingBox values #2594

Closed Duskfall closed 4 years ago

Duskfall commented 4 years ago

Bug report

Environment info

Current (buggy) behaviour When logging the bounding box of a headless instance and non headless instance, they are not the same with same defined settings

Desired behaviour

cytoscape should provide the exact numbers when rendering the 2 instances

The use case for this is to have another instance of cytoscape to render the next position of the elements when calling a layout in a subset of elements so that I can animate between the current position and the final position of those nodes.

Minimum steps to reproduce

  1. Define nodes and edges
  2. Create a cytoscape instance targeting an element (cy)
  3. Create a cytoscape instance using same settings (headlessCy) except adding headless: true, styleEnabled:true and boundingBox: cy.elements.boundingBox()

https://jsbin.com/debihuletu/1/edit?js,console,output

Using dagre layout and the nodes in Wine and cheese I get the following: image

Notes I use timeout in the jsbin demo in case the bounding box isn't rendered properly so I wait it to be completed, while in my other example of the screenshot I just call a second instance right after calling the first one. The results are shown. Just for reference here is my code

var cy = (window.cy = cytoscape({
    container: document.getElementById("app"),
    layout,
    styleEnabled: true,
    style: style,
    //nodeDimensionsIncludeLabels: true,
    fit: true,
    elements: elems
}));

var cy2 = (window.cy2 = cytoscape({
    container: null,
    headless: true,
    layout: {
        ...layout,
        boundingBox: cy.elements().boundingBox()
    },
    styleEnabled: true,
    style: style,
    //nodeDimensionsIncludeLabels: true,
    fit: true,
    elements: elems,
    zoom: cy.zoom(),
    pan: cy.pan()
}));

console.log(cy.elements().boundingBox())
console.log(cy2.elements().boundingBox())

Also after performing the layout when clicking on a node, the pan and the zoom are different between those 2: image

cy.on("tap", "node", function(e) {
    let sel = e.target;

    var irrelevantElements = cy
        .elements()
        .difference(sel.outgoers().union(sel.incomers()))
        .not(sel);

    // headless calculations here
    var sel2 = cy2.elements().filter((element) => element.data('id') === sel.data('id'))
    var irrelevantElements2 = cy2.elements()
        .difference(sel2.outgoers().union(sel2.incomers()))
        .not(sel2);
    var directlyConnected2 = sel2.closedNeighborhood();
    sel2.style({
        width: 150,
        height: 150
    });
    directlyConnected2.not(sel2).style({
        opacity: 1
    }, );
    directlyConnected2
        .not(sel2)
        .nodes()
        .style({
            width: 50,
            height: 50
        });
    directlyConnected2.layout({
        ...layout
    }).run();
    irrelevantElements2.style({
        opacity: 0
    }, );
    //end of headless calculations
    var directlyConnected = sel.closedNeighborhood();

    sel.animate({
        style: {
            width: 150,
            height: 150
        }
    }, {
        duration: 500,
        queue: false
    });
    directlyConnected.not(sel).animate({
        style: {
            opacity: 1
        }
    }, {
        duration: 500,
        queue: false
    });
    directlyConnected
        .not(sel)
        .nodes()
        .animate({
            style: {
                width: 50,
                height: 50
            }
        }, {
            duration: 500,
            queue: false
        });
    directlyConnected.layout({
        ...layout,
        animate: true
    }).run();
    irrelevantElements.animate({
        style: {
            opacity: 0
        }
    }, {
        duration: 500
    });
});
maxkfranz commented 4 years ago

I understand the motivation for having high-fidelity rendered dimensions in a headless instance. However, this is not possible by definition. Therefore, this is not a bug.

A headless instance is an instance that doesn't have a renderer in the first place. Only the renderer can calculate rendered values. A headless instance, which has no renderer, can not get any rendered values.

Running Cytoscape on Node, Rhino, etc. is the main usecase for headless mode. Or you may run a headless instance in the browser if you just want to run some algorithms. Style is disabled by default on a headless instance for this reason. Enabling style will only give you styled dimensions. Although styled dimensions may be somewhat higher fidelity than non-styled dimensions, they are not as high fidelity as rendered dimensions. For example, only rendered dimensions may take into account things like text dimensions.

To get rendered values, you must use a rendered instance of Cytoscape. There is a distinction between a headless browser and a headless Cytoscape instance. For example, you can use Cytosnap to get snapshots with a rendered Cytoscape instance on a headless instance of Chromium. You could use Puppeteer in a similar way to get rendered values.

I've outlined what a headless mode means and how it should be used, but I don't understand how headless mode would help with the usecase you outlined:

The use case for this is to have another instance of cytoscape to render the next position of the elements when calling a layout in a subset of elements so that I can animate between the current position and the final position of those nodes.

I think there should be much simpler ways to do this. Take another pass through the documentation, and it should be clear how to do this. If you want help with writing your code, there may be people in the community on StackOverflow who can help out.