Cytoscape-cxtmenu.js : Displaying Menu commands based on condition - cytoscape.js

I have an app using cytoscape version 3.15.2 and cxtmenu library version 3.1.1. I have a case where I want to add conditions on some edges based on a condition. I have a command called 'info', which I am applying on the menu's on all edges in the graph:
let edges = {
menuRadius: 100,
selector: 'edge',
commands: [ ...
{ // info command
.........
}
]
}
I have another command called "expand", which I want to apply on edges which have a value true for the expand attribute on the edge. So for the edges that satisfy this condition, I want to have both the info and the expand command available. For this, I created another menu variable as follows
let expandEdges = {
menuRadius: 100,
selector: 'edge[expand=true]',
commands: [ ...
{ // info command
.........
},
{ // expand command
.........
}
]
}
When I use this implementation where I apply both the menu variables to the graph, I see the following on my graph.
If you observe, the info and expand commands are overlapped by the info command I applied on all edges. Im not sure how to use selector conditions to apply commands to all edges and some commands based on conditions.
I also wanted to know if I can use something like selector: 'edge[type.contains'expand']' (substring check) as a selector condition.
Also, I've not been able to create a reproducible example as this is part of a very huge app. Sorry for that.

Related

How do you pass nodes to gapInequalities in cytoscape-cola.js?

The cytoscape.js-cola documentation for the gapInequalities element of a layout dictionary says:
gapInequalities: undefined, // list of inequality constraints for the gap between the nodes,
// e.g. [{"axis":"y", "left":node1, "right":node2, "gap":25}]
How do you set up the objects that specify the nodes in the values of left and right?
maxkfranz helpfully pointed out here that these objects need to be collection objects. These contain references to specified nodes ("elements") and are created by querying the cytoscape.js "core object". What's not clear to me is, given that the layout object needs to refer to the node elements, and the elements should not be added to the graph before the layout tells how to render them, how do you properly set up those collection objects?
For example, to specify that node b should be placed above node a, what goes in place of ???a??? and ???b??? in the code below?
cy = cytoscape({
elements: [
{ data: { id: 'a' } },
{ data: { id: 'b' } },
. . .
],
layout: {
name: 'cola',
gapInequalities: [
{ axis: 'y', left: ???'a'???, right: ???'b'???, gap: 25 }
. . .
],
. . .
}
. . .
]);
In this case, the answer can't be cy.$id('a') and cy.$id('b') because the cy object hasn't been created yet. You could get around that by creating the cy object without elements and then calling cy.add() to put them in. But then what goes in the layout object that's passed to cytoscape()?
I'm new to both cytoscape.js and cola.js, so I'm most likely just missing some very elementary idea here. A simple example showing what function calls set up the objects and the sequence in which to call them would probably do it. In my application, nodes and edges are added to the graph gradually, and the animation needs to show them being added, so not having all the elements set up at the start makes more sense, anyway.
Since your nodes and edges are added gradually, you can create cy object without elements and layout object. Then when new nodes come, you can add them to the graph and apply layout.
After initialization of the cy object, every time new nodes come, apply the following :
cy.add(...);
cy.layout({
name: 'cola',
gapInequalities: [{ axis: 'y', left: cy.$id("a"), right: cy.$id("b"), gap: 25 }],
...
}).run();

Object reactivity of complex object

I have an issue with complex object reactivity.
I've read everything I can on stack to find a way to solve it, but nothing works. I've looked at object reactvity and array caveats on vuejs, but not working either.
So I'm asking some help please.
Let me explain the project:
I have 2 columns :
- on the left side, I CRUD my content
- on the right side, I display the results
I have my object, and I'm adding new elements on its "blocks" property (text, images, etc...)
[
{
"uid": 1573224607087,
"animation": "animationName",
"background": {
"bckColor": "#ff55ee",
...
},
"blocks": []
}
]
On click event, I add a new element via this method. Everything is ok, I can CRUD a block.
addBloc(el) {
if (el.type == "text") {
const datasA = {
type: "text",
uid: Date.now(),
slideId: this.pagination.currentPage,
content: el.content,
css: {
color: "#373737",
...
},
...
};
this.slides[this.pagination.currentPage].blocks.push(datasA);
this.$bus.$emit("newElement", datasA);
}
To modify the order of my elements on the display side, I added a drag and drop module to move my block on my DOM tree. Smooth dnd
The problem is, when I drang&drop my element, my object is updated correctly, but the DOM isn't. The dragged element goes back to its initial position.
What is strange, when I try to modify my block (the one I dragged), it modifies the other one.
I'me adding a small video, so you can see what's happening.
Small animation to show you what's going on
I add some more explainations.
I use event bus to communicate between my components, and the right side is using its own object!
I don't know how I can solve this issue.
Tell me if you need more information.
Thank you all !
EDIT 1 :
I added an id to each block to see what happens when I start Drag&Drop. ==> blocks are moving correctly. The problem is not coming from the method onDrop() but from my nested components if I understand well. They don't update. I'm going to search for this new issue.
I've added a new gif to show what's going on.
This is the nested structure
TheSidebar.vue => top container
<Container
:data-index="i"
#drop="onDrop(i,$event)"
:get-child-payload="itemIndex => getChildPayload(i, itemIndex)"
lock-axis="y"
>
<Draggable
v-show="pagination.currentPage === i"
v-for="(input, index) in slides[i].blocks"
:key="index.uid"
:id="'slideBlocksContainer'+index"
class="item"
>
blockId #{{input.uid}}
<AppContainer
v-if="input.type == 'text'"
:blocType="input.type"
:placeholder="input.content"
:id="index"
:slideId="i"
></AppContainer>
</Draggable>
</Container>
Then I have my AppContainer.vue file, which is a top level. In this I have the specific elements of each input type
And I have AppElement.vue file, which is common elements, I can use everywhere
Something like this
TheSidebar
--AppContainer
----AppElement
Know I don't know yet, how to force vue to update AppContainer.vue and AppElement.vue
EDIT 2 :
As suggested in this article I've changed the key of the component and now , when I drag and drop my elements, they stay where they are dropped.
What I see also, is that the AppElement inputs, are related to their own AppContainer. So everything is ok now, but I don't know if it is best practices.
The issue appears to be that the Smooth dnd library you are using is not updating the array of blocks that you are passing to it, it is likely making a copy of the array internally. So when you change the position of the blocks by dragging and dropping, you are not changing your blocks array, just the internal copy.
Looking at the Smooth dnd documentation, if you wanted to access the modified array you could try using the drag-end event handler:
onDragEnd (dragResult) {
const { isSource, payload, willAcceptDrop } = dragResult
}

Cytoscape.js move nodes to allow for pasted nodes

I have to provide a copy/paste functionality using the dagre layout. The idea is the user copies a node and where ever they decide to "paste" it, the hierarchy of nodes copied, will be created there. This would mean that all nodes in the way would have to move.
I first thought maybe I could call layout again but that doesn't "fit" them in.
I'm still learning cytoscape.js so if this is a simple question, please excuse me.
This would be something quite hard to achieve, so hear me out:
First step:
Cytoscape has a extension called context-menus (demo)
Another extension is called clipboard (demo)
make yourself familiar with these two
Second step:
create your graph with these two extensions and a dagre graph
when you copy these nodes, make the copy function behave like this:
add the nodes with their edges in the right hierarchy
when you right click on a node, add a insert into hierarchy function, which adds the copied nodes into the graph
Code examples:
var options1 = {
// List of initial menu items
menuItems: [
{
id: 'addToHierarchy',
content: 'Add to hierarchy',
tooltipText: 'Add nodes to hierarchy here',
selector: 'node',
onClickFunction: function () {
// your handlerFunction
},
disabled: false
}
]
};
var instance = cy.contextMenus( options1 );
var options2 = {
clipboardSize: 0,
// The following 4 options allow the user to provide custom behavior to
// the extension. They can be used to maintain consistency of some data
// when elements are duplicated.
// These 4 options are set to null by default. The function prototypes
// are provided below for explanation purpose only.
// Function executed on the collection of elements being copied, before
// they are serialized in the clipboard
beforeCopy: function(eles) {},
// Function executed on the clipboard just after the elements are copied.
// clipboard is of the form: {nodes: json, edges: json}
afterCopy: function(clipboard) {},
// Function executed on the clipboard right before elements are pasted,
// when they are still in the clipboard.
beforePaste: function(clipboard) {},
// Function executed on the collection of pasted elements, after they
// are pasted.
afterPaste: function(eles) {}
};
var clipboard = cy.clipboard(options2);

prevent node overlap on add new node

how do i prevent from overlapping new added nodes in cytoscape js graph?
i do not want change position of existing nodes
i want new adding nodes do automatically position by this rule that preventing existing node overlap
when i use add API to add nodes to graph added nodes overlap others
how can i change this behavior?
is there a solution for prevent node overlap in adding mode (new node do not overlap existing node)?
cy.add([
{ data: {label:"aaa" ,id:"bbb" } },
{ data: {label:"aaa333" ,id:"rrrr"} },
]);
There is no built in solution. I just look for an empty spot using this code for testing overlap (this is given a fixed node size, you could adapt it for using bounding boxes, but that slows things down).
function overlap(pos, size) {
var overlap = false;
cy.nodes(':visible').forEach(function (node) {
var npos = node.position();
if ((pos.x - size) < npos.x && (pos.x + size) > npos.x && (pos.y - size) < npos.y && (pos.y + size) > npos.y) {
overlap = true;
return false; //break
}
});
return overlap;
}
I use this with the prefered position as first argument ({x:..,y:..}), and when that fails I just add 10 to y to see if it fitst. Wether this is the best solution for you depends on the specific problem.
If you want an automated way of doing this, your best bet is a force-directed/physics layout.
Lock all but the new nodes when you run the layout.
You'll have to experiment with different layouts to see which particular one suits your app/data best.

Cytoscape : multiple instances and no graph display

I want to display several instances of cytoscape in a single page, in a time sequence: first one set of nodes are displayed on the graph, the user must interact with it (create edges), then he moves to a second graph (#cy0 is :hidden and #cy1 is :visible).
For code optimisation sake I wish to use the same initialisation function to display different successive sets of nodes. My initialisation function works fine in the first instance, but the graph is not created (cy.initrender() == false) in the second session. A command is probably missing, I tested a couple, but I don't see what to do.
Here is my code:
//elements
$(function(){ // on dom ready
var elesJson = {
nodes: [
{ data: { id: 'S', faveShape: 'rectangle',} }
...
],
edges: [
{ data: { id: 'loan', source: 'B', target: 'U' } },
...
],
};
// instance index
var indexLevel=0;
// cy initialisation
$("#cy"+indexLevel).cytoscape({
style: cytoscape.stylesheet()...
elements: elesJson,
ready: function(){
window.cy = this;});
// jQuery command to move from one instance to the other.
$('#next').click(function(){
$("#cy"+indexLevel).css("visibility","hidden");
indexLevel++;
$("#cy"+indexLevel).css("visibility","visible");
cy.load(elesJson);
cy.ready();
console.log(cy.initrender());
});
I am able to generate my node.collection, it is not empty, but the canvas element is not created and/or displayed within the #cy div, and cy.initrender() returns "false".
Any solution to this?
As noted in the docs for init, you must call cy.resize() if you play around with the cy div's display or position: http://js.cytoscape.org/#core/initialisation
cy.resize() : http://js.cytoscape.org/#core/viewport-manipulation/cy.resize
Edit: You may want to use z-index instead to simplify things...