Avoid overlapping compound nodes - cytoscape.js

We use some specific layouts to visualize istio's service mesh, and we "Compound nodes" to group nodes of the same service (but different versions).
The problem here is that sometimes one or all of the layouts draws a big compound node that looks like is grouping more nodes than needed.
Look at the next screenshot, it looks like the "reviews" compound node has a lot of nodes, the truth is that the review box only has the top left "v2" and "v1" and the bottom right "v3".
I was thinking of a way to workaround this would be:
Remove the contents of compound nodes (giving them enough space for later)
Layout the remaining nodes
Manually fill the compound nodes (e.g. using a manual vertical layout)
I was wondering whether is there a more simple approach, if not I was thinking on wrapping this idea on a Layout which receives the "real layout" upon creation e.g.
const coseLayout = cy.layout({name: 'cose', ...});
const compoundNodeFixer = cy.layout({name: 'compoundnodefixer', real_layout: coseLayout});
compoundNodeFixer.run();
Update: What we did in the end was to implement something like mentioned above as a new layout. It's not perfect but it works for our use case. The code is public.

Achieving this is quite hard if you need to use a specific layout, as you may have seen a dozen other unanswered questions about node/label overlapping here on StackOverflow.
However there is still the cytoscape-cola.js layout, it provides the needed spacing and has the cool effect of pushing other nodes away. If you can use it, it may just be the right choice for you:
var cy;
var elements = [{
data: {
id: 'M',
parent: 'B'
}
},
{
data: {
id: 'B'
}
},
{
data: {
id: 'H',
parent: 'B'
}
},
{
data: {
id: 'F'
}
},
{
data: {
id: 'H2'
}
},
{
data: {
id: 'T'
}
},
{
data: {
id: 'A'
}
},
{
data: {
id: 'e2',
source: 'M',
target: 'H'
}
},
{
data: {
id: 'e3',
source: 'M',
target: 'F'
}
},
{
data: {
id: 'e4',
source: 'F',
target: 'T'
}
}
]
$(function() { // on dom ready
cy = cytoscape({
container: $('#cy'),
elements: elements,
layout: {
name: 'cola',
infinite: true,
fit: false
}
});
});
#cy {
position: absolute;
left: 1em;
top: 1em;
bottom: 1em;
right: 17em;
border: 1px solid #ccc;
}
html {
font-size: 14px;
font-family: Arial, Helvetica, sans-serif;
}
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.2.3/jquery.min.js"></script>
<script src="https://unpkg.com/cytoscape#3.2.18/dist/cytoscape.js"></script>
<script src="https://unpkg.com/webcola/WebCola/cola.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cytoscape-cola#2.2.3/cytoscape-cola.min.js"></script>
</head>
<body>
<div id="cy"></div>
</body>
</html>
Unfortunately, it is not a core layout but an extension layout, you may or may not experience performance enhancement when using them.

Related

Tabulator header color

Tabulator does not accept header color change
Through the css below, I'm trying to change the header color of all my columns, but I'm not getting this adjustment
tried to do it this way but it's not going.
I use vue3 + vite + tabulator 5.4
<style scoped>
.tabulator {
font-size: 12px;
width: 100%;
}
.tabulator .tabulator-header,
.tabulator-col .headerBackgroundColor {
background-color: #acacac;
}
</style>
this is my tabulator
tabulator.value = new Tabulator('#tabulator', {
data: dataRelatorio.value,
layout: 'fitColumns',
autoResize: true,
pagination: 'local',
paginationSize: 20,
rowHeight: 25,
paginationSizeSelector: [10, 20, 30, 50, 100, 200, 500, 1000],
movableColumns: true,
paginationCounter: 'rows',
responsiveLayout: 'hide',
placeholder: 'No record found',
locale: 'pt-BR',
langs: {
'pt-BR': {
pagination: {
page_title: 'Show page',
first: 'First',
first_title: 'First page',
last: 'Next',
last_title: 'Next page',
prev: 'Previous',
prev_title: 'Previous page',
next: 'Next',
next_title: 'Next Page',
all: 'All',
},
},
},
columns: columns,
rowFormatter: function (row) {
//console.log(row.getData());
if (row.getData().in_delayed === 'delayed') {
const children = row.getElement().childNodes;
children.forEach((child) => {
child.style.backgroundColor = '#FFFACD';
});
}
},
});
This should work:
<style>
.tabulator .tabulator-header .tabulator-col {
background-color: #acacac;
}
</style>
Notes:
I didn't use the scoped directive on <style> tag.
you can have more than one <style> tag in a component, some scoped and some regular. The regular ones are "normal" CSS, they apply to the entire DOM.
you can also apply the above styles inside a scoped style tag. The selector would most likely be .tabulator :deep(.tabulator-header .tabulator-col), but I can't know for sure until you provide a runnable minimal example, so I could inspect it.

How to delete the parent node without deleting the children nodes

This is the code sample that I am using in reactjs.
const node = graph.$(`#${selectedNode.id()}`);
graph.remove(node);
selectedNode.id is the id of the parent node but it deletes all the children nodes inside this parent node.
How can I delete the parent node only and not its descendants?
This question is similar to this on here Remove/hide compound node without removing/hiding descendants but I will appreciate it if some code samples are provided because in the doc here http://js.cytoscape.org/#collection/graph-manipulation/eles.move we have a shallow code example of edges but I am interested in nodes.
Thanks
You can delete a parent node by first moving its children to the parent's parent (if it exist, otherwise you should assig a null value) and then removing the parent node. Select a parent node and click on the delete button in the below example.
var cy = window.cy = cytoscape({
container: document.getElementById('cy'),
layout: {name: 'grid', rows: 2},
style: [{
selector: 'node',
css: {
'label': 'data(id)'
}
}
],
elements: {
nodes: [{
data: {
id: 'n0',
parent: 'n1'
}
},
{
data: {
id: 'n1',
parent: 'n2'
}
},
{
data: {
id: 'n2'
}
},
{
data: {
id: 'n3'
}
}
],
edges: [
{
data: {
id: 'n2n3',
source: 'n2',
target: 'n3',
weight: 7
}
}
]
}
});
document.getElementById("deleteButton").addEventListener("click", function() {
let selected = cy.nodes(':selected')[0];
selected.children().move({parent : (selected.parent().id() ? selected.parent().id() : null)});
selected.remove();
});
body {
font: 14px helvetica neue, helvetica, arial, sans-serif;
}
#button {
z-index = 1000;
}
#cy {
height: 95%;
width: 95%;
left: 0;
top: 50;
z-index = 900;
position: absolute;
}
<html>
<head>
<meta charset=utf-8 />
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
<script src="https://unpkg.com/cytoscape#3.10.0/dist/cytoscape.min.js">
</script>
</head>
<body>
<button id="deleteButton" type="button">Delete selected</button>
<div id="cy"></div>
</body>
</html>

How to set QTip to always show tooltips in Cytoscape.js

I'm looking for a way of getting QTip to concurrently display tooltips for each node in a Cytoscape.js graph, such that they are always displayed and anchored to the nodes in the graph without the user having to click or mouseover the node.
I got close with the code below:
$(document).ready(function(){
cy.nodes().qtip({
content: function(){ return 'Station: ' + this.id() +
'</br> Next Train: ' + this.data('nextTrain') +
'</br> Connections: ' + this.degree();
},
hide: false,
show: {
when: false,
ready: true
}
})
})
The above code displays tooltips on $(document).ready, but they are all located at one node in the Cytoscape graph and they disappear when I zoom in or pan at all.
The goal is to have tooltips anchored to each node in my graph such that when I zoom in and pan around they remain fixed to that node. I'm not sure if there is an easier way to do this just using Cytoscape (i.e., multi-feature labelling).
I'm using Qtip2, jQuery-2.0.3 and the most recent release of cytoscape.js
Any help is much appreciated.
EDIT: If you want to create these elements automatically, use a function and a loop to iterate over cy.nodes():
var makeTippy = function (nodeTemp, node) {
return tippy(node.popperRef(), {
html: (function () {
let div = document.createElement('div');
// do things in div
return div;
})(),
trigger: 'manual',
arrow: true,
placement: 'right',
hideOnClick: false,
multiple: true,
sticky: true
}).tooltips[0];
};
var nodes = cy.nodes();
for (var i = 0; i < nodes.length; i++) {
var tippy = makeTippy(nodes[i]);
tippy.show();
}
If you want a sticky qTip, I would instead recommend the cytoscape extension for popper.js and specificly the tippy version (sticky divs):
document.addEventListener('DOMContentLoaded', function() {
var cy = window.cy = cytoscape({
container: document.getElementById('cy'),
style: [{
selector: 'node',
style: {
'content': 'data(id)'
}
},
{
selector: 'edge',
style: {
'curve-style': 'bezier',
'target-arrow-shape': 'triangle'
}
}
],
elements: {
nodes: [{
data: {
id: 'a'
}
},
{
data: {
id: 'b'
}
}
],
edges: [{
data: {
source: 'a',
target: 'b'
}
}]
},
layout: {
name: 'grid'
}
});
var a = cy.getElementById('a');
var b = cy.getElementById('b');
var makeTippy = function(node, text) {
return tippy(node.popperRef(), {
html: (function() {
var div = document.createElement('div');
div.innerHTML = text;
return div;
})(),
trigger: 'manual',
arrow: true,
placement: 'bottom',
hideOnClick: false,
multiple: true,
sticky: true
}).tooltips[0];
};
var tippyA = makeTippy(a, 'foo');
tippyA.show();
var tippyB = makeTippy(b, 'bar');
tippyB.show();
});
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: 1;
}
h1 {
opacity: 0.5;
font-size: 1em;
font-weight: bold;
}
/* makes sticky faster; disable if you want animated tippies */
.tippy-popper {
transition: none !important;
}
<!DOCTYPE>
<html>
<head>
<title>Tippy > qTip</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/popper.js"></script>
<script src="cytoscape-popper.js"></script>
<script src="https://unpkg.com/tippy.js#2.0.9/dist/tippy.all.js"></script>
<link rel="stylesheet" href="https://unpkg.com/tippy.js#2.0.9/dist/tippy.css" />
<script src="https://cdn.jsdelivr.net/npm/cytoscape-popper#1.0.2/cytoscape-popper.js"></script>
</head>
<body>
<h1>cytoscape-popper tippy demo</h1>
<div id="cy"></div>
</body>
</html>
I think popper is just easier to handle when having the divs 'stick around'

Rally 2.0 JavaScript API - Creating HTML tables and setting values dynamically

I'm using Custom HTML Rally Grid to develop a table that must return some statistics. My JavaScript is being called at the head of my HTML and i'm creating a table in the body tag. So, my JavaScript is setting values in the table using the fields ids. The problem is: the table is being loaded but when the Rally.launchApp method runs, the table disappears. Curiously, if i check the font-code, the table still there.
<!DOCTYPE html>
<html>
<head>
<title>Grid With Freeform Data Example</title>
<script type="text/javascript" src="/apps/2.0rc1/sdk.js"></script>
<script type="text/javascript">
Rally.onReady(function() {
Ext.define('CustomApp', {
extend: 'Rally.app.App',
componentCls: 'app',
launch: function() {
var firstMetricResult;
var secondMetricResult;
var firstMetricName = "% of user stories assigned story points";
var secondMetricName = "Average story points per user story ";
var currentProjectName = Rally.environment.getContext().getProject().Name;
var currentProjectNameID = document.getElementById("currentProjectNameID");
currentProjectNameID.value = currentProjectName;
var benchmark = 20;
var storiesQuery = Ext.create('Rally.data.WsapiDataStore', {
model: 'UserStory',
fetch: ['PlanEstimate', 'LastUpdateDate'],
filters: [
{property: 'ScheduleState',
operator: '=',
value: 'Accepted'},
{property: 'DirectChildrenCount',
operator: '=',
value: '0'},
{property: 'AcceptedDate',
operator: '<',
value: 'LastMonth'},
{property: "Iteration.Name",
operator: "!contains",
value: "hardening"},
{property: "Iteration.Name",
operator: "!contains",
value: "regression"},
{property: "Iteration.Name",
operator: "!contains",
value: "stabilization"}
]
});
storiesQuery.load({
callback: function(records, operation) {
if(operation.wasSuccessful()) {
var estimatedStoriesCount = 0;
Ext.Array.each(records, function(record){
if (record.get('PlanEstimate') != null){
estimatedStoriesCount++;
}
});
var storiesCount = records.length;
firstMetricResult = (estimatedStoriesCount*100)/storiesCount;
alert(firstMetricResult);
}
}
});
var estimatedStoriesQuery = Ext.create('Rally.data.WsapiDataStore', {
model: 'UserStory',
fetch: ['PlanEstimate', 'LastUpdateDate'],
filters: [
{property: 'PlanEstimate',
operator: '!=',
value: 'null'},
{property: 'ScheduleState',
operator: '=',
value: 'Accepted'},
{property: 'DirectChildrenCount',
operator: '=',
value: '0'},
{property: 'AcceptedDate',
operator: '<',
value: 'LastMonth'}
]
});
estimatedStoriesQuery.load({
callback: function(records, operation) {
if(operation.wasSuccessful()) {
var astoriesCount = records.length;
var storiesPointsSum = 0;
Ext.Array.each(records, function(record){
storiesPointsSum += record.get('PlanEstimate');
});
secondMetricResult = storiesPointsSum/astoriesCount;
alert(secondMetricResult);
}
}
});
}
});
Rally.launchApp('CustomApp', {
name: 'Grid With Freeform Data Example'
});
});
</script>
<style type="text/css">
table.gridtable {
font-family: verdana,arial,sans-serif;
font-size:11px;
color:#333333;
border-width: 1px;
border-color: #666666;
border-collapse: collapse;
}
table.gridtable th {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background-color: #dedede;
}
table.gridtable td {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background-color: #ffffff;
}
</style>
</head>
<body>
<table border=1 class='gridtable' id="tab1">
<tr>
<th> Team </th>
<td><b>All prior periods</b></td>
<td><b>All prior periods</b></td>
</tr>
<tr>
<td id="currentProjectNameID">
</td>
</tr></table>
</body>
</html>
I decided to use this simple HTML table because i want to have the power to edit the CSS.
Thanks.
The above answer is correct. Generally when working with apps (and ExtJS) most content is created through javascript rather than literally declared dom.
Check out this guide for a little more info on working with content in apps: https://developer.help.rallydev.com/apps/2.0rc1/doc/#!/guide/add_content
Something like this should work though if you're still sold on the manual dom manipulation:
launch: function() {
this.add({
xtype: 'component',
html: [
'<table border=1 class="gridtable" id="tab1">',
'<tr>',
'<th> Team </th>',
'<td><b>All prior periods</b></td>',
'<td><b>All prior periods</b></td>',
'</tr>',
'<tr>',
'<td id="currentProjectNameID"></td>',
'</tr>',
'</table>'
].join('')
});
},
afterRender: function() {
this.callParent(arguments);
//the rest of your code that used to be in launch here
}
Then you should be able to look up the elements by id and manipulate them as you'd like.
You would still be able to modify the CSS if you used an Ext grid or a rallygrid - besides using normal CSS selectors to style the grid, you also have access to certain properties when creating the grid - the renderer function for a column takes meta as its second parameter, which has an attribute called tdCls which you can use to style cells in that column. For example:
.green > .x-grid-cell-inner {
border : 1px solid #afd3b6;
background : #c6efce;
background : -moz-linear-gradient(top, #c6efce 0%, #afd3b6 100%);
background : -webkit-gradient(linear, left top, left bottom, color-stop(0%,#c6efce), color-stop(100%,#afd3b6));
background : -webkit-linear-gradient(top, #c6efce 0%,#afd3b6 100%);
background : -o-linear-gradient(top, #c6efce 0%,#afd3b6 100%);
background : -ms-linear-gradient(top, #c6efce 0%,#afd3b6 100%);
background : linear-gradient(to bottom, #c6efce 0%,#afd3b6 100%);
filter : progid:DXImageTransform.Microsoft.gradient( startColorstr='#c6efce', endColorstr='#afd3b6',GradientType=0 );
}
{
text : 'State',
dataIndex : 'c_WINListState',
editor : 'stateeditor',
renderer : function(value, meta, record) {
if (value === 'At Risk') {
meta.tdCls = 'yellow';
} else if (value === 'Off Track') {
meta.tdCls = 'red';
} else if (value === 'On Track') {
meta.tdCls = 'green';
} else if (value === 'Complete') {
meta.tdCls = 'grey';
}
return value;
}
},
I would recommend using the built in functionality.
That being said, I think this is the reason for the built in functionality - if you look at the SDK, the launch app function states:
Create and execute the specified app. Ensures all required components have been loaded and the DOM is ready before app code begins executing. As soon as all required dependencies have been loaded the launch method will be called.

SenchaTouch onItemDisclosure 2 icons

I have a list and I want have two icons per line using onItemDisclosure. How can I do that?
I don't know how to implement onItemDisclousre() on two icons but probably this will help you.
In the following example i have put an image on every itemlist and functionality is provided on itemtap event. This will serve the purpose of doing multiple tasks with single itemlist.
//demo.js
Ext.define("Stackoverflow.view.demo", {
extend: "Ext.Container",
requires:"Ext.dataview.List",
alias: "widget.demo",
config: {
layout: {
type: 'fit'
},
items: [
{
xtype: "list",
store: "store",
itemId:"samplelist",
loadingText: "Loading Notes...",
emptyText: "<div class=\"notes-list-empty-text\">No notes found.</div>",
onItemDisclosure: true,
itemTpl:"<div class='x-button related-btn' btnType='related' style='border: none; background: url(\"a.png\") no-repeat;'></div>"+
"<div class=\"list-item-title\">{title}</div>"
grouped: true
}
],
listeners:
[
{
delegate: "#samplelist",
event: "disclose",
fn: "onDiscloseTap"
}
]
},
onDiscloseTap: function (list, record, target, index, evt, options) {
this.fireEvent('ondisclosuretap', this, record);
}
});
// Democontrol.js
Ext.define("Stackoverflow.controller.Democontrol", {
extend: "Ext.app.Controller",
config: {
refs: {
// We're going to lookup our views by xtype.
Demo: "demo",
Demo1: "demo list",
},
control: {
Demo: {
ondisclosuretap: "Disclosure",
},
Demo1: {
itemtap:"imagetap"
}
}
},
Disclosure: function (list, record,target,index,e,obj) {
Ext.Msg.alert('','Disclosure Tap');
},
imagetap: function (dataview,index,list,record, tar, obj) {
tappedItem = tar.getTarget('div.x-button');
btntype = tappedItem.getAttribute('btnType');
if(btntype == 'related')
{
Ext.Msg.alert('','Image/Icon Tap');
}
},
// Base Class functions.
launch: function () {
this.callParent(arguments);
},
init: function () {
this.callParent(arguments);
}
});
//app.css
.related-btn
{
width: 100px;
height: 100px;
position: absolute;
bottom: 0.85em;
right: 2.50em;
-webkit-box-shadow: none;
}
Hope this will help you.
bye.
You can do this by manually adding a disclosure icon inside of itemTpl on your list items. Add this inside of your view:
{
xtype: 'list',
onItemDisclosure: true,
cls: 'my-list-cls',
itemTpl: [
'<div class="x-list x-list-disclosure check-mark" style="right: 48px"></div>'
]
}
Notice that the div inside of itemTpl has the CSS class "x-list x-list-disclosure check-mark". I set style="right: 48px" because I want this icon to appear on the left side of the regular disclosure icon (the one with the right arrow) and this rule leaves enough room on the right to show the arrow icon.
Then, in your app.scss, add:
.my-list-cls {
.x-list.check-mark.x-list-disclosure:before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: '3';
font-family: 'Pictos';
color: #fff;
text-align: center;
text-shadow: 0 0 0;
}
}
This controls the styling for your new disclosure icon.
By setting content: '3';, you are changing the icon from the default right arrow to a checkmark. (See all of the available icons here: Pictos fonts).
The result:
It is possible but not easy. In short you have to extend class Ext.dataview.List and/or Ext.dataview.element.List.