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;
}
{"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 = '+x+'px;left:'+y+'px;" id="database_'+i+'" group="databasegroup_'+i+'">'+
''+
''+i+''+
'+i+'">'+
'';
$("#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));
}