我最近了解了一个优秀的JS库cola.js。它可以进行部队布局和支援小组。在此了解更多信息:
Cola.js
我创建了一个简单的演示来展示一个带有开放组功能的强制布局。但我对这种开放的行为感到困惑。
我认为当打开一个组时,新的布局应该是基于最后一个布局的微小调整。但现在它重新传输所有节点。为什么?
我从这个环节学到了一些理念:
Offical Demo : Online Graph Exploration
这看起来很复杂。新节点动态添加到图形的坐标应设置为打开组的坐标。不幸的是,它也不能解决我的问题。
以下是我的演示:
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);
}