使用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 见工程示例。 里面已经包含绘制拓扑图的最小依赖。
四、 最终效果图