打开组后的新布局不是基于 cola.js 的最后一个布局

New layout after opening a group not base on the last layout with cola.js

最近了解到一个优秀的JS库cola.js。它可以进行部队布局和支持小组。在此处了解更多信息:Cola.js

我创建了一个简单的演示来展示具有打开组功能的强制布局。但是我对开放行为感到困惑。

我觉得开群的时候,新的布局应该是在上一个布局的基础上稍微调整一下。但现在它重新布局所有节点。为什么?

我从这个link : 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);
}
<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/>

我的代码笔演示也运行良好:Cola open group demo

有没有可能是根据上次打开的状态重新布局?不是新的完整布局?

问这个问题已经一个月了,可惜很少有人关注

最近,我一直在学习如何使用JavaScript 库来实现合理布局支持的组功能。我找到了一些图书馆,比如 D3, WebCola, cytoscape.js, yfile

这里有一个link来比较D3 vs cytoscape。基于它,我最终选择了cytoscape,因为它提供了更强大的API和合理的算法来布局复杂的关系。所以我很抱歉 cola.js.

cytoscape 有很多扩展来做更好的布局工作。单击以下 link 以查看更多 cytoscape.js-cose-bilkent, cytoscape.js-expand-collapse, cytoscape.js-cola