Is there a edge weighted spring embedded layout in cytoscape.js? - cytoscape.js

We have a requirement to show nodes and edges graph in our Angular application and we chose to do it in cytoscape over high charts/amCharts/D3. Cytoscape is faster in loading and is the only library to handle 1000s of nodes that we have. Others break or lags.
We downloaded cytoscape app and were able to see a layout 'Edge weighted spring embedded'. It shows our interconnected nodes in a overall globe-like layout.
Now I want the same in cytoscape js and looks like we only have handful of layouts in https://js.cytoscape.org/
Does this layout possible in cytoscape js? How to achieve this? (Currently using fcose layout but we really like the globe look that 'Edge weighted spring embedded layout brings.
EDIT:
Here is another example: I want to achieve this layout in cytoscape.js.
What layout I should follow? Or is this something not available in js and only available in desktop App

There are three options I think:
The Compound Spring Embedder layout cose
The Compound Spring Embedder layout by Bilkent (for an enhanced compound node placement) cose-bilkent
The Constraint-Based Layout cola.js
They all use some sort of force directed layout, I think you should give them a try and use the one most fitting to your needs. There are many parameters you can set for each layout, so take a closer look at them too. For example, if you set infinite to true in the cola.js layout, you will see that you can move the nodes around and see the layout force them back to or to a fitting position:
document.addEventListener("DOMContentLoaded", function() {
var cy = (window.cy = cytoscape({
container: document.getElementById("cy"),
autounselectify: true,
boxSelectionEnabled: false,
layout: {
name: "cola",
infinite: true,
fit: false
},
style: [{
selector: "node",
css: {
"background-color": "#f92411"
}
},
{
selector: "edge",
css: {
"line-color": "#f92411"
}
}
],
elements: {
nodes: [{
data: {
id: "1",
label: "P"
}
},
{
data: {
id: "2",
label: "sucrose phosphate phosphatase"
}
},
{
data: {
id: "4",
label: "sucrose 6-phosphate"
}
},
{
data: {
id: "6",
label: "sucrose"
}
},
{
data: {
id: "8",
label: "invertase"
}
},
{
data: {
id: "10",
label: "fructose"
}
},
{
data: {
id: "12",
label: "fructokinase"
}
},
{
data: {
id: "14",
label: "fructose 6-phosphate"
}
},
{
data: {
id: "20",
label: "phosphoglucose isomerase"
}
},
{
data: {
id: "22",
label: "glucose 6-phosphate"
}
},
{
data: {
id: "28",
label: "glucose"
}
},
{
data: {
id: "30",
label: "hexokinase"
}
},
{
data: {
id: "33",
label: "sucrose synthase"
}
},
{
data: {
id: "36",
label: "UDP - glucose"
}
},
{
data: {
id: "38",
label: "sucrose phosphate synthase"
}
},
{
data: {
id: "41",
label: "UDP"
}
},
{
data: {
id: "44",
label: "fructose 6-phosphate"
}
},
{
data: {
id: "46",
label: "ATP"
}
},
{
data: {
id: "47",
label: "ATP"
}
},
{
data: {
id: "52",
label: "ATP"
}
},
{
data: {
id: "57",
label: "ADP"
}
},
{
data: {
id: "66",
label: "PP"
}
},
{
data: {
id: "71",
label: "UTP"
}
},
{
data: {
id: "76",
label: "UDP glucose pyrophosphorylase"
}
},
{
data: {
id: "80",
label: "glucose 1-phosphate"
}
},
{
data: {
id: "86",
label: "phospho- glucomutase (cPGM)"
}
},
{
data: {
id: "89",
label: "G1P transporter"
}
},
{
data: {
id: "90",
label: "P"
}
},
{
data: {
id: "95",
label: "P"
}
},
{
data: {
id: "102",
label: "P"
}
},
{
data: {
id: "103",
label: "P"
}
},
{
data: {
id: "104",
label: "G6P transporter"
}
},
{
data: {
id: "109",
label: "glucose 6-phosphate"
}
},
{
data: {
id: "115",
label: "phospho- glucomutase (cPGM)"
}
},
{
data: {
id: "121",
label: "glucose 1-phosphate"
}
},
{
data: {
id: "128",
label: "ADPglucose pyrophosphorylase (pAGPase)"
}
},
{
data: {
id: "130",
label: "ADP - glucose"
}
},
{
data: {
id: "136",
label: "PP"
}
},
{
data: {
id: "141",
label: "ATP"
}
},
{
data: {
id: "148",
label: "inorganic diphosphatase"
}
},
{
data: {
id: "149",
label: "P"
}
},
{
data: {
id: "156",
label: "phosphate transporter"
}
},
{
data: {
id: "158",
label: "P"
}
},
{
data: {
id: "164",
label: "starch synthase (simpl.)"
}
},
{
data: {
id: "166",
label: "ADP"
}
},
{
data: {
id: "172",
label: "starch"
}
},
{
data: {
id: "178",
label: "ATP/ADP transporter"
}
},
{
data: {
id: "179",
label: "ADP"
}
},
{
data: {
id: "184",
label: "ADP"
}
},
{
data: {
id: "189",
label: "ATP"
}
}
],
edges: [{
data: {
source: "2",
target: "1"
}
},
{
data: {
source: "4",
target: "2"
}
},
{
data: {
source: "2",
target: "6"
}
},
{
data: {
source: "6",
target: "8"
}
},
{
data: {
source: "8",
target: "10"
}
},
{
data: {
source: "12",
target: "14"
}
},
{
data: {
source: "14",
target: "20"
}
},
{
data: {
source: "20",
target: "22"
}
},
{
data: {
source: "8",
target: "28"
}
},
{
data: {
source: "28",
target: "30"
}
},
{
data: {
source: "30",
target: "22"
}
},
{
data: {
source: "6",
target: "33"
}
},
{
data: {
source: "33",
target: "10"
}
},
{
data: {
source: "33",
target: "36"
}
},
{
data: {
source: "36",
target: "38"
}
},
{
data: {
source: "38",
target: "4"
}
},
{
data: {
source: "38",
target: "41"
}
},
{
data: {
source: "41",
target: "33"
}
},
{
data: {
source: "44",
target: "38"
}
},
{
data: {
source: "52",
target: "12"
}
},
{
data: {
source: "12",
target: "57"
}
},
{
data: {
source: "46",
target: "30"
}
},
{
data: {
source: "30",
target: "47"
}
},
{
data: {
source: "71",
target: "76"
}
},
{
data: {
source: "76",
target: "66"
}
},
{
data: {
source: "76",
target: "36"
}
},
{
data: {
source: "80",
target: "76"
}
},
{
data: {
source: "22",
target: "86"
}
},
{
data: {
source: "86",
target: "80"
}
},
{
data: {
source: "95",
target: "89"
}
},
{
data: {
source: "89",
target: "90"
}
},
{
data: {
source: "102",
target: "104"
}
},
{
data: {
source: "80",
target: "89"
}
},
{
data: {
source: "104",
target: "109"
}
},
{
data: {
source: "115",
target: "109"
}
},
{
data: {
source: "121",
target: "89"
}
},
{
data: {
source: "121",
target: "115"
}
},
{
data: {
source: "121",
target: "128"
}
},
{
data: {
source: "128",
target: "130"
}
},
{
data: {
source: "141",
target: "128"
}
},
{
data: {
source: "128",
target: "136"
}
},
{
data: {
source: "136",
target: "148"
}
},
{
data: {
source: "148",
target: "149"
}
},
{
data: {
source: "149",
target: "156"
}
},
{
data: {
source: "156",
target: "158"
}
},
{
data: {
source: "130",
target: "164"
}
},
{
data: {
source: "164",
target: "166"
}
},
{
data: {
source: "178",
target: "179"
}
},
{
data: {
source: "184",
target: "178"
}
},
{
data: {
source: "178",
target: "189"
}
},
{
data: {
source: "141",
target: "178"
}
},
{
data: {
source: "104",
target: "103"
}
},
{
data: {
source: "10",
target: "12"
}
},
{
data: {
source: "164",
target: "172"
}
},
{
data: {
source: "22",
target: "104"
}
}
]
}
}));
cy.unbind("tapend");
cy.bind("tapend", "node", function() {
cy.animate({
fit: {
eles: cy.elements(),
padding: 20
},
center: {
eles: cy.elements()
}
}, {
duration: 500
});
});
});
body {
font-family: helvetica;
font-size: 14px;
}
#cy {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 999;
}
h1 {
opacity: 0.5;
font-size: 1em;
}
<!DOCTYPE>
<html>
<head>
<title>cytoscape-cola.js demo</title>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<script src="https://unpkg.com/cytoscape/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/webcola/WebCola/cola.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-cola#2.3.0/cytoscape-cola.min.js"></script>
</head>
<body>
<h1>cytoscape-cola demo</h1>
<div id="cy"></div>
</body>
</html>

You can try "Euler" layout, it might give the same expected layout that you wanted. It is euler.js. I am using this layout for my own data set and is giving similar layout that you have shown above.

Related

Cannot read properties of null (reading 'isHeadless') while using cytoscape's layout extensions

I was trying to use "cose-bilkent" layout extension in my react app, when the layout name was changed to "cose-bilkent" from inbuilt "grid" , it throws an error saying
"Cannot read properties of null (reading 'isHeadless')"
Since I am running this on an online code editor, headless shall be false as stated by the document:
To explicitly run a headless instance (e.g. in the browser) you can specify options.headless as true.
So I tried giving "headless:false" explicitly yet I am still getting the same error. I also tried other libraries such as spread, cola to cross check it , I am getting the same console error when I run it.
When navigated to the Line number of cytoscape.cjs.js 18789.35 it has the the following similar code
headless : function () : this.__privateRenderer.isHeadless()
import "./styles.css";
import { useRef, useEffect } from "react";
import cytoscape from "cytoscape";
import COSEBilkent from "cytoscape-cose-bilkent";
import cola from "cytoscape-cola";
import spread from "cytoscape-spread";
import CytoscapeComponent from "react-cytoscapejs";
import Cytoscape from "cytoscape";
Cytoscape.use(cola);
export default function App() {
const containerRef = useRef(null);
useEffect(() => {
const config = {
container: containerRef.current,
layout: {
name: "cola"
},
zoom: 1,
elements: {
nodes: [
{ data: { id: "1", label: "IP 1", type: "ip" } },
{ data: { id: "2", label: "Device 1", type: "device" } },
{ data: { id: "3", label: "IP 2", type: "ip" } },
{ data: { id: "4", label: "Device 2", type: "device" } },
{ data: { id: "5", label: "Device 3", type: "device" } },
{ data: { id: "6", label: "IP 3", type: "ip" } },
{ data: { id: "7", label: "Device 5", type: "device" } },
{ data: { id: "8", label: "Device 6", type: "device" } },
{ data: { id: "9", label: "Device 7", type: "device" } },
{ data: { id: "10", label: "Device 8", type: "device" } },
{ data: { id: "11", label: "Device 9", type: "device" } },
{ data: { id: "12", label: "IP 3", type: "ip" } },
{ data: { id: "13", label: "Device 10", type: "device" } }
],
edges: [
{
data: { source: "1", target: "2", label: "Node2" }
},
{
data: { source: "3", target: "4", label: "Node4" }
},
{
data: { source: "3", target: "5", label: "Node5" }
},
{
data: { source: "6", target: "5", label: " 6 -> 5" }
},
{
data: { source: "6", target: "7", label: " 6 -> 7" }
},
{
data: { source: "6", target: "8", label: " 6 -> 8" }
},
{
data: { source: "6", target: "9", label: " 6 -> 9" }
},
{
data: { source: "3", target: "13", label: " 3 -> 13" }
}
]
},
pannable: true,
position: {
// the model position of the node (optional on init, mandatory after)
x: 600,
y: 100
},
style: [
// the stylesheet for the graph
{
selector: "node",
style: {
"text-valign": "center",
"background-color": "#9D2F2E",
label: "data(id)"
}
},
{
selector: "edge",
style: {
width: 6,
"line-color": "#ccc",
"target-arrow-color": "#dfr",
"target-arrow-shape": "triangle",
"curve-style": "bezier",
label: "data(label)"
}
}
]
};
cytoscape(config);
}, []);
return <div ref={containerRef} style={{ height: "100vh", width: "100vw" }} />;
}
Above is the code that I am running.
Please drop in any inputs you have or any word on if I am going wrong somewhere.
Thanks in advance!
If you still unresolved this problem.
Try rewriting below code.
const cy = cytoscape(config)
cy.layout({
name: 'cola'
}).run()
Then, remove or change this your code.
layout: {
name: "cola"
},
You need to setting other layout options(ex. random, concentric...)
https://js.cytoscape.org/#layouts
reference(about layout.run)
Note that you must call layout.run() in order for it to affect the graph.
https://js.cytoscape.org/#layout.run

How to save and restore layout of compound nodes using cytoscape.js

I'm using the JavaScript library cytoscape.js to draw some topology graph.
I could use the following snippet to save and restore layout for non-compound nodes case.
<button id="save" class="button">save</button>
<button id="reload" class="button">reload</button>
document.getElementById("save").addEventListener("click", function(){
window.localStorage.setItem('savedlayout', JSON.stringify(cy.json()));
});
document.getElementById("reload").addEventListener("click", function(){
cy.json(JSON.parse(window.localStorage.getItem('savedlayout')));
});
This solution seems not working for compound nodes case. I have downloaded some compound nodes demos and tried to add my above snippet, the layout can't be restored correctly.
Please see my demo. You could drag any nodes and click save button, then drag some nodes and click restore.
document.addEventListener('DOMContentLoaded', function(){
var cy = window.cy = cytoscape({
container: document.getElementById('cy'),
ready: function(){
this.nodes().forEach(function(node){
let width = [30, 70, 110];
let size = width[Math.floor(Math.random()*3)];
node.css("width", size);
node.css("height", size);
});
this.layout({name: 'cose-bilkent', animationDuration: 1000}).run();
},
style: [
{
selector: 'node',
style: {
'background-color': '#ad1a66'
}
},
{
selector: ':parent',
style: {
'background-opacity': 0.333
}
},
{
selector: 'edge',
style: {
'width': 3,
'line-color': '#ad1a66'
}
}
],
elements: [{ group:'nodes', data:{ id: 'n0'}},
{ group:'nodes', data:{ id: 'n1'}},
{ group:'nodes', data:{ id: 'n2'}},
{ group:'nodes', data:{ id: 'n3'}},
{ group:'nodes', data:{ id: 'n4', parent: 'n37'}},
{ group:'nodes', data:{ id: 'n5'}},
{ group:'nodes', data:{ id: 'n6'}},
{ group:'nodes', data:{ id: 'n7', parent: 'n37'}},
{ group:'nodes', data:{ id: 'n8', parent: 'n37'}},
{ group:'nodes', data:{ id: 'n9', parent: 'n37'}},
{ group:'nodes', data:{ id: 'n10', parent: 'n38'}},
{ group:'nodes', data:{ id: 'n12'}},
{ group:'nodes', data:{ id: 'n13'}},
{ group:'nodes', data:{ id: 'n14'}},
{ group:'nodes', data:{ id: 'n15'}},
{ group:'nodes', data:{ id: 'n16'}},
{ group:'nodes', data:{ id: 'n17'}},
{ group:'nodes', data:{ id: 'n18'}},
{ group:'nodes', data:{ id: 'n19'}},
{ group:'nodes', data:{ id: 'n20'}},
{ group:'nodes', data:{ id: 'n21'}},
{ group:'nodes', data:{ id: 'n22'}},
{ group:'nodes', data:{ id: 'n23'}},
{ group:'nodes', data:{ id: 'n24', parent: 'n39'}},
{ group:'nodes', data:{ id: 'n25', parent: 'n39'}},
{ group:'nodes', data:{ id: 'n26', parent: 'n42'}},
{ group:'nodes', data:{ id: 'n27', parent: 'n42'}},
{ group:'nodes', data:{ id: 'n28', parent: 'n42'}},
{ group:'nodes', data:{ id: 'n29', parent: 'n40'}},
{ group:'nodes', data:{ id: 'n31', parent: 'n41'}},
{ group:'nodes', data:{ id: 'n32', parent: 'n41'}},
{ group:'nodes', data:{ id: 'n33', parent: 'n41'}},
{ group:'nodes', data:{ id: 'n34', parent: 'n41'}},
{ group:'nodes', data:{ id: 'n35', parent: 'n41'}},
{ group:'nodes', data:{ id: 'n36', parent: 'n41'}},
{ group:'nodes', data:{ id: 'n37'}},
{ group:'nodes', data:{ id: 'n38'}},
{ group:'nodes', data:{ id: 'n39', parent: 'n43'}},
{ group:'nodes', data:{ id: 'n40', parent: 'n42'}},
{ group:'nodes', data:{ id: 'n41', parent: 'n42'}},
{ group:'nodes', data:{ id: 'n42', parent: 'n43'}},
{ group:'nodes', data:{ id: 'n43'}},
{ group:'edges', data:{ id: 'e0', source: 'n0', target: 'n1'} },
{ group:'edges', data:{ id: 'e1', source: 'n1', target: 'n2'} },
{ group:'edges', data:{ id: 'e2', source: 'n2', target: 'n3'} },
{ group:'edges', data:{ id: 'e3', source: 'n0', target: 'n3'} },
{ group:'edges', data:{ id: 'e4', source: 'n1', target: 'n4'} },
{ group:'edges', data:{ id: 'e5', source: 'n2', target: 'n4'} },
{ group:'edges', data:{ id: 'e6', source: 'n4', target: 'n5'} },
{ group:'edges', data:{ id: 'e7', source: 'n5', target: 'n6'} },
{ group:'edges', data:{ id: 'e8', source: 'n4', target: 'n6'} },
{ group:'edges', data:{ id: 'e9', source: 'n4', target: 'n7'} },
{ group:'edges', data:{ id: 'e10', source: 'n7', target: 'n8'} },
{ group:'edges', data:{ id: 'e11', source: 'n8', target: 'n9'} },
{ group:'edges', data:{ id: 'e12', source: 'n7', target: 'n9'} },
{ group:'edges', data:{ id: 'e13', source: 'n13', target: 'n14'} },
{ group:'edges', data:{ id: 'e14', source: 'n12', target: 'n14'} },
{ group:'edges', data:{ id: 'e15', source: 'n14', target: 'n15'} },
{ group:'edges', data:{ id: 'e16', source: 'n14', target: 'n16'} },
{ group:'edges', data:{ id: 'e17', source: 'n15', target: 'n17'} },
{ group:'edges', data:{ id: 'e18', source: 'n17', target: 'n18'} },
{ group:'edges', data:{ id: 'e19', source: 'n18', target: 'n19'} },
{ group:'edges', data:{ id: 'e20', source: 'n17', target: 'n20'} },
{ group:'edges', data:{ id: 'e21', source: 'n19', target: 'n20'} },
{ group:'edges', data:{ id: 'e22', source: 'n16', target: 'n20'} },
{ group:'edges', data:{ id: 'e23', source: 'n20', target: 'n21'} },
{ group:'edges', data:{ id: 'e25', source: 'n23', target: 'n24'} },
{ group:'edges', data:{ id: 'e26', source: 'n24', target: 'n25'} },
{ group:'edges', data:{ id: 'e27', source: 'n26', target: 'n38'} },
{ group:'edges', data:{ id: 'e29', source: 'n26', target: 'n39'} },
{ group:'edges', data:{ id: 'e30', source: 'n26', target: 'n27'} },
{ group:'edges', data:{ id: 'e31', source: 'n26', target: 'n28'} },
{ group:'edges', data:{ id: 'e33', source: 'n21', target: 'n31'} },
{ group:'edges', data:{ id: 'e35', source: 'n31', target: 'n33'} },
{ group:'edges', data:{ id: 'e36', source: 'n31', target: 'n34'} },
{ group:'edges', data:{ id: 'e37', source: 'n33', target: 'n34'} },
{ group:'edges', data:{ id: 'e38', source: 'n32', target: 'n35'} },
{ group:'edges', data:{ id: 'e39', source: 'n32', target: 'n36'} },
{ group:'edges', data:{ id: 'e40', source: 'n16', target: 'n40'} }
],
elements1: [{ group:'nodes', data:{ id: 'n0'}},
{ group:'nodes', data:{ id: 'n1'}},
{ group:'nodes', data:{ id: 'n2'}},
{ group:'nodes', data:{ id: 'n3'}},
{ group:'nodes', data:{ id: 'n4', parent1: 'n37'}},
{ group:'nodes', data:{ id: 'n5'}},
{ group:'nodes', data:{ id: 'n6'}},
{ group:'nodes', data:{ id: 'n7', parent1: 'n37'}},
{ group:'nodes', data:{ id: 'n8', parent1: 'n37'}},
{ group:'nodes', data:{ id: 'n9', parent1: 'n37'}},
{ group:'nodes', data:{ id: 'n10', parent1: 'n38'}},
{ group:'nodes', data:{ id: 'n12'}},
{ group:'nodes', data:{ id: 'n13'}},
{ group:'nodes', data:{ id: 'n14'}},
{ group:'nodes', data:{ id: 'n15'}},
{ group:'nodes', data:{ id: 'n16'}},
{ group:'nodes', data:{ id: 'n17'}},
{ group:'nodes', data:{ id: 'n18'}},
{ group:'nodes', data:{ id: 'n19'}},
{ group:'nodes', data:{ id: 'n20'}},
{ group:'nodes', data:{ id: 'n21'}},
{ group:'nodes', data:{ id: 'n22'}},
{ group:'nodes', data:{ id: 'n23'}},
{ group:'nodes', data:{ id: 'n24', parent1: 'n39'}},
{ group:'nodes', data:{ id: 'n25', parent1: 'n39'}},
{ group:'nodes', data:{ id: 'n26', parent1: 'n42'}},
{ group:'nodes', data:{ id: 'n27', parent1: 'n42'}},
{ group:'nodes', data:{ id: 'n28', parent1: 'n42'}},
{ group:'nodes', data:{ id: 'n29', parent1: 'n40'}},
{ group:'nodes', data:{ id: 'n31', parent1: 'n41'}},
{ group:'nodes', data:{ id: 'n32', parent1: 'n41'}},
{ group:'nodes', data:{ id: 'n33', parent1: 'n41'}},
{ group:'nodes', data:{ id: 'n34', parent1: 'n41'}},
{ group:'nodes', data:{ id: 'n35', parent1: 'n41'}},
{ group:'nodes', data:{ id: 'n36', parent1: 'n41'}},
{ group:'edges', data:{ id: 'e0', source: 'n0', target: 'n1'} },
{ group:'edges', data:{ id: 'e1', source: 'n1', target: 'n2'} },
{ group:'edges', data:{ id: 'e2', source: 'n2', target: 'n3'} },
{ group:'edges', data:{ id: 'e3', source: 'n0', target: 'n3'} },
{ group:'edges', data:{ id: 'e4', source: 'n1', target: 'n4'} },
{ group:'edges', data:{ id: 'e5', source: 'n2', target: 'n4'} },
{ group:'edges', data:{ id: 'e6', source: 'n4', target: 'n5'} },
{ group:'edges', data:{ id: 'e7', source: 'n5', target: 'n6'} },
{ group:'edges', data:{ id: 'e8', source: 'n4', target: 'n6'} },
{ group:'edges', data:{ id: 'e9', source: 'n4', target: 'n7'} },
{ group:'edges', data:{ id: 'e10', source: 'n7', target: 'n8'} },
{ group:'edges', data:{ id: 'e11', source: 'n8', target: 'n9'} },
{ group:'edges', data:{ id: 'e12', source: 'n7', target: 'n9'} },
{ group:'edges', data:{ id: 'e13', source: 'n13', target: 'n14'} },
{ group:'edges', data:{ id: 'e14', source: 'n12', target: 'n14'} },
{ group:'edges', data:{ id: 'e15', source: 'n14', target: 'n15'} },
{ group:'edges', data:{ id: 'e16', source: 'n14', target: 'n16'} },
{ group:'edges', data:{ id: 'e17', source: 'n15', target: 'n17'} },
{ group:'edges', data:{ id: 'e18', source: 'n17', target: 'n18'} },
{ group:'edges', data:{ id: 'e19', source: 'n18', target: 'n19'} },
{ group:'edges', data:{ id: 'e20', source: 'n17', target: 'n20'} },
{ group:'edges', data:{ id: 'e21', source: 'n19', target: 'n20'} },
{ group:'edges', data:{ id: 'e22', source: 'n16', target: 'n20'} },
{ group:'edges', data:{ id: 'e23', source: 'n20', target: 'n21'} },
{ group:'edges', data:{ id: 'e25', source: 'n23', target: 'n24'} },
{ group:'edges', data:{ id: 'e26', source: 'n24', target: 'n25'} },
{ group:'edges', data:{ id: 'e30', source: 'n26', target: 'n27'} },
{ group:'edges', data:{ id: 'e31', source: 'n26', target: 'n28'} },
{ group:'edges', data:{ id: 'e33', source: 'n21', target: 'n31'} },
{ group:'edges', data:{ id: 'e35', source: 'n31', target: 'n33'} },
{ group:'edges', data:{ id: 'e36', source: 'n31', target: 'n34'} },
{ group:'edges', data:{ id: 'e37', source: 'n33', target: 'n34'} },
{ group:'edges', data:{ id: 'e38', source: 'n32', target: 'n35'} },
{ group:'edges', data:{ id: 'e39', source: 'n32', target: 'n36'} },
]
});
document.getElementById("layoutButton").addEventListener("click", function(){
var layout = cy.layout({
name: 'cose-bilkent',
animate: 'end',
animationEasing: 'ease-out',
animationDuration: 1000,
randomize: true
});
layout.run();
});
document.getElementById("randomize").addEventListener("click", function(){
var layout = cy.layout({
name: 'random',
animate: true,
animationDuration: 1000,
animationEasing: 'ease-out'
});
layout.run();
});
document.getElementById("save").addEventListener("click", function(){
window.localStorage.setItem('savedlayout', JSON.stringify(cy.json()));
});
document.getElementById("reload").addEventListener("click", function(){
cy.json(JSON.parse(window.localStorage.getItem('savedlayout')));
});
});
body {
font-family: helvetica;
font-size: 14px;
}
#cy {
width: 100%;
height: 90%;
z-index: 999;
}
h1 {
opacity: 0.5;
font-size: 1em;
}
button {
margin-right: 10px;
}
<script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
<!--polyfills are needed for this extension for old browsers like IE -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/shim.min.js"></script>
<script src="https://unpkg.com/layout-base/layout-base.js"></script>
<script src="https://unpkg.com/cose-base/cose-base.js"></script>
<script src="https://unpkg.com/cytoscape-cose-bilkent/cytoscape-cose-bilkent.js"></script>
<button id="randomize" type="button">Randomize</button>
<button id="layoutButton" type="button">CoSE-Bilkent</button>
<button id="save" class="button">save</button>
<button id="reload" class="button">reload</button>
<div id="cy"></div>
EDIT:
I tried this again, after #Ravenous pointed out the benefits of cy.json():
Using cy.json() in this example works just fine, if you delete the other elements before adding the old ones.
As long as you run the preset layout, the code can also look like this:
document.getElementById("save").addEventListener("click", function () {
window.localStorage.setItem("elements", JSON.stringify( cy.json() ));
});
document.getElementById("reload").addEventListener("click", function () {
cy.elements().remove();
cy.json({ elements: JSON.parse( window.localStorage.getItem("elements") ).elements }).layout({ name: 'preset' }).run();
});
or
document.getElementById("save").addEventListener("click", function() {
window.localStorage.setItem("elements", JSON.stringify({
nodes: cy.nodes.jsons(),
edges: cy.edges.jsons()
}));
});
document.getElementById("reload").addEventListener("click", function() {
cy.elements().remove();
cy.add( JSON.parse(window.localStorage.getItem("elements")) ).layout({ name: 'preset' }).run();
});
Old answer:
This problem has already been adressed here, but it took me some time to find it. Basically, you'll want to use cy.nodes().jsons() insted of just cy.json():
var elements = {
nodes: cy.nodes().jsons(),
edges: cy.edges().jsons(),
};
This way you get the collection with every attribute in a JSON format. After that, you can save the elements.json to your local storage and get them back into the graph via cy.add(). I added a preset layout for the graph, that way the positions are not mixxed up after being reloaded:
var elements1 = [{
group: "nodes",
data: {
id: "n0"
}
},
{
group: "nodes",
data: {
id: "n1"
}
},
{
group: "nodes",
data: {
id: "n2"
}
},
{
group: "nodes",
data: {
id: "n3"
}
},
{
group: "nodes",
data: {
id: "n4",
parent1: "n37"
}
},
{
group: "nodes",
data: {
id: "n5"
}
},
{
group: "nodes",
data: {
id: "n6"
}
},
{
group: "nodes",
data: {
id: "n7",
parent1: "n37"
}
},
{
group: "nodes",
data: {
id: "n8",
parent1: "n37"
}
},
{
group: "nodes",
data: {
id: "n9",
parent1: "n37"
}
},
{
group: "nodes",
data: {
id: "n10",
parent1: "n38"
}
},
{
group: "nodes",
data: {
id: "n12"
}
},
{
group: "nodes",
data: {
id: "n13"
}
},
{
group: "nodes",
data: {
id: "n14"
}
},
{
group: "nodes",
data: {
id: "n15"
}
},
{
group: "nodes",
data: {
id: "n16"
}
},
{
group: "nodes",
data: {
id: "n17"
}
},
{
group: "nodes",
data: {
id: "n18"
}
},
{
group: "nodes",
data: {
id: "n19"
}
},
{
group: "nodes",
data: {
id: "n20"
}
},
{
group: "nodes",
data: {
id: "n21"
}
},
{
group: "nodes",
data: {
id: "n22"
}
},
{
group: "nodes",
data: {
id: "n23"
}
},
{
group: "nodes",
data: {
id: "n24",
parent1: "n39"
}
},
{
group: "nodes",
data: {
id: "n25",
parent1: "n39"
}
},
{
group: "nodes",
data: {
id: "n26",
parent1: "n42"
}
},
{
group: "nodes",
data: {
id: "n27",
parent1: "n42"
}
},
{
group: "nodes",
data: {
id: "n28",
parent1: "n42"
}
},
{
group: "nodes",
data: {
id: "n29",
parent1: "n40"
}
},
{
group: "nodes",
data: {
id: "n31",
parent1: "n41"
}
},
{
group: "nodes",
data: {
id: "n32",
parent1: "n41"
}
},
{
group: "nodes",
data: {
id: "n33",
parent1: "n41"
}
},
{
group: "nodes",
data: {
id: "n34",
parent1: "n41"
}
},
{
group: "nodes",
data: {
id: "n35",
parent1: "n41"
}
},
{
group: "nodes",
data: {
id: "n36",
parent1: "n41"
}
},
{
group: "edges",
data: {
id: "e0",
source: "n0",
target: "n1"
}
},
{
group: "edges",
data: {
id: "e1",
source: "n1",
target: "n2"
}
},
{
group: "edges",
data: {
id: "e2",
source: "n2",
target: "n3"
}
},
{
group: "edges",
data: {
id: "e3",
source: "n0",
target: "n3"
}
},
{
group: "edges",
data: {
id: "e4",
source: "n1",
target: "n4"
}
},
{
group: "edges",
data: {
id: "e5",
source: "n2",
target: "n4"
}
},
{
group: "edges",
data: {
id: "e6",
source: "n4",
target: "n5"
}
},
{
group: "edges",
data: {
id: "e7",
source: "n5",
target: "n6"
}
},
{
group: "edges",
data: {
id: "e8",
source: "n4",
target: "n6"
}
},
{
group: "edges",
data: {
id: "e9",
source: "n4",
target: "n7"
}
},
{
group: "edges",
data: {
id: "e10",
source: "n7",
target: "n8"
}
},
{
group: "edges",
data: {
id: "e11",
source: "n8",
target: "n9"
}
},
{
group: "edges",
data: {
id: "e12",
source: "n7",
target: "n9"
}
},
{
group: "edges",
data: {
id: "e13",
source: "n13",
target: "n14"
}
},
{
group: "edges",
data: {
id: "e14",
source: "n12",
target: "n14"
}
},
{
group: "edges",
data: {
id: "e15",
source: "n14",
target: "n15"
}
},
{
group: "edges",
data: {
id: "e16",
source: "n14",
target: "n16"
}
},
{
group: "edges",
data: {
id: "e17",
source: "n15",
target: "n17"
}
},
{
group: "edges",
data: {
id: "e18",
source: "n17",
target: "n18"
}
},
{
group: "edges",
data: {
id: "e19",
source: "n18",
target: "n19"
}
},
{
group: "edges",
data: {
id: "e20",
source: "n17",
target: "n20"
}
},
{
group: "edges",
data: {
id: "e21",
source: "n19",
target: "n20"
}
},
{
group: "edges",
data: {
id: "e22",
source: "n16",
target: "n20"
}
},
{
group: "edges",
data: {
id: "e23",
source: "n20",
target: "n21"
}
},
{
group: "edges",
data: {
id: "e25",
source: "n23",
target: "n24"
}
},
{
group: "edges",
data: {
id: "e26",
source: "n24",
target: "n25"
}
},
{
group: "edges",
data: {
id: "e30",
source: "n26",
target: "n27"
}
},
{
group: "edges",
data: {
id: "e31",
source: "n26",
target: "n28"
}
},
{
group: "edges",
data: {
id: "e33",
source: "n21",
target: "n31"
}
},
{
group: "edges",
data: {
id: "e35",
source: "n31",
target: "n33"
}
},
{
group: "edges",
data: {
id: "e36",
source: "n31",
target: "n34"
}
},
{
group: "edges",
data: {
id: "e37",
source: "n33",
target: "n34"
}
},
{
group: "edges",
data: {
id: "e38",
source: "n32",
target: "n35"
}
},
{
group: "edges",
data: {
id: "e39",
source: "n32",
target: "n36"
}
}
];
document.addEventListener("DOMContentLoaded", function() {
var cy = (window.cy = cytoscape({
container: document.getElementById("cy"),
ready: function() {
this.nodes().forEach(function(node) {
let width = [30, 70, 110];
let size = width[Math.floor(Math.random() * 3)];
node.css("width", size);
node.css("height", size);
});
this.layout({
name: "cose-bilkent",
animationDuration: 1000
}).run();
},
style: [{
selector: "node",
style: {
'content': 'data(id)',
'text-valign': 'center',
'text-halign': 'center',
'background-color': '#ad1a66'
}
},
{
selector: ":parent",
style: {
"background-opacity": 0.333
}
},
{
selector: "edge",
style: {
width: 3,
"line-color": "#ad1a66"
}
}
],
elements: [{
group: "nodes",
data: {
id: "n0"
}
},
{
group: "nodes",
data: {
id: "n1"
}
},
{
group: "nodes",
data: {
id: "n2"
}
},
{
group: "nodes",
data: {
id: "n3"
}
},
{
group: "nodes",
data: {
id: "n4",
parent: "n37"
}
},
{
group: "nodes",
data: {
id: "n5"
}
},
{
group: "nodes",
data: {
id: "n6"
}
},
{
group: "nodes",
data: {
id: "n7",
parent: "n37"
}
},
{
group: "nodes",
data: {
id: "n8",
parent: "n37"
}
},
{
group: "nodes",
data: {
id: "n9",
parent: "n37"
}
},
{
group: "nodes",
data: {
id: "n10",
parent: "n38"
}
},
{
group: "nodes",
data: {
id: "n12"
}
},
{
group: "nodes",
data: {
id: "n13"
}
},
{
group: "nodes",
data: {
id: "n14"
}
},
{
group: "nodes",
data: {
id: "n15"
}
},
{
group: "nodes",
data: {
id: "n16"
}
},
{
group: "nodes",
data: {
id: "n17"
}
},
{
group: "nodes",
data: {
id: "n18"
}
},
{
group: "nodes",
data: {
id: "n19"
}
},
{
group: "nodes",
data: {
id: "n20"
}
},
{
group: "nodes",
data: {
id: "n21"
}
},
{
group: "nodes",
data: {
id: "n22"
}
},
{
group: "nodes",
data: {
id: "n23"
}
},
{
group: "nodes",
data: {
id: "n24",
parent: "n39"
}
},
{
group: "nodes",
data: {
id: "n25",
parent: "n39"
}
},
{
group: "nodes",
data: {
id: "n26",
parent: "n42"
}
},
{
group: "nodes",
data: {
id: "n27",
parent: "n42"
}
},
{
group: "nodes",
data: {
id: "n28",
parent: "n42"
}
},
{
group: "nodes",
data: {
id: "n29",
parent: "n40"
}
},
{
group: "nodes",
data: {
id: "n31",
parent: "n41"
}
},
{
group: "nodes",
data: {
id: "n32",
parent: "n41"
}
},
{
group: "nodes",
data: {
id: "n33",
parent: "n41"
}
},
{
group: "nodes",
data: {
id: "n34",
parent: "n41"
}
},
{
group: "nodes",
data: {
id: "n35",
parent: "n41"
}
},
{
group: "nodes",
data: {
id: "n36",
parent: "n41"
}
},
{
group: "nodes",
data: {
id: "n37"
}
},
{
group: "nodes",
data: {
id: "n38"
}
},
{
group: "nodes",
data: {
id: "n39",
parent: "n43"
}
},
{
group: "nodes",
data: {
id: "n40",
parent: "n42"
}
},
{
group: "nodes",
data: {
id: "n41",
parent: "n42"
}
},
{
group: "nodes",
data: {
id: "n42",
parent: "n43"
}
},
{
group: "nodes",
data: {
id: "n43"
}
},
{
group: "edges",
data: {
id: "e0",
source: "n0",
target: "n1"
}
},
{
group: "edges",
data: {
id: "e1",
source: "n1",
target: "n2"
}
},
{
group: "edges",
data: {
id: "e2",
source: "n2",
target: "n3"
}
},
{
group: "edges",
data: {
id: "e3",
source: "n0",
target: "n3"
}
},
{
group: "edges",
data: {
id: "e4",
source: "n1",
target: "n4"
}
},
{
group: "edges",
data: {
id: "e5",
source: "n2",
target: "n4"
}
},
{
group: "edges",
data: {
id: "e6",
source: "n4",
target: "n5"
}
},
{
group: "edges",
data: {
id: "e7",
source: "n5",
target: "n6"
}
},
{
group: "edges",
data: {
id: "e8",
source: "n4",
target: "n6"
}
},
{
group: "edges",
data: {
id: "e9",
source: "n4",
target: "n7"
}
},
{
group: "edges",
data: {
id: "e10",
source: "n7",
target: "n8"
}
},
{
group: "edges",
data: {
id: "e11",
source: "n8",
target: "n9"
}
},
{
group: "edges",
data: {
id: "e12",
source: "n7",
target: "n9"
}
},
{
group: "edges",
data: {
id: "e13",
source: "n13",
target: "n14"
}
},
{
group: "edges",
data: {
id: "e14",
source: "n12",
target: "n14"
}
},
{
group: "edges",
data: {
id: "e15",
source: "n14",
target: "n15"
}
},
{
group: "edges",
data: {
id: "e16",
source: "n14",
target: "n16"
}
},
{
group: "edges",
data: {
id: "e17",
source: "n15",
target: "n17"
}
},
{
group: "edges",
data: {
id: "e18",
source: "n17",
target: "n18"
}
},
{
group: "edges",
data: {
id: "e19",
source: "n18",
target: "n19"
}
},
{
group: "edges",
data: {
id: "e20",
source: "n17",
target: "n20"
}
},
{
group: "edges",
data: {
id: "e21",
source: "n19",
target: "n20"
}
},
{
group: "edges",
data: {
id: "e22",
source: "n16",
target: "n20"
}
},
{
group: "edges",
data: {
id: "e23",
source: "n20",
target: "n21"
}
},
{
group: "edges",
data: {
id: "e25",
source: "n23",
target: "n24"
}
},
{
group: "edges",
data: {
id: "e26",
source: "n24",
target: "n25"
}
},
{
group: "edges",
data: {
id: "e27",
source: "n26",
target: "n38"
}
},
{
group: "edges",
data: {
id: "e29",
source: "n26",
target: "n39"
}
},
{
group: "edges",
data: {
id: "e30",
source: "n26",
target: "n27"
}
},
{
group: "edges",
data: {
id: "e31",
source: "n26",
target: "n28"
}
},
{
group: "edges",
data: {
id: "e33",
source: "n21",
target: "n31"
}
},
{
group: "edges",
data: {
id: "e35",
source: "n31",
target: "n33"
}
},
{
group: "edges",
data: {
id: "e36",
source: "n31",
target: "n34"
}
},
{
group: "edges",
data: {
id: "e37",
source: "n33",
target: "n34"
}
},
{
group: "edges",
data: {
id: "e38",
source: "n32",
target: "n35"
}
},
{
group: "edges",
data: {
id: "e39",
source: "n32",
target: "n36"
}
},
{
group: "edges",
data: {
id: "e40",
source: "n16",
target: "n40"
}
}
],
}));
document.getElementById("layoutButton").addEventListener("click", function() {
var layout = cy.layout({
name: "cose-bilkent",
animate: "end",
animationEasing: "ease-out",
animationDuration: 1000,
randomize: true
});
layout.run();
});
document.getElementById("randomize").addEventListener("click", function() {
var layout = cy.layout({
name: "random",
animate: true,
animationDuration: 1000,
animationEasing: "ease-out"
});
layout.run();
});
document.getElementById("save").addEventListener("click", function() {
window.localStorage.setItem("elements", JSON.stringify(cy.json()));
});
document.getElementById("reload").addEventListener("click", function() {
cy.elements().remove();
cy.json({
elements: JSON.parse(window.localStorage.getItem("elements")).elements
}).layout({
name: 'preset'
}).run();
cy.fit();
cy.center();
});
});
body {
font-family: helvetica;
font-size: 14px;
}
#cy {
height: 100%;
width: 90%;
position: absolute;
}
h1 {
opacity: 0.5;
font-size: 1em;
}
button {
margin-right: 10px;
}
<script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
<!--polyfills are needed for this extension for old browsers like IE -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/shim.min.js"></script>
<script src="https://unpkg.com/layout-base/layout-base.js"></script>
<script src="https://unpkg.com/cose-base/cose-base.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-cose-bilkent#4.1.0/cytoscape-cose-bilkent.min.js"></script>
<body>
<button id="randomize" type="button">Randomize</button>
<button id="layoutButton" type="button">CoSE-Bilkent</button>
<button id="save" class="button">save</button>
<button id="reload" class="button">reload</button>
<div id="cy"></div>
</body>

Elastic (animated) edges in graph with nodes exerting gravitational pull on neighbours?

Is there a layout extension for Cytoscape that enables a dynamic behavior similar to that of the Neo4j Browser, i.e. when you drag a node, its edges are elastic to some extend, but that also drag connected nodes along part of the way (I'd call it localized gravity)?
Update:
The example I'm working on uses the CoSE Bilkent (compound) layout extension, but it doesn't seem to support the effect/animation I'm after out of the box. I had hoped the scarcely documented { gravity: 1 } option might be related - after all, when a node pulls other nodes along, that could well be described as gravity - but changing the setting on a small sample graph had no such effect; some digging ensued and I now think that setting has to do with how close to the graph's center nodes are displayed. I then looked at all layout demos, none seem to display the effect I'm after out of the box. AllegroViva appears to implement similar behavior (video), but from my understanding, it's rendering a time series in that video (so might just be manually redrawing the graph). Their website seems to be disfunct, too.
What I'm looking for is something easy, ideally out of the box, or something that can be implemented quickly. Unfortunately, I'm not quite sure what the right search term would be for what I want (especially in Cytoscape's domain). Hence me asking what is admittedly a very high-level question. Today, I found out Visjs calls it physics events (or at least uses the effect I'm after as part of that demo) - but as I mentioned, gravity in Cytoscape is apparently not what I'm looking for. At this point, however, I'm merely trying to establish if Cytoscape is the right library, or whether I need to look at others, like Alchemy (cf. its Philosophers' Relatedness example).
Answer:
Thank you for editing the question, I think i can help you with this one. Cytoscape.js has this feature buried deep in the extension layouts, specifically in cytoscape.js-cola. The layout is a physics layout with "springy" like nodes, so that the distance between them stays the same. The catch here is, that in the paragraph Notes, the author describes this:
If you want to maintain interactivity, you probably should not mix infinite: true with fit: true. Fitting naturally changes the zoom level, making dragging misaligned and feel weird to users --- though it still works technically. Better to just fit: false when infinite: true, and cy.center() or cy.fit() on layoutready.
The alignment option isn't as flexible as the raw Cola option. Here, only integers can be used to specify relative positioning, so it's a bit limited. If you'd like to see a more sophisticated implementation, please send a pull request.
Code:
So this example should work (note that the nodes can be moved, but they tend to stay in formation because of their already optimal position):
document.addEventListener("DOMContentLoaded", function() {
var cy = (window.cy = cytoscape({
container: document.getElementById("cy"),
autounselectify: true,
boxSelectionEnabled: false,
layout: {
name: "cola",
infinite: true,
fit: false
},
style: [{
selector: "node",
css: {
"background-color": "#f92411"
}
},
{
selector: "edge",
css: {
"line-color": "#f92411"
}
}
],
elements: {
nodes: [{
data: {
id: "1",
label: "P"
}
},
{
data: {
id: "2",
label: "sucrose phosphate phosphatase"
}
},
{
data: {
id: "4",
label: "sucrose 6-phosphate"
}
},
{
data: {
id: "6",
label: "sucrose"
}
},
{
data: {
id: "8",
label: "invertase"
}
},
{
data: {
id: "10",
label: "fructose"
}
},
{
data: {
id: "12",
label: "fructokinase"
}
},
{
data: {
id: "14",
label: "fructose 6-phosphate"
}
},
{
data: {
id: "20",
label: "phosphoglucose isomerase"
}
},
{
data: {
id: "22",
label: "glucose 6-phosphate"
}
},
{
data: {
id: "28",
label: "glucose"
}
},
{
data: {
id: "30",
label: "hexokinase"
}
},
{
data: {
id: "33",
label: "sucrose synthase"
}
},
{
data: {
id: "36",
label: "UDP - glucose"
}
},
{
data: {
id: "38",
label: "sucrose phosphate synthase"
}
},
{
data: {
id: "41",
label: "UDP"
}
},
{
data: {
id: "44",
label: "fructose 6-phosphate"
}
},
{
data: {
id: "46",
label: "ATP"
}
},
{
data: {
id: "47",
label: "ATP"
}
},
{
data: {
id: "52",
label: "ATP"
}
},
{
data: {
id: "57",
label: "ADP"
}
},
{
data: {
id: "66",
label: "PP"
}
},
{
data: {
id: "71",
label: "UTP"
}
},
{
data: {
id: "76",
label: "UDP glucose pyrophosphorylase"
}
},
{
data: {
id: "80",
label: "glucose 1-phosphate"
}
},
{
data: {
id: "86",
label: "phospho- glucomutase (cPGM)"
}
},
{
data: {
id: "89",
label: "G1P transporter"
}
},
{
data: {
id: "90",
label: "P"
}
},
{
data: {
id: "95",
label: "P"
}
},
{
data: {
id: "102",
label: "P"
}
},
{
data: {
id: "103",
label: "P"
}
},
{
data: {
id: "104",
label: "G6P transporter"
}
},
{
data: {
id: "109",
label: "glucose 6-phosphate"
}
},
{
data: {
id: "115",
label: "phospho- glucomutase (cPGM)"
}
},
{
data: {
id: "121",
label: "glucose 1-phosphate"
}
},
{
data: {
id: "128",
label: "ADPglucose pyrophosphorylase (pAGPase)"
}
},
{
data: {
id: "130",
label: "ADP - glucose"
}
},
{
data: {
id: "136",
label: "PP"
}
},
{
data: {
id: "141",
label: "ATP"
}
},
{
data: {
id: "148",
label: "inorganic diphosphatase"
}
},
{
data: {
id: "149",
label: "P"
}
},
{
data: {
id: "156",
label: "phosphate transporter"
}
},
{
data: {
id: "158",
label: "P"
}
},
{
data: {
id: "164",
label: "starch synthase (simpl.)"
}
},
{
data: {
id: "166",
label: "ADP"
}
},
{
data: {
id: "172",
label: "starch"
}
},
{
data: {
id: "178",
label: "ATP/ADP transporter"
}
},
{
data: {
id: "179",
label: "ADP"
}
},
{
data: {
id: "184",
label: "ADP"
}
},
{
data: {
id: "189",
label: "ATP"
}
}
],
edges: [{
data: {
source: "2",
target: "1"
}
},
{
data: {
source: "4",
target: "2"
}
},
{
data: {
source: "2",
target: "6"
}
},
{
data: {
source: "6",
target: "8"
}
},
{
data: {
source: "8",
target: "10"
}
},
{
data: {
source: "12",
target: "14"
}
},
{
data: {
source: "14",
target: "20"
}
},
{
data: {
source: "20",
target: "22"
}
},
{
data: {
source: "8",
target: "28"
}
},
{
data: {
source: "28",
target: "30"
}
},
{
data: {
source: "30",
target: "22"
}
},
{
data: {
source: "6",
target: "33"
}
},
{
data: {
source: "33",
target: "10"
}
},
{
data: {
source: "33",
target: "36"
}
},
{
data: {
source: "36",
target: "38"
}
},
{
data: {
source: "38",
target: "4"
}
},
{
data: {
source: "38",
target: "41"
}
},
{
data: {
source: "41",
target: "33"
}
},
{
data: {
source: "44",
target: "38"
}
},
{
data: {
source: "52",
target: "12"
}
},
{
data: {
source: "12",
target: "57"
}
},
{
data: {
source: "46",
target: "30"
}
},
{
data: {
source: "30",
target: "47"
}
},
{
data: {
source: "71",
target: "76"
}
},
{
data: {
source: "76",
target: "66"
}
},
{
data: {
source: "76",
target: "36"
}
},
{
data: {
source: "80",
target: "76"
}
},
{
data: {
source: "22",
target: "86"
}
},
{
data: {
source: "86",
target: "80"
}
},
{
data: {
source: "95",
target: "89"
}
},
{
data: {
source: "89",
target: "90"
}
},
{
data: {
source: "102",
target: "104"
}
},
{
data: {
source: "80",
target: "89"
}
},
{
data: {
source: "104",
target: "109"
}
},
{
data: {
source: "115",
target: "109"
}
},
{
data: {
source: "121",
target: "89"
}
},
{
data: {
source: "121",
target: "115"
}
},
{
data: {
source: "121",
target: "128"
}
},
{
data: {
source: "128",
target: "130"
}
},
{
data: {
source: "141",
target: "128"
}
},
{
data: {
source: "128",
target: "136"
}
},
{
data: {
source: "136",
target: "148"
}
},
{
data: {
source: "148",
target: "149"
}
},
{
data: {
source: "149",
target: "156"
}
},
{
data: {
source: "156",
target: "158"
}
},
{
data: {
source: "130",
target: "164"
}
},
{
data: {
source: "164",
target: "166"
}
},
{
data: {
source: "178",
target: "179"
}
},
{
data: {
source: "184",
target: "178"
}
},
{
data: {
source: "178",
target: "189"
}
},
{
data: {
source: "141",
target: "178"
}
},
{
data: {
source: "104",
target: "103"
}
},
{
data: {
source: "10",
target: "12"
}
},
{
data: {
source: "164",
target: "172"
}
},
{
data: {
source: "22",
target: "104"
}
}
]
}
}));
cy.unbind("tapend");
cy.bind("tapend", "node", function() {
cy.animate({
fit: {
eles: cy.elements(),
padding: 20
},
center: {
eles: cy.elements()
}
}, {
duration: 500
});
});
});
body {
font-family: helvetica;
font-size: 14px;
}
#cy {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 999;
}
h1 {
opacity: 0.5;
font-size: 1em;
}
<!DOCTYPE>
<html>
<head>
<title>cytoscape-cola.js demo</title>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<script src="https://unpkg.com/cytoscape/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/webcola/WebCola/cola.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-cola#2.3.0/cytoscape-cola.min.js"></script>
</head>
<body>
<h1>cytoscape-cola demo</h1>
<div id="cy"></div>
</body>
</html>
Conclusion:
Cytoscape.js has the ability to display the force layout, but it is a little worse than the two examples you provided (they can display the graph without moving the nodes to their optimal position but the user preferred one). If you like to use cytoscape.js, you can do that just fine, but always consider which use cases you need and check, if any other application can handle the situation better (cytoscape.js has so many awesome features you can use, so if you can make it work, cytoscape would be my way to go).
Best of luck!
You can use force-directed layout of d3.js Here is an example graph.
I believe Neo4j Browser also uses d3.js
From the official website:
The default Neo4j Server has a powerful, customizable data visualization tool based on the built-in D3.js library.
I found a video that may be similar as your questionUse of Cytoscape.js for Client project, also there is another extension LAYOUT with d3.js cytoscape.js-d3-forcethat may be a good reference.

GraphQL schema with nested objects

How to write schema for this kind of response.
{
"adult": false,
"backdrop_path": "/dnaitaoCh8MftfYEVnprcuYExZp.jpg",
"belongs_to_collection": {
"id": 256322,
"name": "The Purge Collection",
"poster_path": "/nP3c8mTSxlis4vfg0UjlkK8LRG9.jpg",
"backdrop_path": "/quFWGOA4I5KCTsyDbvLh6PHNZwv.jpg"
},
"budget": 13000000,
"genres": [
{
"id": 28,
"name": "Action"
},
{
"id": 27,
"name": "Horror"
},
{
"id": 878,
"name": "Science Fiction"
},
{
"id": 53,
"name": "Thriller"
}
]
}
const typeDefs = `
type BelongsToCollectionType {
id: ID!
name: String
poster_path: String
backdrop_path: String
}
type GenreType {
id: ID!
name: String
}
type SomeType {
adult: Boolean
backdrop_path: String
belongs_to_collection: BelongsToCollectionType
budget: Int
genres: [GenreType]!
}
`;
Soon after I asked this question I figured it out. Here is the schema:
const MovieType = new GraphQLObjectType({
name: 'Movie',
fields: () => ({
id: { type: GraphQLString },
adult: { type: GraphQLBoolean },
backdrop_path: { type: GraphQLString },
belongs_to_collection: { type: BelongsToCollection },
budget: { type: GraphQLInt },
overview: { type: GraphQLString },
popularity: { type: GraphQLInt },
poster_path: { type: GraphQLString },
production_companies: {
type: new GraphQLList(CompaniesType)
},
genres: {
type: new GraphQLList(GenreType)
},
release_date: { type: GraphQLString },
tagline: { type: GraphQLString },
title: { type: GraphQLString },
vote_average: { type: GraphQLInt },
vote_count: { type: GraphQLInt }
})
});
const CompaniesType = new GraphQLObjectType({
name: 'ProductionCompanies',
fields: {
id: { type: GraphQLInt },
name: { type: GraphQLString },
logo_path: { type: GraphQLString },
original_country: { type: GraphQLString }
}
});
const GenreType = new GraphQLObjectType({
name: 'Genre',
fields: () => ({
id: { type: GraphQLInt },
name: { type: GraphQLString }
})
})
const BelongsToCollection = new GraphQLObjectType({
name: 'BelongsToCollection',
fields: () => ({
id: { type: GraphQLInt },
name: { type: GraphQLString },
poster_path: { type: GraphQLString },
backdrop_path: { type: GraphQLString }
})
});

using Select2 with Durandal

Hi I am trying to use Select2 (multiselect) with Durandal (http://jsfiddle.net/anasnakawa/6XvqX/381/),
but the popup does not work below is part of my VM and HTML, here I am trying to bind a list of states to an input it should work as an auto complete but there seems to a issue showing the selection popup, can someone please help
define(['durandal/app', 'services/datacontext', 'plugins/router', 'services/bindinghandlers'],
function (app, datacontext, router) {
var withs = ko.observableArray(),
states = [
{ id: "AL", text: "Alabama" },
{ id: "AK", text: "Alaska" },
{ id: "AZ", text: "Arizona" },
{ id: "AR", text: "Arkansas" },
{ id: "CA", text: "California" },
{ id: "CO", text: "Colorado" },
{ id: "CT", text: "Connecticut" },
{ id: "DE", text: "Delaware" },
{ id: "FL", text: "Florida" },
{ id: "GA", text: "Georgia" },
{ id: "HI", text: "Hawaii" },
{ id: "ID", text: "Idaho" },
{ id: "IL", text: "Illinois" },
{ id: "IN", text: "Indiana" },
{ id: "IA", text: "Iowa" },
{ id: "KS", text: "Kansas" },
{ id: "KY", text: "Kentucky" },
{ id: "LA", text: "Louisiana" },
{ id: "ME", text: "Maine" },
{ id: "MD", text: "Maryland" },
{ id: "MA", text: "Massachusetts" },
{ id: "MI", text: "Michigan" },
{ id: "MN", text: "Minnesota" },
{ id: "MS", text: "Mississippi" },
{ id: "MO", text: "Missouri" },
{ id: "MT", text: "Montana" },
{ id: "NE", text: "Nebraska" },
{ id: "NV", text: "Nevada" },
{ id: "NH", text: "New Hampshire" },
{ id: "NJ", text: "New Jersey" },
{ id: "NM", text: "New Mexico" },
{ id: "NY", text: "New York" },
{ id: "NC", text: "North Carolina" },
{ id: "ND", text: "North Dakota" },
{ id: "OH", text: "Ohio" },
{ id: "OK", text: "Oklahoma" },
{ id: "OR", text: "Oregon" },
{ id: "PA", text: "Pennsylvania" },
{ id: "RI", text: "Rhode Island" },
{ id: "SC", text: "South Carolina" },
{ id: "SD", text: "South Dakota" },
{ id: "TN", text: "Tennessee" },
{ id: "TX", text: "Texas" },
{ id: "UT", text: "Utah" },
{ id: "VT", text: "Vermont" },
{ id: "VA", text: "Virginia" },
{ id: "WA", text: "Washington" },
{ id: "WV", text: "West Virginia" },
{ id: "WI", text: "Wisconsin" },
{ id: "WY", text: "Wyoming" }
];
var vm = {
withs: withs,
states: states,
};
return vm;
});
<select multiple="true" data-bind="options: states, optionsValue: 'id', optionsText: 'text', selectedOptions: withs, select2: {}" style="width: 300px"></select>
Register select2 in the Require.js configuration inside main.js:
requirejs.config({
paths: {
'select2': 'path/to/select2'
}
});
Define it in your view model:
define(['select2'], function (select2) {
// ...
});
Don't forget to declare it. You'll have to use the attached callback:
var vm = {
withs: withs,
states: states,
attached: function() {
$('select').select2();
}
};