I recently learned about a excellent JS library cola.js . It can do a force layout and support groups. Learn more here : Cola.js
I create a simple demo to show a force layout with opening group feature. But I was confused with the openning behavior.
I think when open a group, the new layout should be a minor adjustments base on the last layout. But now it relayout all nodes. Why ?
I learned some ideals from this link : Offical Demo : Online Graph Exploration, which looks very complicated. The coordinates of new nodes dynamic adding to graph should be set as the coordinate of opening group. Unfortunately, it also can't resolve my issue.
The following is my demo :
var w = 480, h = 420, cola;
var data = {
"nodes": [
{"name": "Top","width": 60,"height": 60},
{"name": "A","width": 60,"height": 60},
{"name": "B","width": 60,"height": 60},
{"name": "C","width": 60,"height": 60},
{"name": "D","width": 60,"height": 60},
{"name": "E","width": 60,"height": 60},
{"name": "F","width": 60,"height": 60},
{"name": "G","width": 60,"height": 60},
{"name": "H","width": 60,"height": 60},
{"name": "I","width": 60,"height": 60}
],
"links": [
{"source": 0,"target": 6},
{"source": 0,"target": 4},
{"source": 0,"target": 3},
{"source": 0,"target": 7},
{"source": 0,"target": 8},
{"source": 6,"target": 0},
{"source": 6,"target": 7},
{"source": 4,"target": 0},
{"source": 4,"target": 3},
{"source": 3,"target": 0},
{"source": 3,"target": 4},
{"source": 7,"target": 0},
{"source": 7,"target": 6},
{"source": 7,"target": 4},
{"source": 8,"target": 0},
{"source": 8,"target": 7}
],
"groups": [
{"leaves": [0,1,2],"groups": [1],"name": "Product"},
{"leaves": [7],"name": "Businness"},
{"leaves": [3,4,6,8,9],"name": "Tech"}
]
};
cola = cola.d3adaptor()
.linkDistance(150)
.avoidOverlaps(true)
.handleDisconnected(true)
.size([w, h]);
svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.on("dblclick.zoom", null);
svg.append('rect')
.attr("width", w)
.attr("height", h)
.style("fill", "none")
.style("pointer-events", "all");
svg = svg.append('g');
update(data);
cola.on("tick", function () {
svg.selectAll(".link")
.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
svg.selectAll(".nodeimage").attr("x", function(d){ return d.x - 25 / 2 }).attr("y", function(d){ return d.y - 25 / 2 });
svg.selectAll(".group")
.attr("x", function (d) { return d.bounds.x; })
.attr("y", function (d) { return d.bounds.y; })
.attr("width", function (d) { return d.bounds.width(); })
.attr("height", function (d) { return d.bounds.height(); });
svg.selectAll(".label").attr("x", function (d) {
var w = this.getBBox().width;
return d.x - w/2;
})
.attr("y", function (d) {
var h = this.getBBox().height;
return d.y + 25;
});
svg.selectAll('.groupclosebutton')
.attr("x", function (d) { return d.bounds.x + d.bounds.width() - 20; })
.attr("y", function (d) { return d.bounds.y + 2; });
svg.selectAll('.groupname')
.attr("x", function (d) { return d.bounds.x + 5; })
.attr("y", function (d) { return d.bounds.y + 15; });
});
function update(data){
//data.groups.forEach(function (g) { g.padding = 15; });
cola.nodes(data.nodes).links(data.links).groups(data.groups).start();
var color = ['#d3d4e5', '#f7e0c8', '#dee8f2', '#cbe5c4', '#ededeb'];
var group = svg.selectAll(".group").data(data.groups, function(d) { return d.name;});
group.enter().append("rect")//, ":last-child"
.attr("rx", 8).attr("ry", 8)
.attr("class", "group")
.style("fill", function (d, i) { return color[i%5]; })
.call(cola.drag);
group.exit().remove();
var groupName = svg.selectAll(".groupname").data(data.groups, function(d) { return d.name;});
groupName.enter().append("text")
.attr("class", "groupname")
.attr("width", "40px")
.attr("height", "13px")
.text(function (d) { return d.name; });
groupName.exit().remove();
var link = svg.selectAll(".link").data(data.links, function(d) { return d.source.name+'-'+d.target.name;});
link.enter().append("line").attr("class", "link").style("stroke", "rgb(168, 168, 168)");
link.exit().remove();
var nodes = svg.selectAll('.nodeimage').data(data.nodes, function(d) { return d.name;});
nodes.enter().append('svg:image')
.attr("class", "nodeimage")
.call(cola.drag)
.attr("xlink:href", function(d){
var img = "http://icons.iconarchive.com/icons/hopstarter/sleek-xp-basic/24/Folder-icon.png";
return img;
})
.attr('temp', function(d){
var self = d3.select(this);
self.attr("width", 25);
self.attr("height", 25);
})
.on("dblclick", function(node, index, selection){
d3.event.preventDefault();
openGroup(node);
});
nodes.exit().transition().attr("width", 0).attr("width", 0).remove();
var label = svg.selectAll(".label").data(data.nodes, function(d) { return d.name;});
label.enter().append("text")
.attr("class", "label")
.attr("width", "40")
.attr("height", 15)
.text(function (d) { return d.name; })
.call(cola.drag);
label.exit().remove();
}
function openGroup(node){
var i,j, flag,maxnodes = 3, groupDeletedIndex = -1;
// Delete the node
for(i = 0; i < this.data.nodes.length; i++){
if(this.data.nodes[i].name == node.name){
this.data.nodes.splice(i, 1);
break;
}
}
// Delete old links linked to the node
for(i = this.data.links.length - 1; i >= 0; i--){
if(this.data.links[i].source.name == node.name
|| this.data.links[i].target.name == node.name){
this.data.links.splice(i, 1);
}
}
// Delete the relationship of the node
flag = false;
for(i = 0; i < this.data.groups.length; i++){
for(j = 0; j < this.data.groups[i].leaves.length; j++){
if(this.data.groups[i].leaves[j].name == node.name){
this.data.groups[i].leaves.splice(j, 1);
flag = true;
groupDeletedIndex = i;
break;
}
}
if(flag)break;
}
// Create new nodes belong to openning group
for(var i = 0; i < maxnodes; i++){
var obj = {
name : node.name+'child'+i ,
width : 100,
height : 100,
x:node.x,
y:node.y,
px:node.px,
py:node.py
};
this.data.nodes.push(obj);
if(i%3!=0){
this.data.links.push({// Create demo links
source : this.data.nodes.length-1,
target : Math.floor(Math.random(this.data.nodes.length-1))
});
}
}
// Create a group to contain the new nodes and push to groups
this.data.groups.push({
leaves : [],
name : node.name,
bounds : {x:node.x, y:node.y, X:node.x+100, Y:node.y+100},
padding : 15
});
var begin = this.data.nodes.length - maxnodes;
for(var i = 0; i < maxnodes; i++){
this.data.groups[this.data.groups.length-1].leaves.push(begin+i);
}
if(groupDeletedIndex > -1){
if(!this.data.groups[groupDeletedIndex].groups){
this.data.groups[groupDeletedIndex].groups = [];
}
this.data.groups[groupDeletedIndex].groups.push(this.data.groups.length-1);
}
update(this.data);
}
<script src="https://marvl.infotech.monash.edu/webcola/cola.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<body/>
My codepen demo also works well : Cola open group demo
Is it possible that the relayout base on the last opening state ? Not a newly full relayout?
It's been a month since I asked this question, but unfortunately few people pay attention to it.
Recently, I have been learning how to do a reasonable layout supported group feature with JavaScript library. I find some libraries, like D3, WebCola, cytoscape.js, yfile
Here is a link to compare D3 vs cytoscape. Base on it, I choose cytoscape finally, because it provide a more powerful API and reasonable algorithm to layout complex relationship. So I'm so sorry to cola.js.
cytoscape has many extensions to do a better layout work. Click the following links to see more cytoscape.js-cose-bilkent, cytoscape.js-expand-collapse, cytoscape.js-cola
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With