Cytoscape Cola Layout: How to restart the layout without any change of positions? - layout

I'm trying to use the Cytoscape cola layout to render a graph that should apply a force directed layout while using it (so when dragging nodes around, they should act as if there is some gravity involved).
Relevant libraries:
https://github.com/cytoscape/cytoscape.js
https://github.com/tgdwyer/WebCola
https://github.com/cytoscape/cytoscape.js-cola
My first problem is that adding nodes to the graph via add(node) doesn't include them in the cola layout algorithm. The only way I found around that is to destroy the layout, re-initialize it and start it again. But this causes the nodes to jump in some cases.
I assumed that this was due to the fact that I completely destroyed the old layout but when setting up a minimal example, I realized that even just calling layout.stop() and layout.run() leads to nodes being repositioned.
In the following example, there is only one node. Moving the node via drag and drop, then pressing the "stop" button and then the "start" button causes the node to jump back to its initial position:
document.addEventListener('DOMContentLoaded', function(){
// Register cola layout
cytoscapeCola(cytoscape);
var nodes = [{ data: { id: 1, name: 1 } }]
var edges = [];
var cy = window.cy = cytoscape({
container: document.getElementById('cy'),
style: [
{
selector: 'node[name]',
style: {
'content': 'data(name)'
}
},
{
selector: 'edge',
style: {
'curve-style': 'bezier',
'target-arrow-shape': 'triangle'
}
},
],
elements: {
nodes: nodes,
edges: edges
}
});
var layout = cy.layout({
name: 'cola',
infinite: true,
fit: false,
});
layout.run();
document.querySelector('#start').addEventListener('click', function() {
layout.run();
});
document.querySelector('#stop').addEventListener('click', function() {
layout.stop();
});
document.querySelector('#add-node').addEventListener('click', function() {
var id = Math.random();
cy.add({ group: 'nodes', data: { id: id, name: id } });
cy.add({ group: 'edges', data: { source: id, target: _.head(nodes).data.id } });
layout.stop();
layout.destroy();
layout = cy.layout({
name: 'cola',
infinite: true,
fit: false,
});
layout.run();
});
});
body {
font-family: helvetica neue, helvetica, liberation sans, arial, sans-serif;
font-size: 14px;
}
#cy {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 999;
}
h1 {
opacity: 0.5;
font-size: 1em;
font-weight: bold;
}
#buttons {
position: absolute;
right: 0;
bottom: 0;
z-index: 99999;
}
<!DOCTYPE>
<html>
<head>
<title>cytoscape-edgehandles.js demo for infinite layout</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>
<script src="https://unpkg.com/webcola/WebCola/cola.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-cola#2.4.0/cytoscape-cola.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.js"></script>
<script src="cytoscape-edgehandles.js"></script>
</head>
<body>
<h1>cytoscape-edgehandles demo with an infinite layout</h1>
<div id="cy"></div>
<div id="buttons">
<button id="start">Start</button>
<button id="stop">Stop</button>
<button id="add-node">Add Node</button>
</div>
</body>
</html>
Is this a bug or am I doing something wrong? Does anyone know how to stop and restart the layout without the nodes changing their position?
Thanks a lot,
Jesse

Okay actually you were very close #Stephan.
The problem was that WebCola centers the nodes when calling start by default:
https://github.com/tgdwyer/WebCola/blob/78a24fc0dbf0b4eb4a12386db9c09b087633267d/src/layout.ts#L504
The cytoscape wrapper for WebCola does not currently support this option, so I forked it and added the option myself:
https://github.com/deje1011/cytoscape.js-cola/commit/f357b97aba900327e12f97b1530c4df624ff9d61
I'll open a pull request at some point.
Now you can smoothly restart the layout like this:
layout.stop();
layout.destroy(); // cleanup event listeners
layout = graph.layout({ name: 'cola', infinite: true, fit: false, centerGraph: false });
layout.run()
This way, the nodes keep their position 🎉

Related

Phaser 3: Spritesheet doesn't load correctly

I tried to add a spritesheet to my Phaser game the same way I've done it a few times before, however this time it seems to not load the images correctly, causing it to display a black & green square instead and causing it to throw an error when trying to play an animation.
Can anyone tell what is causing this issue?
(Warning: Playing the Code here seems to freeze up your browser for a few seconds, alternatively view on JSFiddle: https://jsfiddle.net/cak3goru/4/ )
// Configs and Constants
const gameState = {
gameWidth: 800,
gameHeight: 800,
textStyle: {
fontFamily: "'Comic Sans MS'",
fill: "#fff",
align: "center",
boundsAlignH: "left",
boundsAlignV: "top"
},
mouseDown: false,
menu: {
options: [
"Feed", "Clean", "Play"
],
barColor: "0x123456",
items: [],
itemText: [],
itemColor: "0x654321",
}
};
function preload() {
// Clean Tools
this.load.atlas('clean', 'https://gaylie.neocities.org/game/assets/clean.png', 'https://gaylie.neocities.org/game/assets/clean.json');
}
function create () {
this.anims.create({
key: "shower_cursor",
framerate: 12,
frames: this.anims.generateFrameNumbers("clean", {
prefix: "showerhead_000",
start: 0,
end: 7}),
repeat: -1
});
gameState.shower_cursor = this.add.sprite(400,400,'shower_cursor');
console.log(gameState.shower_cursor.frame);
//gameState.shower_cursor.playAfterDelay('shower_cursor', 100);
//gameState.shower_cursor.alpha = 0;
}
var config = {
backgroundColor: "0xf0f0f0",
scale: {
width: gameState.gameWidth,
height: gameState.gameHeight,
autoCenter: Phaser.Scale.CENTER_BOTH
},
scene: {
preload, create
}
};
var game = new Phaser.Game(config);
<head>
<title>Pet Simulator</title>
<script src="//cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.min.js"></script>
</head>
<body style="height: 100%; width: 100%; margin: 0;">
</body>
The problem is, that you are using the wrong function, you should use this.anims.generateFrameNames, and not this.anims.generateFrameNumbers.
And set the correct key clean for the sprite.
the line should be:
gameState.shower_cursor = this.add.sprite(200, 100, 'clean');
because shower_cursor, is only the key of the animation, not the key of the sprite.
P.s.: the posted code doesn't run on jsfiddler or Stackoverflow due to CORS-Error, but if all assets are on the same server, it should not be a problem.

What's the real meaning of randomize=false using cose-bilkent layout

What the real meaning of randomize=false using cose-bilkent layout ?
I can only read such a simple comment to describe this feature in this extension wiki page.
// Whether to enable incremental mode
randomize: true,
I also find another description for randomize in cytoscape.js-expand-collapse wiki page.
// recommended usage: use cose-bilkent layout with randomize: false to preserve mental map upon expand/collapse
So I have learned that randomize: false is to preserve mental map. But what is the real behavior ?
Suppose that I have run a topology graph using cose-bilkent layout, then call the following snippet to append new nodes and edges to the graph with randomize: false. What will be the expected result on incremental mode ? Should the old nodes and edges keep the relative position as so called mental map , while new nodes and edges should be positioned to a resonable coordinate?
var demoId = 10000;
document.getElementById("append").addEventListener("click", function() {
var tid = cy.nodes()[Math.floor(Math.random()*cy.nodes().length)].data().id;
var sid = ++demoId;
var data = {
nodes : [
{data:{"id":"id"+source, "name":"name"+source}}
],
edges :[
{data:{"id":"id"+source+"-"+target, "source":"id"+source, "target":target}}
]
};
cy.add(data);
cy.layout({
name: 'cose-bilkent',
animate: false,
randomize : false,
fit : false
}).run();
});
See my demo. My test result is that all nodes and edges will be relayout. You could watch some nodes and their neighborhood. The neighborhood will be changed after each layout. I can't get a clear mental map. So could the experts explain how incremental layout task effect here.
document.addEventListener("DOMContentLoaded", function() {
var cy = (window.cy = cytoscape({
container: document.getElementById("cy"),
ready: function() {
this.layout({
name: "cose-bilkent",
animationDuration: 1000
}).run();
},
style: [{
selector: "node",
style: {
'content': 'data(id)',
'text-valign': 'center',
'text-halign': 'center',
'background-color': 'blue',
'color':'red',
'width':'10px',
'height':'10px'
}
},
{
selector: ":parent",
style: {
"background-opacity": 0.333
}
},
{
selector: "edge",
style: {
width: 3,
"line-color": "#ad1a66"
}
}
],
elements: [{"group":"nodes","data":{"id":"p_1","parent":"n_72"}},{"group":"nodes","data":{"id":"p_298","parent":"n_72"}},{"group":"nodes","data":{"id":"p_4","parent":"n_72"}},{"group":"nodes","data":{"id":"p_5","parent":"n_72"}},{"group":"nodes","data":{"id":"p_9","parent":"n_72"}},{"group":"nodes","data":{"id":"p_32","parent":"n_72"}},{"group":"nodes","data":{"id":"p_607","parent":"n_72"}},{"group":"nodes","data":{"id":"p_57","parent":"n_72"}},{"group":"nodes","data":{"id":"p_242","parent":"n_72"}},{"group":"nodes","data":{"id":"p_64","parent":"n_72"}},{"group":"nodes","data":{"id":"p_77","parent":"n_72"}},{"group":"nodes","data":{"id":"p_81","parent":"n_72"}},{"group":"nodes","data":{"id":"p_82","parent":"n_72"}},{"group":"nodes","data":{"id":"p_289","parent":"n_72"}},{"group":"nodes","data":{"id":"p_803","parent":"n_72"}},{"group":"nodes","data":{"id":"n_0"}},{"group":"nodes","data":{"id":"n_72"}},{"group":"edges","data":{"source":"n_0","target":"n_0","id":"n_0-n_0"}},{"group":"edges","data":{"source":"n_0","target":"p_81","id":"n_0-p_81"}},{"group":"edges","data":{"source":"n_0","target":"p_4","id":"n_0-p_4"}},{"group":"edges","data":{"source":"n_0","target":"p_298","id":"n_0-p_298"}},{"group":"edges","data":{"source":"n_0","target":"p_803","id":"n_0-p_803"}},{"group":"edges","data":{"source":"n_0","target":"p_607","id":"n_0-p_607"}},{"group":"edges","data":{"source":"n_0","target":"p_1","id":"n_0-p_1"}},{"group":"edges","data":{"source":"n_0","target":"p_289","id":"n_0-p_289"}},{"group":"edges","data":{"source":"n_0","target":"p_57","id":"n_0-p_57"}},{"group":"edges","data":{"source":"n_0","target":"p_82","id":"n_0-p_82"}},{"group":"edges","data":{"source":"n_0","target":"p_32","id":"n_0-p_32"}},{"group":"edges","data":{"source":"p_77","target":"n_0","id":"p_77-n_0"}},{"group":"edges","data":{"source":"p_64","target":"n_0","id":"p_64-n_0"}},{"group":"edges","data":{"source":"p_607","target":"n_0","id":"p_607-n_0"}},{"group":"edges","data":{"source":"p_5","target":"n_0","id":"p_5-n_0"}},{"group":"edges","data":{"source":"p_9","target":"n_0","id":"p_9-n_0"}},{"group":"edges","data":{"source":"p_9","target":"p_803","id":"p_9-p_803"}},{"group":"edges","data":{"source":"p_242","target":"n_0","id":"p_242-n_0"}}],
}));
var demoId = 10000;
document.getElementById("append").addEventListener("click", function() {
var tid = cy.nodes()[Math.floor(Math.random()*cy.nodes().length)].data().id;
var sid = ++demoId;
var data = {
nodes : [
{data:{"id":sid}}
],
edges :[
{data:{"id":sid+"-"+tid, "source":sid, "target":tid}}
]
};
cy.add(data);
cy.layout({
name: 'cose-bilkent',
animate: false,
randomize : false,
fit : true
}).run();
});
});
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="append" class="button">Append</button>
<div id="cy"></div>
</body>

How can I set an Overlay on top when I click on it

I have an Openlayers map with a lot of overlays (Point-coordinates).
These overlays are often very close to each other or overlapping.
When I click on an existing Overlay I want the Overlay to be set on top, so that it is fully seen, not behind any other Overlay.
So far I have only seen that the Layers can be set with an z-index. Is it possible to do that with overlays, too?
I would like to do something like that:
map.setLayerIndex(markers, 99);
but with an overlay
Overlays are controls, which are positioned on an coordinate instead of being in a fixed place. They are basically nothing more but regular html div elements and change position with the map.
This also means, you can apply normal CSS styling and use z-index on them.
var layer = new ol.layer.Tile({
source: new ol.source.OSM()
});
var map = new ol.Map({
layers: [layer],
target: 'map',
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
// Vienna marker
var marker1 = new ol.Overlay({
position: ol.proj.fromLonLat([16.3725, 48.208889]),
positioning: 'center-center',
element: document.getElementById('marker1'),
stopEvent: false,
className: 'm1 ol ol-overlay-container ol-selectable'
});
map.addOverlay(marker1);
marker2 = new ol.Overlay({
position: ol.proj.fromLonLat([23.3725, 48.208889]),
positioning: 'center-center',
element: document.getElementById('marker2'),
stopEvent: false,
className: 'm2 ol ol-overlay-container ol-selectable'
});
map.addOverlay(marker2);
function clicked(selector) {
console.log('clicked overlay', selector);
document.querySelectorAll(".ol").forEach(function(el){
el.classList.remove('active');
});
document.querySelector(selector).classList.add('active');
}
html, body, .map {
min-height: 50px;
min-width: 50px;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
.marker {
width: 30px;
height: 30px;
border: 1px solid #088;
border-radius: 15px;
background-color: #0FF;
}
.m1 .marker {
background-color: #FF0;
}
.active {
z-index: 1234782904789;
}
.active .marker {
background-color: red;
}
<link href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#main/dist/en/v7.0.0/legacy/ol.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#main/dist/en/v7.0.0/legacy/ol.js"></script>
<div id="map" class="map"></div>
<div id="marker1" title="Marker" class="marker" onclick="clicked('.m1')"></div>
<div id="marker2" title="Marker" class="marker" onclick="clicked('.m2')"></div>
The existing answer works, but it doesn't preserve the z-order of the overlays, it only guarantees that the clicked one will be on top. Since it is the only element with a z-index in this stacking context, the z-order of the other elements will be random.
Here is a solution that brings the clicked overlay to the front, while preserving the current z-order of all the other ones:
export function bringToFront (map: PluggableMap, clickedOverlayElement: HTMLElement) {
const overlays = map.getOverlays().sort(zIndexComparator);
overlays.forEach((overlay, i) => {
const element = overlay.get('element');
const container = pointInfo.closest('.ol-overlay-container') as HTMLElement;
container.style.zIndex = element === clickedOverlayElement ? overlays.length.toFixed() : i.toFixed();
});
}
function getOverlayContainer (overlay: Overlay) {
return overlay.get('element').closest('.ol-overlay-container') as HTMLElement;
}
function zIndexComparator (a: Overlay, b: Overlay) {
return (getOverlayContainer(a).style.zIndex > getOverlayContainer(b).style.zIndex)
? 1
: -1;
}
Just call the bringToFront() function when your overlay element is clicked.

Is it possible to change color of one pixel inside rectangle created with KineticJS?

This is my code:
var Editor = {
layer: new Kinetic.Layer(),
map: null,
init: function () {
var stage = new Kinetic.Stage({
container: 'board',
width: 800,
height: 800
})
this.map = new Kinetic.Shape({
sceneFunc: function(context) {
context.beginPath();
context.moveTo(0, 0);
context.lineTo(mapWidth, 0);
context.lineTo(mapWidth, mapHeight);
context.lineTo(0, mapHeight);
context.closePath();
context.fillStrokeShape(this);
},
x: 0,
y: 0,
fill: 'green',
draggable: true
})
this.layer.add(this.map)
stage.add(this.layer)
}
}
I want to change the colors of the pixels in the rectangle. Colors of pixels will be generated by the "diamond-square" algorithm. Is it possible to change the colors of individual pixels? If so, how can I do this?
[ Changed answer ]
Use an offscreen html canvas to overlay the pixels in your "diamond-square" algorithm.
Demo: http://jsfiddle.net/m1erickson/6mDSm/
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Prototype</title>
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<script src="http://d3lp1msu2r81bx.cloudfront.net/kjs/js/lib/kinetic-v5.0.1.min.js"></script>
<style>
body{padding:20px;}
#container{
border:solid 1px #ccc;
margin-top: 10px;
width:350px;
height:350px;
}
</style>
<script>
$(function(){
var stage = new Kinetic.Stage({
container: 'container',
width: 350,
height: 350
});
var layer = new Kinetic.Layer();
stage.add(layer);
var mapWidth=50;
var mapHeight=75;
// use an offscreen canvas as a pixel-map overlaying the green map
var pixelCanvas=document.createElement("canvas");
var ctx=pixelCanvas.getContext("2d");
pixelCanvas.width=mapWidth;
pixelCanvas.height=mapHeight;
pixelCanvas.pixels=[]
pixelCanvas.setPixel=function(x,y,color){
ctx.fillStyle=color;
ctx.fillRect(x,y,1,1);
};
// create a group
// that holds the green map background and pixel-map overlay
var mapGroup=new Kinetic.Group({
x:30,
y:30,
width:mapWidth,
height:mapHeight,
draggable:true
});
layer.add(mapGroup);
// the green background
var map=new Kinetic.Rect({
x:0,
y:0,
width:mapWidth,
height:mapHeight,
fill:"green"
});
mapGroup.add(map);
// an image overlay that
// gets "live-updates" from an offscreen canvas
var pixels=new Kinetic.Image({
x:0,
y:0,
image:pixelCanvas
});
mapGroup.add(pixels);
layer.draw();
// testing
var y=15;
$("#add").click(function(){
for(var i=0;i<5;i++){
pixelCanvas.setPixel(15,y,"red");
pixelCanvas.setPixel(25,y,"gold");
pixelCanvas.setPixel(35,y++,"blue");
}
pixels.draw();
});
}); // end $(function(){});
</script>
</head>
<body>
<button id="add">Add test Pixels</button>
<div id="container"></div>
</body>
</html>
Note: my previous answer using Kinetic.Shape was leaving an undesirable "ghost"

fusion table can not show two layers if we add different styles to the two layers

Due to the limit of 5 fusion table layers and 5 styles of one fusion table layer, I have to try it as: use 5 fusion table layers and each one use two styles, then I can get to my purpose: show 10 different styles in a map.
But after I implemented, I found it only show the first fusion table layer.
Then I wrote a testing case to check why. And found:
If we set styles in two layers, only the first layer can be displayed and the second one is gone. If I set style for one layer, it works well.
Below is my code, can someone help on it? Now only one layer is displayed. If we comment the style setting for them or one of them, both layers can be displayed.
Thanks in advance!
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<style type="text/css">
html { height: 100% }
body { height: 100%; margin: 0px; padding: 0px }
#top-box {padding: 10px; background-color:#336699;}
.para-line {font-weight:bold;}
#map_canvas { height: 100% }
</style>
<script type="text/javascript"src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript">
var map;
function initialize() {
map = new google.maps.Map(document.getElementById("map_canvas"));
map.setMapTypeId('roadmap');
map.setCenter(new google.maps.LatLng(38.4985464, -98.3834298));
map.setZoom(4);
var tableid1 = 4436842;
var style = [{
where: "State in('IL','PA')",
polygonOptions:
{
fillColor: "#rrggbb",
fillOpacity: 0.7
}
},{
where: "State in('AL')",
polygonOptions:
{
fillColor: "#006400",
fillOpacity: 0.7
}
}
];
var query1 = {
select: ['geometry','name'],
from: tableid1,
where: "State in('IL','PA')"
}
var query2 = {
select: ['geometry','name'],
from: tableid1,
where: "State in('AL')"
}
var layer1 = new google.maps.FusionTablesLayer({
query:query1,
styles: style,
suppressInfoWindows: false,
clickable:true
});
layer1.setMap(map);
var layer2 = new google.maps.FusionTablesLayer({
query:query2,
styles:style,
suppressInfoWindows: false,
clickable:true
});
layer2.setMap(map);
return;
}
</script>
</head>
<body onload="initialize()">
<div id="map_canvas" style="width:100%; height:100%"></div>
</body>
</html>
The limit is (from the documentation "You can use the Maps API to add up to five Fusion Tables layers to a map, one of which can be styled with up to five styling rules."
You can style the layers using the FusionTable User Interface, but only one can be styled dynamically and that one can only have 5 styling rules.

Resources