I've been searching for a good topology layout javascript library for a long time. I learned many library D3, WebCola, and so on, and eventually I was deeply attracted to cytoscape and its amazing extension cytoscape.js-expand-collapse
What I want is a javascript layout library, which can do a reasonable layout with a lot of nodes. The parent nodes contains children nodes, in other words, there is an inheritance relationship between the nodes.
This Demo is almost extactly what I need. The expanding and collapsing feature is really great.
And I create my demo base on the above demo. But, when expand a node and then collapse the node, all nodes on the graph are changed.
Initial graph
The state after expand and collapse node at first time
The state after expand and collapse node at second time
Obviously that's not my need. And I learn the original offial demo again. I find the elements
values of demo has position
on each data.
{"data":{"id":"nwtN_50c55b8c-3489-4c4e-8bea-6a1c1162ac9c"},"position":{"x":577.5410894097904,"y":612.5647477282114},"group":"nodes"}
I know if each data has reasonable coordinate, all nodes position will not be changed after expand and collapse some nodes.
The key point is that I don't know the coordinate and I can't set the initial coordinate for my nodes. I think the core algorithm of layout is to calculate the appropriate coordinate points for each point.
So, I can't set the initial coordinate for all nodes and I expect all nodes position are fixed , no matter expand and collpase any node. Is it possible?
The following is my demo.
document.addEventListener('DOMContentLoaded', function() {
var cy = window.cy = cytoscape({
container: document.getElementById('cy'),
ready: function() {
var api = this.expandCollapse({
layoutBy: {
name: "cose-bilkent",
animate: "end",
randomize: false,
fit: false
},
fisheye: true,
animate: false,
undoable: false
});
api.collapseAll();
},
style: [{
selector: 'node',
style: {
'label': 'data(id)'
}
}],
elements: [{
"group": "nodes",
"data": {
"id": "n_0",
"name": "External Network"
}
}, {
"group": "nodes",
"data": {
"id": "n_4",
"name": "虚拟机网络",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "n_3",
"name": "VM Network 2",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_128",
"name": "bfcui-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_105",
"name": "bychen-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_93",
"name": "CE-bj",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_100",
"name": "changliu-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_67",
"name": "chaoma-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_83",
"name": "chenwang",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_68",
"name": "cwang-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_15",
"name": "gqpei-bj",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_1",
"name": "gwxu-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_118",
"name": "gyzhao-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_76",
"name": "hlli-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_18",
"name": "hwzhang-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_40",
"name": "hxqu-pc"
}
}, {
"group": "nodes",
"data": {
"id": "v_69",
"name": "hxwang-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_71",
"name": "jbshi-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_64",
"name": "jdai-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_16",
"name": "jfxiao-bj",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_78",
"name": "jhhou-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_91",
"name": "jjsun-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_17",
"name": "jppan-bj",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_45",
"name": "jqwang-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_50",
"name": "jxli-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_42",
"name": "jyyou-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_28",
"name": "jyzhou-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_46",
"name": "jzhao-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_19",
"name": "lfeng-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_65",
"name": "lhzhen-pc",
"parent": "group2"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_1",
"source": "n_0",
"target": "v_1"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_100",
"source": "n_0",
"target": "v_100"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_46",
"source": "n_0",
"target": "v_46"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_64",
"source": "n_0",
"target": "v_64"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_65",
"source": "n_0",
"target": "v_65"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_67",
"source": "n_0",
"target": "v_67"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_69",
"source": "n_0",
"target": "v_69"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_71",
"source": "n_0",
"target": "v_71"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_76",
"source": "n_0",
"target": "v_76"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_78",
"source": "n_0",
"target": "v_78"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_83",
"source": "n_0",
"target": "v_83"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_91",
"source": "n_0",
"target": "v_91"
}
}, {
"data": {
"group": "edges",
"id": "v_1n_0",
"source": "v_1",
"target": "n_0"
}
}, {
"data": {
"group": "edges",
"id": "v_1v_128",
"source": "v_1",
"target": "v_128"
}
}, {
"data": {
"group": "edges",
"id": "v_100n_0",
"source": "v_100",
"target": "n_0"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_16",
"source": "v_118",
"target": "v_16"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_18",
"source": "v_118",
"target": "v_18"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_46",
"source": "v_118",
"target": "v_46"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_67",
"source": "v_118",
"target": "v_67"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_69",
"source": "v_118",
"target": "v_69"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_71",
"source": "v_118",
"target": "v_71"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_78",
"source": "v_118",
"target": "v_78"
}
}, {
"data": {
"group": "edges",
"id": "v_128n_0",
"source": "v_128",
"target": "n_0"
}
}, {
"data": {
"group": "edges",
"id": "v_128v_1",
"source": "v_128",
"target": "v_1"
}
}, {
"data": {
"group": "edges",
"id": "v_128v_105",
"source": "v_128",
"target": "v_105"
}
}, {
"data": {
"group": "edges",
"id": "v_128v_46",
"source": "v_128",
"target": "v_46"
}
}, {
"data": {
"group": "edges",
"id": "v_128v_65",
"source": "v_128",
"target": "v_65"
}
}, {
"data": {
"group": "edges",
"id": "v_15n_0",
"source": "v_15",
"target": "n_0"
}
}, {
"data": {
"group": "edges",
"id": "v_50v_40",
"source": "v_50",
"target": "v_40"
}
}, {
"data": {
"group": "edges",
"id": "v_50v_46",
"source": "v_50",
"target": "v_46"
}
}, {
"data": {
"group": "edges",
"id": "v_50v_64",
"source": "v_50",
"target": "v_64"
}
}, {
"data": {
"group": "edges",
"id": "v_65v_19",
"source": "v_65",
"target": "v_19"
}
}, {
"data": {
"group": "edges",
"id": "v_65v_91",
"source": "v_65",
"target": "v_91"
}
}, {
"data": {
"group": "edges",
"id": "v_67n_0",
"source": "v_67",
"target": "n_0"
}
}, {
"data": {
"group": "edges",
"id": "v_67v_100",
"source": "v_67",
"target": "v_100"
}
}, {
"data": {
"group": "edges",
"id": "v_67v_105",
"source": "v_67",
"target": "v_105"
}
}, {
"data": {
"group": "edges",
"id": "v_67v_42",
"source": "v_67",
"target": "v_42"
}
}, {
"data": {
"group": "edges",
"id": "v_91v_16",
"source": "v_91",
"target": "v_16"
}
}, {
"data": {
"group": "edges",
"id": "v_91v_18",
"source": "v_91",
"target": "v_18"
}
}, {
"data": {
"group": "edges",
"id": "v_91v_28",
"source": "v_91",
"target": "v_28"
}
}, {
"data": {
"group": "edges",
"id": "v_91v_45",
"source": "v_91",
"target": "v_45"
}
}, {
"group": "nodes",
"data": {
"id": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "group2"
}
}]
});
var api = cy.expandCollapse('get');
var beforeExpand = null;
cy.unbind('expandcollapse.beforeexpand');
cy.nodes().bind('expandcollapse.beforeexpand', function(event) {
if (beforeExpand == null)
beforeExpand = cy.elements().clone(); // save the graph before the first expand
}); // Triggered before a node is expanded
cy.unbind('expandcollapse.aftercollapse');
cy.nodes().bind('expandcollapse.aftercollapse', function(event) {
if(beforeExpand != null) {
cy.elements().remove();
cy.add(beforeExpand); // set the graph to original values
beforeExpand = null;
}
});
});
body {
font-family: helvetica neue, helvetica, liberation sans, arial, sans-serif;
font-size: 14px;
}
#cy {
z-index: 999;
width: 100%;
height: 100%;
}
h1 {
opacity: 0.5;
font-size: 1em;
font-weight: bold;
}
<script src="https://code.jquery.com/jquery-2.0.3.min.js"></script>
<script src="https://unpkg.com/cytoscape@3.1.0/dist/cytoscape.min.js"></script>
<!-- for testing with local version of cytoscape.js -->
<!--<script src="../cytoscape.js/build/cytoscape.js"></script>-->
<script src="https://unpkg.com/cytoscape-cose-bilkent@4.0.0/cytoscape-cose-bilkent.js"></script>
<script src="https://unpkg.com/cytoscape-expand-collapse@3.1.1/cytoscape-expand-collapse.js"></script>
<div id="cy"></div>
Finally, sum up what I need :
Init graph with some nodes, which may be expandable depending on a property like type. Nodes with type=1 are expandable and type=2 not.
All nodes do a reasonable layout, like layoutBy:{name:'cose-bilkent'}
When expand one node (eg : A), Send ajax request to get children nodes (eg: A1, A2, A3) and then layout children. The graph maybe need an appropriate adjustments. I hope it's a incremental layout, not a full re-layout.
When collapse the previous compond nodes (group A with A1,A2,A3), all nodes on the graph keep the previous position.
When expand the last group node (eg : A), the children nodes are also keep the previous position.
I think my requirement is very basic, but I can't find a demo to display this feature? Do I describe my requirement clearly ?
Can someone help me? Thanks in advance. Thanks very much.
What you are trying to do is not really efficient. You said yourself, that you don't know the coordinates of your nodes, so cytoscape doesn'know that either. But non the less, cose-blikent still positions the elements as good as possible. The position may change, but the structure stays the same. There is really no problem there that justifies the trouble and work you'd have to go through to.
If you really want to achieve this, i suppose you can do this within a specific event that has occured:
var nodes = cy.nodes();
var positions = [];
for (node in nodes) {
positions[node] = nodes[node].position(); // save the i'th nodes positions
}
and then after you collapse a parent and expand it again, you can set all the nodes positions that you stored before:
var nodes = cy.nodes();
for (node in nodes) {
nodes[node].position(positions[node]); // set x and y of node
}
cy.nodes().on("expandcollapse.beforecollapse", function(event) { var node = this; ... }) // Triggered before a node is collapsed
cy.nodes().on("expandcollapse.aftercollapse", function(event) { var node = this; ... }) // Triggered after a node is collapsed
cy.nodes().on("expandcollapse.beforeexpand", function(event) { var node = this; ... }) // Triggered before a node is expanded
cy.nodes().on("expandcollapse.afterexpand", function(event) { var node = this; ... }) // Triggered after a node is expanded
var beforeExpand = null;
cy.unbind('expandcollapse.beforeexpand');
cy.nodes().bind('expandcollapse.beforeexpand', function(event) {
if (beforeExpand == null)
beforeExpand = cy.elements().clone(); // save the graph before the first expand
}); // Triggered before a node is expanded
cy.unbind('expandcollapse.aftercollapse');
cy.nodes().bind('expandcollapse.aftercollapse', function(event) {
if(beforeExpand != null) {
cy.elements().remove();
cy.add(beforeExpand); // set the graph to original values
beforeExpand = null;
}
}); // Triggered before a node is expanded
Here is how you repair your demo:
document.addEventListener('DOMContentLoaded', function() {
var cy = window.cy = cytoscape({
container: document.getElementById('cy'),
ready: function() {
var api = this.expandCollapse({
layoutBy: {
name: "cose-bilkent",
animate: "end",
randomize: false,
fit: true // set this to true
},
fisheye: true,
animate: false,
undoable: false
});
api.collapseAll();
},
style: [{
selector: 'node',
style: {
'label': 'data(id)'
}
}],
elements: [{
"group": "nodes",
"data": {
"id": "n_0",
"name": "External Network"
}
}, {
"group": "nodes",
"data": {
"id": "n_4",
"name": "虚拟机网络",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "n_3",
"name": "VM Network 2",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_128",
"name": "bfcui-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_105",
"name": "bychen-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_93",
"name": "CE-bj",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_100",
"name": "changliu-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_67",
"name": "chaoma-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_83",
"name": "chenwang",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_68",
"name": "cwang-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_15",
"name": "gqpei-bj",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_1",
"name": "gwxu-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_118",
"name": "gyzhao-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_76",
"name": "hlli-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_18",
"name": "hwzhang-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_40",
"name": "hxqu-pc"
}
}, {
"group": "nodes",
"data": {
"id": "v_69",
"name": "hxwang-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_71",
"name": "jbshi-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_64",
"name": "jdai-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_16",
"name": "jfxiao-bj",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_78",
"name": "jhhou-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_91",
"name": "jjsun-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_17",
"name": "jppan-bj",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_45",
"name": "jqwang-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_50",
"name": "jxli-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_42",
"name": "jyyou-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_28",
"name": "jyzhou-pc",
"parent": "group2"
}
}, {
"group": "nodes",
"data": {
"id": "v_46",
"name": "jzhao-pc",
"parent": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "v_19",
"name": "lfeng-pc",
"parent": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "v_65",
"name": "lhzhen-pc",
"parent": "group2"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_1",
"source": "n_0",
"target": "v_1"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_100",
"source": "n_0",
"target": "v_100"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_46",
"source": "n_0",
"target": "v_46"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_64",
"source": "n_0",
"target": "v_64"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_65",
"source": "n_0",
"target": "v_65"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_67",
"source": "n_0",
"target": "v_67"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_69",
"source": "n_0",
"target": "v_69"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_71",
"source": "n_0",
"target": "v_71"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_76",
"source": "n_0",
"target": "v_76"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_78",
"source": "n_0",
"target": "v_78"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_83",
"source": "n_0",
"target": "v_83"
}
}, {
"data": {
"group": "edges",
"id": "n_0v_91",
"source": "n_0",
"target": "v_91"
}
}, {
"data": {
"group": "edges",
"id": "v_1n_0",
"source": "v_1",
"target": "n_0"
}
}, {
"data": {
"group": "edges",
"id": "v_1v_128",
"source": "v_1",
"target": "v_128"
}
}, {
"data": {
"group": "edges",
"id": "v_100n_0",
"source": "v_100",
"target": "n_0"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_16",
"source": "v_118",
"target": "v_16"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_18",
"source": "v_118",
"target": "v_18"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_46",
"source": "v_118",
"target": "v_46"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_67",
"source": "v_118",
"target": "v_67"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_69",
"source": "v_118",
"target": "v_69"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_71",
"source": "v_118",
"target": "v_71"
}
}, {
"data": {
"group": "edges",
"id": "v_118v_78",
"source": "v_118",
"target": "v_78"
}
}, {
"data": {
"group": "edges",
"id": "v_128n_0",
"source": "v_128",
"target": "n_0"
}
}, {
"data": {
"group": "edges",
"id": "v_128v_1",
"source": "v_128",
"target": "v_1"
}
}, {
"data": {
"group": "edges",
"id": "v_128v_105",
"source": "v_128",
"target": "v_105"
}
}, {
"data": {
"group": "edges",
"id": "v_128v_46",
"source": "v_128",
"target": "v_46"
}
}, {
"data": {
"group": "edges",
"id": "v_128v_65",
"source": "v_128",
"target": "v_65"
}
}, {
"data": {
"group": "edges",
"id": "v_15n_0",
"source": "v_15",
"target": "n_0"
}
}, {
"data": {
"group": "edges",
"id": "v_50v_40",
"source": "v_50",
"target": "v_40"
}
}, {
"data": {
"group": "edges",
"id": "v_50v_46",
"source": "v_50",
"target": "v_46"
}
}, {
"data": {
"group": "edges",
"id": "v_50v_64",
"source": "v_50",
"target": "v_64"
}
}, {
"data": {
"group": "edges",
"id": "v_65v_19",
"source": "v_65",
"target": "v_19"
}
}, {
"data": {
"group": "edges",
"id": "v_65v_91",
"source": "v_65",
"target": "v_91"
}
}, {
"data": {
"group": "edges",
"id": "v_67n_0",
"source": "v_67",
"target": "n_0"
}
}, {
"data": {
"group": "edges",
"id": "v_67v_100",
"source": "v_67",
"target": "v_100"
}
}, {
"data": {
"group": "edges",
"id": "v_67v_105",
"source": "v_67",
"target": "v_105"
}
}, {
"data": {
"group": "edges",
"id": "v_67v_42",
"source": "v_67",
"target": "v_42"
}
}, {
"data": {
"group": "edges",
"id": "v_91v_16",
"source": "v_91",
"target": "v_16"
}
}, {
"data": {
"group": "edges",
"id": "v_91v_18",
"source": "v_91",
"target": "v_18"
}
}, {
"data": {
"group": "edges",
"id": "v_91v_28",
"source": "v_91",
"target": "v_28"
}
}, {
"data": {
"group": "edges",
"id": "v_91v_45",
"source": "v_91",
"target": "v_45"
}
}, {
"group": "nodes",
"data": {
"id": "group0"
}
}, {
"group": "nodes",
"data": {
"id": "group1"
}
}, {
"group": "nodes",
"data": {
"id": "group2"
}
}]
});
var api = cy.expandCollapse('get');
var beforeExpand = null;
cy.unbind('expandcollapse.beforeexpand');
cy.nodes().bind('expandcollapse.beforeexpand', function(event) {
if (beforeExpand == null)
beforeExpand = cy.elements().clone(); // save the graph before the first expand
}); // Triggered before a node is expanded
cy.unbind('expandcollapse.aftercollapse');
cy.nodes().bind('expandcollapse.aftercollapse', function(event) {
if(beforeExpand != null) {
cy.elements().remove();
cy.add(beforeExpand); // set the graph to original values
beforeExpand = null;
}
});
});
body {
font-family: helvetica;
font-size: 14px;
}
#cy { /*change your css*/
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 999;
}
h1 {
opacity: 0.5;
font-size: 1em;
}
<script src="https://code.jquery.com/jquery-2.0.3.min.js"></script>
<script src="https://unpkg.com/cytoscape@3.1.0/dist/cytoscape.min.js"></script>
<!-- for testing with local version of cytoscape.js -->
<!--<script src="../cytoscape.js/build/cytoscape.js"></script>-->
<script src="https://unpkg.com/cytoscape-cose-bilkent@4.0.0/cytoscape-cose-bilkent.js"></script>
<script src="https://unpkg.com/cytoscape-expand-collapse@3.1.1/cytoscape-expand-collapse.js"></script>
<div id="cy"></div>