Dynamically adding nodes to Cytoscape - javascript

My company is building a graph-view editor for chatbots. We are using Cytoscape along with the cytoscape-cola extension to accomplish this. One of the issues we are facing is dynamically adding new nodes to the graph without them overlapping with existing nodes on the graph.
I have looked through previous similar questions(listed below) but to no avail:
Cytoscape - handle locked nodes
I tried the solution in there i.e. applying the layout only on the newly added nodes but they keep getting stacked in the centre of the screen.
Cytoscape - ignore locked nodes
I tried the solution mentioned in here but regardless of locking the nodes in one go i.e. cy.nodes().lock() or individually i.e. cy.nodes().forEach(node => node.lock()), the locked nodes still keep moving. Also interesting thing to note here is that when locking nodes individually, the newly added node(s) are also locked regardless of whether or not I am locking in the call above or not.
Cytoscape - dynamically add nodes without moving others
I tried this solution as well but the locked nodes still move and the complete layout is changed - sometimes altogether, sometimes just a little.
Code
This is currently what I am using to construct the graph:
const layoutConfig = {
name: "cola",
handleDisconnected: true,
animate: true,
avoidOverlap: true,
infinite: false,
unconstrIter: 1,
userConstIter: 0,
allConstIter: 1,
ready: e => {
e.cy.fit()
e.cy.center()
}
}
this.graph = Cytoscape({ ... })
this.layout = this.grapg.makeLayout(layoutConfig)
this.layout.run();
This is what I am using to add new node(s) to the graph:
const addElements = (elements: ElementSingular | ElementMultiple) => {
this.graph.nodes().forEach(node => {
node.lock();
})
this.graph.add(elements)
this.layout = this.graph.makeLayout(layoutConfig)
this.layout.on("layoutready", () => {
this.nodes().forEach(node => {
node.unlock();
})
})
this.layout.run()
this.graph.nodes().forEach(node => {
node.unlock();
})
}
I'd like to do one of the following:
Understand what I am doing wrong if what I am trying to accomplish is possible but my code doesn't achieve it
Understand why I would not be able to accomplish it i.e. the limitations that govern it

Edit: Is this what you were looking for? https://output.jsbin.com/hokineluwo
Edit: I didn't saw before, but you are also unlocking the nodes right after the layout call, Cola is async, the run only kicks-off the process. Remove that code and only use the layoutstop method.
I don't remember correctly, but i think that cola keeps moving the elements after the layoutready. From their code:
// trigger layoutready when each node has had its position set at least once
There is a layoutstop event that you could use (and cytoscape-cola uses too).
From the docs (emphasis is mine):
layoutready : when a layout has set initial positions for all the
nodes (but perhaps not final positions)
layoutstop : when a layout has
finished running completely or otherwise stopped running
I would try to remove that callback and inspect if it gives good results.
Also, you should try to reproduce it here: http://jsbin.com/fiqugiq
It makes it easier for others (even the authors) to play with it and see if you have found a bug or not.

Related

React: UI not painted correctly with dhtmlx-gantt library when applied with filters

I am using React with dhtmlx-gantt library to create gantt chart. I met with issue when using the filter function together with useEffect/useLayoutEffect lifecycle.
The gist of it is that the browser is not painting/rendering the correct UI on the screen on certain condition.
The start state load screen looks like this:
6 Task
After filter, this should be how it looks like:
Should be left with 5 task after filtering away duration > 4
But this is how it is:
Left with 5 task but an empty row is shown rather than "refreshing" (not sure if this is the right term to use)
I have created a github repo with different scenario describing the problem, and how to reproduce those issue. More information on how to run the sample can be found in the README.md. Please let me know if more information needs to be provided.
Sample 1: Using conditional rendering will cause issue on painting UI changes
Sample 2: Turning smart_rendering config on and off cause issue on painting UI changes
Sample 3: Calling the function within the parent component and in child component with exact same code cause issue on painting UI
My desired outcome is to able to render the UI correctly, whether or not this code to filter the data is ran on parent or child component.
I should also mention that a workaround was to use if (document.querySelector(".gantt_grid")) gantt.render(); rather than gantt.refreshData() in onBeforeTaskDisplay event which will then correctly paint the UI changes. But I still like to understand why does this happens. Is there anything I did wrongly in term of using the React lifecycle and so on.
Thank you.
Your code looks fine and should work correctly.
The issue is on dhtmlxGantt end, it has been confirmed and is now fixed in the dev branch.
The bug itself was caused by the new smart rendering mechanism introduced in v6.2.0.
It caches previously calculated positions of tasks in order to minimize calculations. In certain circumstances when a gantt instance has been initialized multiple times, it didn't invalidate that cache when it was necessary. Because of that, tasks were displayed at the same positions as they had before filtering was applied (thus the blank row where the first task has been).
In short, the issue will be fixed in the next bugfix update - v6.2.6.
If everything goes as planned, it will be released tomorrow (2019-09-19)
Try gantt.render() after gantt.refreshData() in your code:
useEffect(() => {
const onBeforeTaskDisplay = gantt.attachEvent("onBeforeTaskDisplay", function (id, task) {
console.log("filters", task.text, filter)
if (filter && task.duration > 4) {
return false;
}
return true;
});
gantt.refreshData();
gantt.render();
// This should have been here
return () => {
gantt.detachEvent(onBeforeTaskDisplay);
}
}, [filter])

AG-Grid (Enterprise) Column Menu Listener

Is there a way to set a listener to column-menu, so that an event is fired when I open and close the menu?
Feature description: https://www.ag-grid.com/javascript-grid-column-menu/
I already searched in the official documentation, but didn't find an answer.
Background is:
I want to store the table state with displayed cols, sorting, position of cols, filter etc. in a database. Of course I could use the listeners like onFilterChanged, onDisplayedColumnsChanged or onSortChanged.
Problem is, that it will be fired every time when something changes and so there are produced a lot of unwanted api-calls.
Thats why I want to perform one call when the column-menu is closed.
Update
As Viqas said in his Answer, there is no official way to do it. I
tried to avoid the solution with postProcessPopup and tried to find a cleaner
solution for my problem - to store the table state.
For a workaround with a callback when ColumnMenu is closed Viqas Answer is more appropriate.
Notice that this is no workaround for the callback itself - it is just a (possible) solution to store the table state and perform ONE API Call
I used the ngOnDestory() function of Angular.
ngOnDestory(): void {
const tableState = {
columnState: this.gridOptions.columnApi.getColumnState(),
columnGroupState: this.gridOptions.columnApi.getColumnGroupState(),
sortState: this.gridOptions.api.getSortModel(),
filterState: this.gridOptions.api.getFilterModel(),
displayedColumns: this.gridOptions.columnApi.getAllDisplayedColumns()
};
// submit it to API
}
You're right, there's no official way to do it. A workaround could be to detect when the menu is closed yourself. Ag-grid does provide you the postProcessPopup callback (see here) which provides the parameter of type PostProcessPopupParams; this contains the column menu popup element that is displayed, so you could check when the menu is no longer visible.
Create a variable to store the columnMenu element in:
columnMenu: any = null;
Store the columnMenu in this variable using the ag-grid event postProcessPopup:
<ag-grid-angular [postProcessPopup]="postProcessPopup"></ag-grid-angular>
this.postProcessPopup = function(params) {
this.columnMenu = params.ePopup;
}.bind(this);
Then create a listener to detect when the column menu is no longer visible in the dom:
this.renderer.listen('window', 'click',(e:Event)=>{
console.log(this.columnMenu)
const columnMenuIsInDom = document.body.contains(this.columnMenu);
if (!columnMenuIsInDom && this.columnMenu != null)
{
this.columnMenu = null;
}
});
This is slightly hacky and a workaround, but I can't think of a better way at the moment.
Take a look at this Plunker for illustration.

Kendo Treeview Expanding big tree issue

I have created a tree control using kendo TreeView.it has more than 10,000 nodes and i have used loadOnDemand false when creating Tree.
I am providing a feature to expand the tree by its level, for this i have created a method which takes the parameter "level" as number and expand it accordingly and user can enter 15 (max level) into the method, it works fine with 500 to 600 nodes for all the levels but when tree has more than 5000 nodes than if user is trying to expand above the 2nd level nodes then browser hangs and shows not responding error.
Method which i have created to expand the tree is :-
function ExapandByLevel(level, currentLevel) {
if (!currentLevel) {
currentLevel = 0;
}
if (level != currentLevel) {
var collapsedItems = $("#treeView").find(".k-plus:visible");
if (collapsedItems.length > 0) {
setTimeout(function () {
currentLevel++;
var $tree = $("#treeView");
var treeView = $tree.data("kendoTreeView");
var collapsedItemsLength = collapsedItems.length;
for (var i = 0; i < collapsedItemsLength; i++) {
treeView.expand($(collapsedItems[i]).closest(".k-item"));
}
ExapandByLevel(level, currentLevel);
}, 100);
}
else {
//console.timeEnd("ExapandByLevel");
hideLoading();
}
}
if (level == currentLevel) {
hideLoading();
}
}
call above given method like this:-
ExapandByLevel(15);
here 15 is level to expand in tree.
when tree has more than 5000 nodes than if user is trying to expand above the 2nd level nodes then browser hangs and shows not responding error.
please suggest any way to do this,what i want is expand the tree which can contains more than 5000 nodes.
I had a similar problem with kendo TreeView, when I wanted to load a tree with 30,000 nodes. The browser would freeze for a long time to load this number of nodes even when loadOnDemand was set to true.
So we decided to implement the server-side functionality for expanding nodes, and that's what you should do. You need to have 2 changes in your existing code.
Change your tree use server side Expand method.
When you call expand, you should make sure the node is expanded.
These two steps will be explained below. The thing you should know is, this way your browser doesn't hang at all, but it may take some time to complete the operation, because there will be so many webservice calls to the server.
Change your tree to use server side Expand method:
Please see Kendo UI's demos for Binding to Remote Data in this
link. Note that loadOnDemand should be set to true. In addition the server side Expand web service should be implemented too.
When you call expand, you should make sure the node is expanded:
In order to do this, there should be an event like Expanded defined in Kendo UI TreeView, but unfortunately there is none, except Expanding event. Using setTimeout in this case is not reliable, because the network is not reliable. So we ended up using a while statement to check that the node's children are created or not. There might be a better solution for this, however this satisfies our current requirement. Here's the change you should make when expanding nodes:
if (collapsedItems.length > 0) {
currentLevel++;
var $tree = $("#treeView");
var treeView = $tree.data("kendoTreeView");
var collapsedItemsLength = collapsedItems.length;
for (var i = 0; i < collapsedItemsLength; i++) {
var node = $(collapsedItems[i]).closest(".k-item")
if (!node.hasChildren)
continue; // do not expand if the node does not have children
treeView.expand(node);
// wait until the node is expanded
while (!node.Children || node.Children.length == 0);
}
ExapandByLevel(level, currentLevel);
}
You can also do the expand calls in a parallel way in order to decrease the loading time, but then you should change the way you check if all the nodes are expanded or not. I just wrote a sample code here that should work fine.
Hope this helps.
The solution to your problem is pretty simple: Update the version of Kendo UI that you are using since they have optimized (a loooooooooot) the code for HierarchicalDataSource and for TreeView.
Check this: http://jsfiddle.net/OnaBai/GHdwR/135/
This is your code where I've change the version of kendoui.all.min.js to v2014.1.318. I didn't even changed the CSS (despite you should). You will see that opening those 5000 nodes is pretty fast.
Nevertheless, if you go to 10000 elements you will very likely consider it slow but sorry for challenging you: do you really think that 10000 nodes tree is User Friendly? Is a Tree the correct way of presenting such a huge amount of data?

Sencha Touch Ext.navigation.View pop to root

I have an Ext.navigation.View in which I have pushed a few views. Certain user interactions require that I go directly back to the top level of the navigation view -- the equivalent of popToRootViewControllerAnimated: on a UINavigationController in iOS.
I have tried various things like:
while(navigationView.getItems().getCount() > 1)
navigationView.pop();
and
while(navigationView.canPop())
navigationView.pop();
Neither work. The first example seems to put me into an infinite loop which isn't too surprising. The second example only seems to pop one view off.
So the question: What is the proper way to pop to the root view in an Ext.navigation.View in Sencha Touch (version 2 developer preview)?
There has been a number of interim methods for achieving this.
The one I was using was to pop a number higher than the number of levels you would ever have e.g.
navigationView.pop(10);
and that worked fine, but I was never happy with that, but I see they have now introduced a reset method. Which you can call thus...
navigationView.reset();
Internally within the Sencha source code (see below) you can see that it does a similar job to what #Mithralas said, but just easier to write.
// From the Sencha source code
this.pop(this.getInnerItems().length);
popToRoot: function() {
this.pop(this.getItems().length - 1);
}
Ensure to use the NavigationView.reset() method. To make it clear, if your main navigation view is Main, you'd do something like this in the controller:
this.getMain().reset();
The solution turned out to be extending the navigation view with the following:
popToRoot: function(destroy)
{
var navBar = this.getNavigationBar(),
stackLn = this.stack.length,
stackRm;
//just return if we're at root
if(stackLn <= 1) return;
//just return if we're already animating
if(navBar && navBar.animating) return;
//splice the stack to get rid of items between top and root
stackRm = this.stack.splice(1, stackLn-2);
//remove views that were removed from the stack if required
if(destroy) {
stackRm.forEach(function(val, idx, arr) {
this.remove(val, true);
});
}
//clear out back button stack
navBar.backButtonStack = [];
//now we can do a normal pop
this.pop();
}

How do I prevent the Javascript InfoVis SpaceTree `ST.select()` method from collapsing nodes?

I'm using the JavaScript InfoVis Toolkit and in particular the SpaceTree visualisation.
I need to expand all of the tree and then show a path from a particular leaf node back to the root.
I've got the tree to expand just fine but it's the selection of a leaf node and highlighting the path back to the root that's causing me some problems.
I'm using the ST.select(node, onComplete) function to select the leaf node I'm interested in and indeed the path back to the root (lines and nodes) are highlighted.
To do this I implemented the onBeforePlotNode and onBeforePlotLine ST.Controller methods to allow me to highlight the nodes back to the root and their plotlines:
onBeforePlotNode: function(node){
//add some color to the nodes in the path between the
//root node and the selected node.
if (node.selected) {
node.data.$color = "#dddddd";
} else {
delete node.data.$color;
}
},
onBeforePlotLine: function(adj){
if (adj.nodeFrom.selected && adj.nodeTo.selected) {
adj.data.$color = "#33CC33";
adj.data.$lineWidth = 5;
} else {
delete adj.data.$color;
delete adj.data.$lineWidth;
}
}
The problem is that when I call ST.select() to highlight the leaf node all child nodes beneath this level are collapsed/hidden.
To see this in action I've uploaded a couple of examples:
Full tree expansion - leaf not selected
Leaf selected - path shown, but all children below node N2 missing
You may need to scroll down if your browser window is a bit small.
So my question is, how do I show nodes from a leaf node back to the root node in JavaScript InfoVis without collapsing level 3's children (level 1 being the root)?
If there was a way to find my leafe node and walk the tree back to the root (setting styles on the way) then I'd be happing doing that
OK after digging through all that code, cluttering it with console.log() calls and breakpoints, I found it.
It has to do with the inital onClick call, the fact that the graph as an update loop that's running in the background and the fact that everything besides onClick seems to ignore the busy state of the graph.
What happens
onClick gets called and triggers a chain of events, part of them is asynchronous
select is being called which is more or less synchronous and does its work
onClick finally gets done and one of it's side effects is that it re-expands the graph
select has set clickedNode and now onClick uses the newly set value of it and screws up
Solution
We need to redesign select so it respects the busy state of the graph:
select: function(id, onComplete) {
var that = this;
if (this.busy) {
window.setTimeout(function() {
that.select(id, onComplete);
}, 1);
return;
}
// original select code follows here (remove the old var that = this; assignment)
That's all, we simply check for the busy state and delay select until it's false.
This should also be applied to all other function besides onClick that are called from the outside, the library designer here did a bad job of indicating what has side effects and what has not though.
Did you try setting "constrained: false" in ST's properties? That solved it for me.
http://thejit.org/static/v20/Docs/files/Visualizations/Spacetree-js.html

Categories