JSPlumb.js 绘制关系拓扑图(力导向布局算法简单实现)

JSPlumb.js 绘制关系拓扑图(力导向布局算法简单实现)

d3 echarts 都可以画关系拓扑图(有环),但是总觉得不是自己想要的,改起来也很麻烦,所以可以用JSPlumb 自己绘制自己的关系拓扑图,拓扑图 = 绘图 + 自动布局算法。jsplumb 可以实现绘自己的结点,而自动布局则可以采用 力导向布局算法。
这里参考的是 https://www.jianshu.com/p/d3c64a39535a

随机生成结点 与结点的位置

	/**
 * A force directed graph layout implementation by liuchang on 2018/05/10.
 */
	const CANVAS_WIDTH = 1000;
	const CANVAS_HEIGHT = 1000;
	let k;
	let mNodeList = [];
	let mEdgeList = [];
	let mDxMap = {};
	let mDyMap = {};
	let mNodeMap = {};
	var json ={} ;

function ForceDirected() {
  //generate nodes and edges
  for (let i = 0; i < 20; i++) {
    mNodeList.push(new Node(i));
  }

  for (let i = 0; i < 20; i++) {
    let edgeCount = Math.random() * 8 + 1;
    for (let j = 0; j < edgeCount; j++) {
      let targetId = Math.floor(Math.random() * 20);
      let edge = new Edge(i, targetId);
      mEdgeList.push(edge);
    }
  }
  if (mNodeList && mEdgeList) {
    k = Math.sqrt(CANVAS_WIDTH * CANVAS_HEIGHT / mNodeList.length);
  }
  for (let i = 0; i < mNodeList.length; i++) {
    let node = mNodeList[i];
    if (node) {
      mNodeMap[node.id] = node;
    }
  }

  //随机生成坐标. Generate coordinates randomly.
  let initialX, initialY, initialSize = 40.0;
  for (let i in mNodeList) {
    initialX = CANVAS_WIDTH * .5;
    initialY = CANVAS_HEIGHT * .5;
    mNodeList[i].x = initialX + initialSize * (Math.random() - .5);
    mNodeList[i].y = initialY + initialSize * (Math.random() - .5);
  }

  //迭代200次. Iterate 200 times.
  for (let i = 0; i < 200; i++) {
    calculateRepulsive();
    calculateTraction();
    updateCoordinates();
  }
  
  json = JSON.stringify(new Result(mNodeList, mEdgeList));
  //console.log();
  
}

function Node(id = null) {
  this.id = id;
  this.x = 22;
  this.y = null;
}
function Edge(source = null, target = null) {
  this.source = source;
  this.target = target;
}

/**
 * 计算两个Node的斥力产生的单位位移。
 * Calculate the displacement generated by the repulsive force between two nodes.*
 */
function calculateRepulsive() {
  let ejectFactor = 6;
  let distX, distY, dist;
  for (let i = 0; i < mNodeList.length; i++) {
    mDxMap[mNodeList[i].id] = 0.0;
    mDyMap[mNodeList[i].id] = 0.0;
    for (let j = 0; j < mNodeList.length; j++) {
      if (i !== j) {
        distX = mNodeList[i].x - mNodeList[j].x;
        distY = mNodeList[i].y - mNodeList[j].y;
        dist = Math.sqrt(distX * distX + distY * distY);
      }
      if (dist < 30) {
        ejectFactor = 5;
      }
      if (dist > 0 && dist < 250) {
        let id = mNodeList[i].id;
        mDxMap[id] = mDxMap[id] + distX / dist * k * k / dist * ejectFactor;
        mDyMap[id] = mDyMap[id] + distY / dist * k * k / dist * ejectFactor;
      }
    }
  }
}

/**
 * 计算Edge的引力对两端Node产生的引力。
 * Calculate the traction force generated by the edge acted on the two nodes of its two ends.
 */
function calculateTraction() {
  let condenseFactor = 3;
  let startNode, endNode;
  for (let e = 0; e < mEdgeList.length; e++) {
    let eStartID = mEdgeList[e].source;
    let eEndID = mEdgeList[e].target;
    startNode = mNodeMap[eStartID];
    endNode = mNodeMap[eEndID];
    if (!startNode) {
      console.log("Cannot find start node id: " + eStartID + ", please check it out.");
      return;
    }
    if (!endNode) {
      console.log("Cannot find end node id: " + eEndID + ", please check it out.");
      return;
    }
    let distX, distY, dist;
    distX = startNode.x - endNode.x;
    distY = startNode.y - endNode.y;
    dist = Math.sqrt(distX * distX + distY * distY);
    mDxMap[eStartID] = mDxMap[eStartID] - distX * dist / k * condenseFactor;
    mDyMap[eStartID] = mDyMap[eStartID] - distY * dist / k * condenseFactor;
    mDxMap[eEndID] = mDxMap[eEndID] + distX * dist / k * condenseFactor;
    mDyMap[eEndID] = mDyMap[eEndID] + distY * dist / k * condenseFactor;
  }
}

/**
 * 更新坐标。
 * update the coordinates.
 */
function updateCoordinates() {
  let maxt = 4, maxty = 3; //Additional coefficients.
  for (let v = 0; v < mNodeList.length; v++) {
    let node = mNodeList[v];
    let dx = Math.floor(mDxMap[node.id]);
    let dy = Math.floor(mDyMap[node.id]);

    if (dx < -maxt) dx = -maxt;
    if (dx > maxt) dx = maxt;
    if (dy < -maxty) dy = -maxty;
    if (dy > maxty) dy = maxty;
    node.x = node.x + dx >= CANVAS_WIDTH || node.x + dx <= 0 ? node.x - dx : node.x + dx;
    node.y = node.y + dy >= CANVAS_HEIGHT || node.y + dy <= 0 ? node.y - dy : node.y + dy;
  }
}

function Result(nodes = null, links = null) {
  this.nodes = nodes;
  this.links = links;
}


产生的json数据

	{"nodes":[{"id":0,"x":253.0333390012338,"y":252.6177473512779},{"id":1,"x":385.40902721571337,"y":621.2774375590121},{"id":2,"x":785.7603499553587,"y":385.22055035590023},{"id":3,"x":405.77265246315744,"y":758.9663294857901},{"id":4,"x":387.42566167935115,"y":375.9496386440198},{"id":5,"x":636.2256279616934,"y":650.2703930201333},{"id":6,"x":903.5701082424029,"y":473.84409520512554},{"id":7,"x":629.0119557644166,"y":417.6638952006809},{"id":8,"x":823.6644385662,"y":707.0517481889663},{"id":9,"x":684.9305534356378,"y":343.37841641501205},{"id":10,"x":664.0325218669059,"y":738.3612618675652},{"id":11,"x":538.0046990344931,"y":416.53344738862125},{"id":12,"x":528.4011490009761,"y":646.4668782641444},{"id":13,"x":723.8174222062573,"y":495.39056742955427},{"id":14,"x":556.9332189690231,"y":297.1420909111563},{"id":15,"x":507.85974528045915,"y":536.5949966440751},{"id":16,"x":496.19506343566377,"y":196.06821865786628},{"id":17,"x":398.0298555427812,"y":486.6329335530637},{"id":18,"x":638.6390255996482,"y":526.7116765697044},{"id":19,"x":865.8762559421327,"y":567.6816928531405}],"links":[{"source":0,"target":14},{"source":0,"target":17},{"source":1,"target":2},{"source":1,"target":18},{"source":1,"target":1},{"source":2,"target":6},{"source":2,"target":11},{"source":2,"target":13},{"source":2,"target":11},{"source":2,"target":18},{"source":2,"target":9},{"source":3,"target":12},{"source":3,"target":1},{"source":3,"target":15},{"source":3,"target":10},{"source":3,"target":12},{"source":4,"target":9},{"source":4,"target":3},{"source":5,"target":9},{"source":5,"target":15},{"source":5,"target":13},{"source":5,"target":12},{"source":5,"target":12},{"source":5,"target":12},{"source":5,"target":13},{"source":5,"target":10},{"source":5,"target":7},{"source":6,"target":18},{"source":6,"target":2},{"source":6,"target":19},{"source":6,"target":8},{"source":6,"target":19},{"source":6,"target":2},{"source":6,"target":10},{"source":7,"target":6},{"source":7,"target":15},{"source":7,"target":7},{"source":7,"target":17},{"source":7,"target":19},{"source":8,"target":5},{"source":8,"target":10},{"source":8,"target":2},{"source":9,"target":13},{"source":9,"target":15},{"source":9,"target":12},{"source":9,"target":6},{"source":9,"target":14},{"source":10,"target":1},{"source":10,"target":13},{"source":10,"target":8},{"source":10,"target":9},{"source":11,"target":4},{"source":11,"target":11},{"source":11,"target":18},{"source":11,"target":7},{"source":11,"target":14},{"source":11,"target":7},{"source":11,"target":17},{"source":12,"target":5},{"source":12,"target":7},{"source":12,"target":14},{"source":12,"target":10},{"source":12,"target":11},{"source":12,"target":17},{"source":12,"target":8},{"source":12,"target":3},{"source":12,"target":14},{"source":13,"target":16},{"source":13,"target":4},{"source":13,"target":13},{"source":13,"target":8},{"source":13,"target":15},{"source":13,"target":18},{"source":13,"target":19},{"source":14,"target":9},{"source":14,"target":16},{"source":14,"target":16},{"source":15,"target":4},{"source":15,"target":7},{"source":15,"target":10},{"source":15,"target":1},{"source":15,"target":9},{"source":15,"target":11},{"source":16,"target":7},{"source":16,"target":18},{"source":16,"target":16},{"source":16,"target":14},{"source":16,"target":4},{"source":16,"target":0},{"source":16,"target":11},{"source":17,"target":15},{"source":17,"target":3},{"source":17,"target":13},{"source":17,"target":4},{"source":17,"target":12},{"source":17,"target":1},{"source":17,"target":5},{"source":17,"target":7},{"source":17,"target":14},{"source":18,"target":11},{"source":18,"target":2},{"source":18,"target":17},{"source":18,"target":11},{"source":18,"target":8},{"source":18,"target":7},{"source":18,"target":11},{"source":18,"target":7},{"source":19,"target":6},{"source":19,"target":14},{"source":19,"target":8},{"source":19,"target":11},{"source":19,"target":2},{"source":19,"target":19}]}

获得结点坐标与结点父子关系后 绘制拓扑图

   		ForceDirected();
   		console.log(json);
   		json = JSON.parse(json);	 
   		//生成div
   		for(var i = 0; i<20 ; i++){
   	
   			var x = json["nodes"][i]["x"];
   			var y = json["nodes"][i]["y"];
   			var div = '<div class="group-container  database-trace-library lib" style="width:60px;height:60px;top:'+x+'px;left:'+y+'px;" id="database_'+i+'" group="databasegroup_'+i+'">'+
   			'<div  style="position: relative">'+
   				'<span class="database-library-tit"><img src="../../assets/images/analyze_images/analyze_database.png" alt="">'+i+'</span>'+
   				'<div style="position: absolute;top:0;left:0;height: 20px;width:20px" id="service_'+i+'"></div>'+
   				'</div></div>';
   			
   			$("#canvas").append(div);
   			var next = i + 1;
   			add2Group("database_"+i,"databasegroup_"+i,"service_"+i,j);
   		}
   		
   		$.each(json["links"],function(index,link){
   			
   			var sourceId = link["source"];
   			var target = link["target"];
   			
   			connectA2B(document.querySelector("#database_"+sourceId),document.querySelector("#service_"+target),j );
   		
   		});
   		function connectA2B(a,b,j){
   
   	     // connect some before configuring group
           j.connect({
               source: a,
               target: b,
               anchors:["Right","Left"],
               endpoint: ["Dot", {radius: 1}],
               overlays: [
                   ["Arrow",
                       {   width: 5,
                           length:5,
                           location: 1,
                           id: "arrow"
                       }
                   ]
               ],
               paintStyle : { strokeWidth : 1, stroke : "#456" }//连线样式

           });
   	
   }
   
   function add2Group( groupId,groupName,divId,j){
   
   		j.addGroup({
               el: document.querySelector("#"+groupId),
               id: groupName,
               dropOverride: true,
               endpoint: ["Dot", {radius: 3}]
//                droppable:false
           });  //(the default is to revert)
   	
           j.addToGroup(groupName,document.querySelector("#"+divId));

   	
   }

最终效果图

20个结点两百次迭代的效果图,没想到离开了学校还是要和物理打交道,万物皆可做模型。
JSPlumb.js 绘制关系拓扑图(力导向布局算法简单实现)