轉自:http://www.it165.net/pro/html/201311/7616.html
使用JsPlumb繪制拓撲圖的通用方法
一、 實現目標
繪制拓撲圖, 實際上是個數據結構和算法的問題。 需要設計一個合適的數據結構來表達拓撲結構,設計一個算法來計算拓撲節點的位置及連接。

二、 實現思想
1. 數據結構
首先, 從節點開始。 顯然, 需要一個字段 type 表示節點類型, 一個字段 data 表示節點數據(詳情), 對於連接, 則采用一個 rel 字段, 表示有哪些節點與之關聯, 相當於C 里面的指針。 為了唯一標識該節點, 還需要一個字段 key 。 通過 type-key 組合來唯一標識該節點。 這樣, 初步定下數據結構如下:
a. 節點數據結構: node = { type: 'typeName', key: 'key', rel: [], data: {'More Info'}}
b. rel, data 可選 , type-key 唯一標識該節點, rel 為空標識該節點為葉子節點
c. 關聯關系: rel: [node1, node2, ..., nodeN]
d. 更多詳情: 關於節點的更多信息可放置於此屬性中
2. 算法
在算法上, 要預先規划好各個節點類型如何布局以及如何連接。 連接方向很容易定: 根據起始節點及終止節點的類型組合, 可以規定不同的連接方向。 位置確定稍有點麻煩。 這里采用的方法是: 采用深度遍歷方法, 下一個的節點位置通過上一個節點位置確定, 不同類型的節點位置計算不一樣, 但是相同類型的節點位置是重合的, 需要在后面進行調整。實際上, 這個節點位置的算法是不夠高明的, 如果有更好的算法, 請告知。
3. JsPlumb
jsPlumb 有幾個基本概念。 首先, 拓撲節點實際上是 DIV 區域,每個DIV 都必須有一個ID,用於唯一標識該節點。 連接拓撲節點的一個重要概念是EndPoint . EndPoint 是附着於節點上的連接線的端點, 簡稱“附着點”。 將附着點 attach 到指定拓撲節點上的方法如下:
jsPlumb.addEndpoint(toId, this.sourceEndpoint, { anchor: sourceAnchor, uuid:sourceUUID });
toId 是 拓撲節點的 DIV 區域的 ID 值, sourceEndpoint 是附着點的樣式設置, 可以復用 , sourceAnchor 是附着點位置, 共有八種:
- Top
(also aliased as TopCenter
) - TopRight
- Right
(also aliased as RightMiddle
) - BottomRight
- Bottom
(also aliased asBottomCenter
) -BottomLeft
- Left
(also aliased as LeftMiddle
) - TopLeft
sourceUUID 是拓撲節點與附着位置的結合, 也就是說, 要將一個 附着點附着到拓撲節點為 toId 的 sourceAnchor 指定的位置上。 每個拓撲節點都可以定義多個源附着點和目標附着點。 源附着點是連接線的起始端, 目標附着點是連接線的終止端。
兩個 uuid 即可定義一條連接線:
jsPlumb.connect({uuids:[startPoint, endPoint], editable: false});
startPoint 和 endPoint 分別是連接線的起始端 Endpoint uuid 和 終止段 Endpoint uuid. 它定義了從起始拓撲節點的指定附着點連接到終止拓撲節點的指定附着點。
三、 實現代碼
drawTopo.js 提供繪制拓撲圖的基本方法, 只要按照數據結構扔進去, 就可以自動繪制出拓撲圖來。
1 /** 2 * 使用 jsPlumb 根據指定的拓撲數據結構繪制拓撲圖 3 * 使用 drawTopo(topoData, nodeTypeArray) 方法 4 * 5 */ 6 7 /** 8 * 初始化拓撲圖實例及外觀設置 9 */ 10 (function() { 11 12 jsPlumb.importDefaults({ 13 14 DragOptions : { cursor: 'pointer', zIndex:2000 }, 15 16 EndpointStyles : [{ fillStyle:'#225588' }, { fillStyle:'#558822' }], 17 18 Endpoints : [ [ "Dot", { radius:2 } ], [ "Dot", { radius: 2 } ]], 19 20 ConnectionOverlays : [ 21 [ "Arrow", { location:1 } ], 22 [ "Label", { 23 location:0.1, 24 id:"label", 25 cssClass:"aLabel" 26 }] 27 ] 28 }); 29 30 var connectorPaintStyle = { 31 lineWidth: 1, 32 strokeStyle: "#096EBB", 33 joinstyle:"round", 34 outlineColor: "#096EBB", 35 outlineWidth: 1 36 }; 37 38 var connectorHoverStyle = { 39 lineWidth: 2, 40 strokeStyle: "#5C96BC", 41 outlineWidth: 2, 42 outlineColor:"white" 43 }; 44 45 var endpointHoverStyle = { 46 fillStyle:"#5C96BC" 47 }; 48 49 window.topoDrawUtil = { 50 51 sourceEndpoint: { 52 endpoint:"Dot", 53 paintStyle:{ 54 strokeStyle:"#1e8151", 55 fillStyle:"transparent", 56 radius: 2, 57 lineWidth:2 58 }, 59 isSource:true, 60 maxConnections:-1, 61 connector:[ "Flowchart", { stub:[40, 60], gap:10, cornerRadius:5, alwaysRespectStubs:true } ], 62 connectorStyle: connectorPaintStyle, 63 hoverPaintStyle: endpointHoverStyle, 64 connectorHoverStyle: connectorHoverStyle, 65 dragOptions:{}, 66 overlays:[ 67 [ "Label", { 68 location:[0.5, 1.5], 69 label:"", 70 cssClass:"endpointSourceLabel" 71 } ] 72 ] 73 }, 74 75 targetEndpoint: { 76 endpoint: "Dot", 77 paintStyle: { fillStyle:"#1e8151",radius: 2 }, 78 hoverPaintStyle: endpointHoverStyle, 79 maxConnections:-1, 80 dropOptions:{ hoverClass:"hover", activeClass:"active" }, 81 isTarget:true, 82 overlays:[ 83 [ "Label", { location:[0.5, -0.5], label:"", cssClass:"endpointTargetLabel" } ] 84 ] 85 }, 86 87 initConnection: function(connection) { 88 connection.getOverlay("label").setLabel(connection.sourceId + "-" + connection.targetId); 89 connection.bind("editCompleted", function(o) { 90 if (typeof console != "undefined") 91 console.log("connection edited. path is now ", o.path); 92 }); 93 }, 94 95 addEndpoints: function(toId, sourceAnchors, targetAnchors) { 96 for (var i = 0; i < sourceAnchors.length; i++) { 97 var sourceUUID = toId + sourceAnchors[i]; 98 jsPlumb.addEndpoint(toId, this.sourceEndpoint, { anchor:sourceAnchors[i], uuid:sourceUUID }); 99 } 100 for (var j = 0; j < targetAnchors.length; j++) { 101 var targetUUID = toId + targetAnchors[j]; 102 jsPlumb.addEndpoint(toId, this.targetEndpoint, { anchor:targetAnchors[j], uuid:targetUUID }); 103 } 104 } 105 }; 106 107 108 })(); 109 110 /** 111 * drawTopo 根據給定拓撲數據繪制拓撲圖 112 * @param topoData 拓撲數據 113 * @param rootPosition 拓撲圖根節點的位置 114 * @param nodeTypeArray 節點類型數組 115 * 116 * 拓撲圖的所有節點是自動生成的, DIV class = "node" , id= nodeType.toUpperCase + "-" + key 117 * 拓撲圖的所有節點連接也是自動生成的, 可以進行算法改善與優化, 但使用者不需要關心此問題 118 * 需要定義節點類型數組 nodeTypeArray 119 * 120 * 拓撲數據結構: 121 * 1. 節點數據結構: node = { type: 'typeName', key: 'key', rel: [], data: {'More Info'}} 122 * rel, data 可選 , type-key 唯一標識該節點 123 * 2. 關聯關系: rel: [node1, node2, ..., nodeN] 124 * 3. 更多詳情: 關於節點的更多信息可放置於此屬性中 125 * 4. 示例: 126 * var topoData = { 127 * type: 'VM', key: '110.75.188.35', 128 * rel: [ 129 * { type: 'DEVICE', key: '3-120343' }, 130 * { type: 'DEVICE', key: '3-120344' }, 131 * { type: 'VIP', key: '223.6.250.2', 132 * rel: [ 133 * { type: 'VM', key: '110.75.189.12' }, 134 * { type: 'VM', key: '110.75.189.12' } 135 * ] 136 * }, 137 * { type: 'NC', key: '10.242.192.2', 138 * rel: [ 139 * { type: 'VM', key: '110.75.188.132' }, 140 * { type: 'VM', key: '110.75.188.135' }, 141 * { type: 'VM', key: '110.75.188.140' } 142 * ] 143 * 144 * } 145 * ] 146 * }; 147 * 148 */ 149 function drawTopo(topoData, rootPosition, nodeTypeArray) { 150 151 // 創建所有拓撲節點及連接並確定其位置 152 createNodes(topoData, rootPosition, nodeTypeArray); 153 154 // 調整重合節點的位置, 添加節點的附着點, 即連接線的端點 155 adjust(topoData, nodeTypeArray); 156 157 // 使所有拓撲節點均為可拉拽的 158 jsPlumb.draggable(jsPlumb.getSelector(".node"), { grid: [5, 5] }); 159 160 // 創建所有節點連接 161 createConnections(topoData, nodeTypeArray); 162 163 } 164 165 /** 166 * 根據給定拓撲數據繪制拓撲節點並確定其位置, 使用深度優先遍歷 167 * @param topoData 拓撲數據 168 * @param rootPosition 根節點的位置設定 169 * @param nodeTypeArray 拓撲節點類型 170 */ 171 function createNodes(rootData, rootPosition, nodeTypeArray) { 172 173 if (rootData == null) { 174 return ; 175 } 176 177 var topoRegion = $('#topoRegion'); 178 var relData = rootData.rel; 179 var i=0, relLen = relLength(relData);; 180 var VM_TYPE = nodeTypeArray[0]; 181 var DEVICE_TYPE = nodeTypeArray[1]; 182 var NC_TYPE = nodeTypeArray[2]; 183 var VIP_TYPE = nodeTypeArray[3]; 184 185 // 根節點的位置, 單位: px 186 var rootTop = rootPosition[0]; 187 var rootLeft = rootPosition[1]; 188 189 var nextRootData = {}; 190 var nextRootPosition = []; 191 192 // 自動生成並插入根節點的 DIV 193 var divStr = createDiv(rootData); 194 var nodeDivId = obtainNodeDivId(rootData); 195 topoRegion.append(divStr); 196 //console.log(divStr); 197 198 // 設置節點位置 199 $('#'+nodeDivId).css('top', rootTop + 'px'); 200 $('#'+nodeDivId).css('left', rootLeft + 'px'); 201 202 for (i=0; i < relLen; i++) { 203 nextRootData = relData[i]; 204 nextRootPosition = obtainNextRootPosition(rootData, nextRootData, rootPosition, nodeTypeArray); 205 createNodes(nextRootData, nextRootPosition, nodeTypeArray); 206 } 207 208 } 209 210 /** 211 * 調整重合節點的位置, 並添加節點的附着點, 即連接線的端點 212 */ 213 function adjust(topoData, nodeTypeArray) { 214 215 var vm_deviceOffset = 0; // 起始節點為 vm , 終止節點為 device, device div 的偏移量 216 var vm_vipOffset = 0; // 起始節點為 vm , 終止節點為 vip, vip div 的偏移量 217 var vm_ncOffset = 0; // 起始節點為 vm , 終止節點為 nc, nc div 的偏移量 218 var vip_vmOffset = 0; // 起始節點為 vip , 終止節點為 vm, vm div 的偏移量 219 var nc_vmOffset = 0; // 起始節點為nc , 終止節點為 vm, vm div 的偏移量 220 var verticalDistance = 120; 221 var horizontalDistance = 150; 222 223 var VM_TYPE = nodeTypeArray[0]; 224 var DEVICE_TYPE = nodeTypeArray[1]; 225 var NC_TYPE = nodeTypeArray[2]; 226 var VIP_TYPE = nodeTypeArray[3]; 227 228 $('.node').each(function(index, element) { 229 var nodeDivId = $(element).attr('id'); 230 var nodeType = nodeDivId.split('-')[0]; 231 var offset = $(element).offset(); 232 var originalTop = offset.top; 233 var originalLeft = offset.left; 234 var parentNode = $(element).parent(); 235 var parentNodeType = parentNode.attr('id').split('-')[0]; 236 switch (nodeType) { 237 case VM_TYPE: 238 // VM 位置水平偏移 239 $(element).css('left', (originalLeft + vip_vmOffset*horizontalDistance) + 'px'); 240 vip_vmOffset++; 241 topoDrawUtil.addEndpoints(nodeDivId, ['Top', 'Bottom', 'Right'], []); 242 break; 243 case DEVICE_TYPE: 244 // DEVICE 位置垂直偏移 245 $(element).css('top', (originalTop + (vm_deviceOffset-1)*verticalDistance) + 'px'); 246 vm_deviceOffset++; 247 topoDrawUtil.addEndpoints(nodeDivId, [], ['Left']); 248 break; 249 case VIP_TYPE: 250 // VIP 位置水平偏移 251 $(element).css('left', (originalLeft + vm_vipOffset*horizontalDistance) + 'px'); 252 vm_vipOffset++; 253 topoDrawUtil.addEndpoints(nodeDivId, ['Top'], ['Bottom']); 254 break; 255 case NC_TYPE: 256 // NC 位置水平偏移 257 $(element).css('left', (originalLeft + vm_ncOffset*verticalDistance) + 'px'); 258 vm_ncOffset++; 259 topoDrawUtil.addEndpoints(nodeDivId, ['Bottom'], ['Top']); 260 break; 261 default: 262 break; 263 } 264 }); 265 } 266 267 /** 268 * 獲取下一個根節點的位置, 若節點類型相同, 則位置會重合, 需要后續調整一次 269 * @root 當前根節點 270 * @nextRoot 下一個根節點 271 * @rootPosition 當前根節點的位置 272 * @nodeTypeArray 節點類型數組 273 */ 274 function obtainNextRootPosition(root, nextRoot, rootPosition, nodeTypeArray) { 275 276 var VM_TYPE = nodeTypeArray[0]; 277 var DEVICE_TYPE = nodeTypeArray[1]; 278 var NC_TYPE = nodeTypeArray[2]; 279 var VIP_TYPE = nodeTypeArray[3]; 280 281 var startNodeType = root.type; 282 var endNodeType = nextRoot.type; 283 var nextRootPosition = []; 284 var rootTop = rootPosition[0]; 285 var rootLeft = rootPosition[1]; 286 287 var verticalDistance = 120; 288 var horizontalDistance = 250; 289 var shortVerticalDistance = 80; 290 291 switch (startNodeType) { 292 case VM_TYPE: 293 if (endNodeType == VIP_TYPE) { 294 nextRootPosition = [rootTop-verticalDistance, rootLeft]; 295 } 296 else if (endNodeType == DEVICE_TYPE) { 297 nextRootPosition = [rootTop, rootLeft+horizontalDistance]; 298 } 299 else if (endNodeType == NC_TYPE) { 300 nextRootPosition = [rootTop+verticalDistance, rootLeft]; 301 } 302 break; 303 case VIP_TYPE: 304 if (endNodeType == VM_TYPE) { 305 nextRootPosition = [rootTop-shortVerticalDistance, rootLeft]; 306 } 307 break; 308 case NC_TYPE: 309 if (endNodeType == VM_TYPE) { 310 nextRootPosition = [rootTop+shortVerticalDistance, rootLeft]; 311 } 312 break; 313 default: 314 break; 315 } 316 return nextRootPosition; 317 } 318 319 /** 320 * 根據給定拓撲數據, 繪制節點之間的連接關系, 使用深度優先遍歷 321 * @param topoData 拓撲數據 322 * @param nodeTypeArray 節點類型數組 323 */ 324 function createConnections(topoData, nodeTypeArray) { 325 326 if (topoData == null) { 327 return ; 328 } 329 var rootData = topoData; 330 var relData = topoData.rel; 331 var i=0, len = relLength(relData);; 332 for (i=0; i < len; i++) { 333 connectionNodes(rootData, relData[i], nodeTypeArray); 334 createConnections(relData[i], nodeTypeArray); 335 } 336 } 337 338 /** 339 * 連接起始節點和終止節點 340 * @beginNode 起始節點 341 * @endNode 終止節點 342 * NOTE: 根據是起始節點與終止節點的類型 343 */ 344 function connectionNodes(beginNode, endNode, nodeTypeArray) 345 { 346 var startNodeType = beginNode.type; 347 var endNodeType = endNode.type; 348 var startDirection = ''; 349 var endDirection = ''; 350 351 var VM_TYPE = nodeTypeArray[0]; 352 var DEVICE_TYPE = nodeTypeArray[1]; 353 var NC_TYPE = nodeTypeArray[2]; 354 var VIP_TYPE = nodeTypeArray[3]; 355 356 switch (startNodeType) { 357 case VM_TYPE: 358 if (endNodeType == VIP_TYPE) { 359 // VIP 繪制於 VM 上方 360 startDirection = 'Top'; 361 endDirection = 'Bottom'; 362 } 363 else if (endNodeType == DEVICE_TYPE) { 364 // DEVICE 繪制於 VM 右方 365 startDirection = 'Right'; 366 endDirection = 'Left'; 367 } 368 else if (endNodeType == NC_TYPE) { 369 // NC 繪制於 VM 下方 370 startDirection = 'Bottom'; 371 endDirection = 'Top'; 372 } 373 break; 374 case VIP_TYPE: 375 if (endNodeType == VM_TYPE) { 376 // VM 繪制於 VIP 上方 377 startDirection = 'Top'; 378 endDirection = 'Top'; 379 } 380 break; 381 case NC_TYPE: 382 if (endNodeType == VM_TYPE) { 383 // VM 繪制於 NC 下方 384 startDirection = 'Bottom'; 385 endDirection = 'Bottom'; 386 } 387 break; 388 default: 389 break; 390 } 391 var startPoint = obtainNodeDivId(beginNode) + startDirection; 392 var endPoint = obtainNodeDivId(endNode) + endDirection; 393 jsPlumb.connect({uuids:[startPoint, endPoint], editable: false}); 394 } 395 396 function createDiv(metaNode) { 397 return '<div class="node" id="' + obtainNodeDivId(metaNode) + '"><strong>' 398 + metaNode.type + '<br/><a href="http://aliyun.com">' + metaNode.key + '</a><br/></strong></div>' 399 } 400 401 /** 402 * 生成節點的 DIV id 403 * divId = nodeType.toUpperCase + "-" + key 404 * key 可能為 IP , 其中的 . 將被替換成 ZZZ , 因為 jquery id 選擇器中 . 屬於轉義字符. 405 * eg. {type: 'VM', key: '1.1.1.1' }, divId = 'VM-1ZZZ1ZZZ1ZZZ1' 406 */ 407 function obtainNodeDivId(metaNode) { 408 return metaNode.type.toUpperCase() + '-' + transferKey(metaNode.key); 409 } 410 411 function transferKey(key) { 412 return key.replace(/\./g, 'ZZZ'); 413 } 414 415 function revTransferKey(value) { 416 return value.replace(/ZZZ/g, '.'); 417 } 418 419 420 /** 421 * 合並新的拓撲結構到原來的拓撲結構中, 新的拓撲結構中有節點與原拓撲結構中的某個節點相匹配: type-key 相等 422 * @param srcTopoData 原來的拓撲結構 423 * @param newTopoData 要添加的的拓撲結構 424 */ 425 function mergeNewTopo(srcTopoData, newTopoData) { 426 427 var srcTopoData = shallowCopyTopo(srcTopoData); 428 429 if (srcTopoData == null || newTopoData == null) { 430 return srcTopoData || newTopoData; 431 } 432 433 var srcRoot = srcTopoData; 434 var newRoot = newTopoData; 435 436 var newRelData = newTopoData.rel; 437 var i=0, newRelLen = relLength(newRelData); 438 439 var matched = findMatched(srcRoot, newRoot); 440 if (matched == null) { 441 // 沒有找到匹配的節點, 直接返回原有的拓撲結構 442 return srcTopoData; 443 } 444 matched.rel = matched.rel.concat(newRelData); 445 return srcTopoData; 446 } 447 448 /** 449 * 在原拓撲結構中查找與新拓撲結構根節點 newRootData 匹配的節點 450 * @param srcRootData 原拓撲結構 451 * @param newRootData 新拓撲結構的根節點 452 * @returns 原拓撲結構中與新拓撲結構根節點匹配的節點 or null if not found 453 */ 454 function findMatched(srcRootData, newRootData) { 455 var srcRelData = srcRootData.rel; 456 var i=0, srcRelLen = relLength(srcRelData); 457 var matched = null; 458 if ((srcRootData.type == newRootData.type) && (srcRootData.key == newRootData.key)) { 459 return srcRootData; 460 } 461 for (i=0; i<srcRelLen; i++) { 462 matched = findMatched(srcRelData[i], newRootData); 463 if (matched != null) { 464 return matched; 465 } 466 } 467 return matched; 468 } 469 470 function relLength(relData) { 471 if (isArray(relData)) { 472 return relData.length; 473 } 474 return 0; 475 } 476 477 function isArray(value) { 478 return value && (typeof value === 'object') && (typeof value.length === 'number'); 479 } 480 481 /** 482 * 淺復制拓撲結構 483 */ 484 function shallowCopyTopo(srcTopoData) { 485 return srcTopoData; 486 } 487 488 /** 489 * 深復制拓撲結構 490 */ 491 function deepCopyTopo(srcTopoData) { 492 //TODO identical to deep copy of js json 493 }
topodemo.html 繪制拓撲圖的客戶端接口。 只要引進相應的依賴 JS,預置一個 <div id="topoRegion"></div>
<!doctype html> <html> <head> <title>jsPlumb 1.5.3 - flowchart connectors demonstration - jQuery</title> <link rel="stylesheet" href="topo-all.css"> <link rel="stylesheet" href="topo.css"> <!-- DEP --> <script src="../jsPlumb/jquery-1.9.0-min.js"></script> <script src="../jsPlumb/jquery-ui-1.9.2-min.js"></script> <!-- /DEP --> <!-- JS --> <!-- support lib for bezier stuff --> <script src="../jsPlumb/jsBezier-0.6-min.js"></script> <!-- <a href="http://www.it165.net/pro/webjsp/" target="_blank" class="keylink">jsp</a>lumb geom functions --> <script src="../jsPlumb/<a href="http://www.it165.net/pro/webjsp/" target="_blank" class="keylink">jsp</a>lumb-geom-0.1.js"></script> <!-- jsplumb util --> <script src="../jsPlumb/util.js"></script> <!-- base DOM adapter --> <script src="../jsPlumb/dom-adapter.js"></script> <!-- main jsplumb engine --> <script src="../jsPlumb/jsPlumb.js"></script> <!-- endpoint --> <script src="../jsPlumb/endpoint.js"></script> <!-- connection --> <script src="../jsPlumb/connection.js"></script> <!-- anchors --> <script src="../jsPlumb/anchors.js"></script> <!-- connectors, endpoint and overlays --> <script src="../jsPlumb/defaults.js"></script> <!-- connector editors --> <script src="../jsPlumb/connector-editors.js"></script> <!-- bezier connectors --> <script src="../jsPlumb/connectors-bezier.js"></script> <!-- state machine connectors --> <script src="../jsPlumb/connectors-statemachine.js"></script> <!-- flowchart connectors --> <script src="../jsPlumb/connectors-flowchart.js"></script> <!-- SVG renderer --> <script src="../jsPlumb/renderers-svg.js"></script> <!-- canvas renderer --> <script src="../jsPlumb/renderers-canvas.js"></script> <!-- vml renderer --> <script src="../jsPlumb/renderers-vml.js"></script> <!-- jquery jsPlumb adapter --> <script src="../jsPlumb/jquery.jsPlumb.js"></script> <!-- /JS --> <!-- demo code --> <script src="drawtopo.js"></script> <script type="text/javascript"> jsPlumb.bind("ready", function() { // 拓撲數據結構根節點位置設置 var rootPosition = [270, 300]; var nodeTypeArray = ['VM', 'DEVICE', 'NC', 'VIP']; var topoData = { type: 'VM', key: '110.75.188.35', rel: [ { type: 'DEVICE', key: '3-120343' }, { type: 'DEVICE', key: '3-120344' }, { type: 'VIP', key: '223.6.250.2', rel: [ { type: 'VM', key: '110.75.189.12' }, { type: 'VM', key: '110.75.189.13' } ] }, { type: 'NC', key: '10.242.192.2', rel: [ { type: 'VM', key: '110.75.188.132' }, { type: 'VM', key: '110.75.188.135' } ] } ] }; drawTopo(topoData, rootPosition, nodeTypeArray); var newTopoData = { type: 'NC', key: '10.242.192.2', rel: [ { type: 'VM', key: '110.75.188.140' } ] }; var mergedTopoData = mergeNewTopo(topoData, newTopoData); $('#topoRegion').empty(); drawTopo(mergedTopoData, rootPosition, nodeTypeArray); }); </script> </head> <body> <div id="topoRegion"> </div> </body> </html>
樣式文件及依賴JS 見工程示例。 里面已經包含繪制拓撲圖的最小依賴。
四、 最終效果圖