最近有客户提到自定义链路的需求,个人感觉非常有代表意义,现在共享出来给大家参考一下。先来看看需求:
- 链路要分成两半,用两种颜色填充。
- 填充百分比在不同值域时,用不同颜色。
- 显示刻度
- 有个开关,可以控制链路变短,变短后,链路只画开始和结束部分(相当于原始链路的缩影),中间不画
- 如果有多条链路,链路合并后两端分别显示这些链路中的最高填充百分比
合并前:
合并后:
- 进入子网后,节点上显示和上层节点的连线信息进入子网前,节点2和子网内节点4之间有链路:
进入子网后,节点4上也显示此链路:
先看看实现的效果,后面我们慢慢解释如何定制链路:
前5个需求可以通过自定义Link和LinkUI实现,需要注意:
- 用Link#getBundleLinks获取所有的捆绑链路
- 用LinkUI#drawLinePoints画线
完整代码如下:
// 自定义Link构造函数 demo.ScaleLink = function(id, from, to) { // 调用基类构造函数 demo.ScaleLink.superClass.constructor.call(this, id, from, to); // 设置链路宽度为10个像素 this.setStyle('link.width', 10); //this.setStyle('link.color', 'rgba(0, 0, 0, 0)'); // 设置Link类型为平行 this.setStyle('link.type', 'parallel'); // 设置链路捆绑的间距为40 this.setStyle('link.bundle.offset', 40); // 设置刻度颜色 this.setClient('scaleColor', 'black'); // 设置刻度宽度 this.setClient('scaleWidth', 1); // 设置刻度个数 this.setClient('scaleNumbers', 4); // 设置是否变短 this.setClient('shortened', false); // 设置变短后的长度 this.setClient('shortenLength', 100); // 设置分割线颜色 this.setClient('splitterColor', 'black'); // 设置起始填充百分比 this.setClient('fromFillPercent', 0); // 设置结束填充百分比 this.setClient('toFillPercent', 0); }; // 设置自定义Link继承twaver.Link twaver.Util.ext('demo.ScaleLink', twaver.Link, { // 重载获取UI类方法,返回自定义UI类 getCanvasUIClass : function () { return demo.ScaleLinkUI; }, // 根据百分比获取填充颜色 getFillColor: function(percent) { if (percent < 0.25) { return 'green'; } if (percent < 0.5) { return 'yellow'; } if (percent < 0.75) { return 'magenta'; } return 'red'; }, // 获取起始填充颜色 getFromFillColor: function () { return this.getFillColor(this.getFromFillPercent()); }, // 获取结束填充颜色 getToFillColor: function () { return this.getFillColor(this.getToFillPercent()); }, // 获取起始百分比 getFromFillPercent: function () { // 如果是链路捆绑代理,返回所有捆绑链路中填充百分比最大的值 if (this.isBundleAgent()) { var fromAgent = this.getFromAgent(), percentKey, maxPercent = 0, percent; this.getBundleLinks().forEachSiblingLink(function (link) { percentKey = fromAgent === link.getFromAgent() ? 'fromFillPercent' : 'toFillPercent'; percent = link.getClient(percentKey); maxPercent = percent > maxPercent ? percent : maxPercent; }); return maxPercent; } else { return this.getClient('fromFillPercent'); } }, // 获取结束百分比 getToFillPercent: function () { // 如果是链路捆绑代理,返回所有捆绑链路中填充百分比最大的值 if (this.isBundleAgent()) { var toAgent = this.getToAgent(), percentKey, maxPercent = 0, percent; this.getBundleLinks().forEachSiblingLink(function (link) { percentKey = toAgent === link.getToAgent() ? 'toFillPercent' : 'fromFillPercent'; percent = link.getClient(percentKey); maxPercent = percent > maxPercent ? percent : maxPercent; }); return maxPercent; } else { return this.getClient('toFillPercent'); } }, // 重载获取网元名称方法,判断如果是链路捆绑代理,就返回起始和结束代理节点的名称 getName: function () { if (this.getClient('shortened')) { return null; } else if (this.isBundleAgent()) { return this.getFromAgent().getName() + '-' + this.getToAgent().getName(); } else { return demo.ScaleLink.superClass.getName.call(this); } } }); // 自定义LinkUI构造函数 demo.ScaleLinkUI = function(network, element){ // 调用基类构造函数 demo.ScaleLinkUI.superClass.constructor.call(this, network, element); }; // 设置自定义Link继承twaver.canvas.LinkUI twaver.Util.ext('demo.ScaleLinkUI', twaver.canvas.LinkUI, { // 获取Link角度 getAngle: function () { return getAngle(this.getFromPoint(), this.getToPoint()); }, // 获取Link中间点 getMiddlePoint: function (from, to, percent) { return { x: from.x + (to.x - from.x) * percent, y: from.y + (to.y - from.y) * percent }; }, // 画刻度线 drawScaleLine: function (from, to, angle, length, ctx, percent, lineWidth, lineColor) { var point = this.getMiddlePoint(from, to, percent); var y = length/2 * Math.sin(angle), x = length/2 * Math.cos(angle); ctx.beginPath(); ctx.lineWidth = lineWidth; ctx.strokeStyle = lineColor; ctx.moveTo(point.x + x, point.y + y); ctx.lineTo(point.x - x, point.y -y); ctx.stroke(); }, // 获取是否将链路变短 isShorten: function () { var link = this.getElement(); return link.getClient('shortened') && this.getLineLength() > link.getClient('shortenLength') * 2; }, // 重载画链路函数,用自定义逻辑画链路 paintBody: function (ctx) { var points = this.getLinkPoints(), link = this.getElement(); if (!points || points.size() < 2) { return; } var lineLength = this.getLineLength(), shortenLength = link.getClient('shortenLength'), percent = shortenLength / lineLength, from = points.get(0), to = points.get(1), angle = this.getAngle() + Math.PI/2; if (this.isShorten()) { fromPoints = new twaver.List([from, this.getMiddlePoint(from, to, percent)]); toPoints = new twaver.List([this.getMiddlePoint(from, to, 1 - percent), to]); this._paintBody(ctx, fromPoints, angle); this._paintBody(ctx, toPoints, angle); // 画文字 ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'black'; var textCenter = {x: (fromPoints.get(0).x + fromPoints.get(1).x)/2, y: (fromPoints.get(0).y + fromPoints.get(1).y)/2}; ctx.fillText(link.getName(), textCenter.x, textCenter.y); textCenter = {x: (toPoints.get(0).x + toPoints.get(1).x)/2, y: (toPoints.get(0).y + toPoints.get(1).y)/2}; ctx.fillText(link.getName(), textCenter.x, textCenter.y); ctx.fillText(link.getToNode().getName(), fromPoints.get(1).x, fromPoints.get(1).y); ctx.fillText(link.getFromNode().getName(), toPoints.get(0).x, toPoints.get(0).y); } else { this._paintBody(ctx, points, angle); } // 画起始箭头 if (link.getClient('arrow.from')) { twaver.Util.drawArrow(ctx, 12, 9, points, true, 'arrow.standard', true, 'gray', 0, 0, 1, 'black'); } // 画结束箭头 if (link.getClient('arrow.to')) { twaver.Util.drawArrow(ctx, 12, 9, points, false, 'arrow.standard', true, 'gray', 0, 0, 1, 'black'); } }, _paintBody: function (ctx, points, angle) { var link = this.getElement(), width = link.getStyle('link.width'), grow = width, outerColor = this.getOuterColor(); if (outerColor) { var outerWidth = link.getStyle('outer.width'); grow += outerWidth * 2; } var selectBorder = !this.getEditAttachment() && link.getStyle('select.style') === 'border' && this.getNetwork().isSelected(link); if (selectBorder) { var selectWidth = link.getStyle('select.width'); grow += selectWidth * 2; } ctx.lineCap = link.getStyle('link.cap'); ctx.lineJoin = link.getStyle('link.join'); // 画选中边框 if (selectBorder) { this.drawLinePoints(ctx, points, grow, link.getStyle('select.color')); } // 画边框 if (outerColor) { this.drawLinePoints(ctx, points, width + outerWidth * 2, outerColor); } // 画Link this.drawLinePoints(ctx, points, width, this.getInnerColor() || link.getStyle('link.color')); var fromFillPercent = link.getFromFillPercent(), toFillPercent = link.getToFillPercent(), fromFillColor = link.getFromFillColor(), toFillColor = link.getToFillColor(), from = points.get(0), to = points.get(1); var x = from.x + (to.x - from.x) / 2 * fromFillPercent, y = from.y + (to.y - from.y) / 2 * fromFillPercent; var middle = {x: x, y: y}; var fromPoints = new twaver.List([from, middle]); // 画起始填充色 this.drawLinePoints(ctx, fromPoints, width, fromFillColor); from = points.get(1); to = points.get(0); x = from.x + (to.x - from.x) / 2 * toFillPercent; y = from.y + (to.y - from.y) / 2 * toFillPercent; middle = {x: x, y: y}; var toPoints = new twaver.List([from, middle]); // 画结束填充色 this.drawLinePoints(ctx, toPoints, width, toFillColor); from = points.get(0); to = points.get(1); var scaleWidth = link.getClient('scaleWidth'), scaleColor = link.getClient('scaleColor'); // 画刻度 for (var i = 1, n = link.getClient('scaleNumbers') * 2; i < n; i++) { this.drawScaleLine(from, to, angle, width/2, ctx, i/n, scaleWidth, scaleColor); } // 画分隔线 this.drawScaleLine(from, to, angle, width, ctx, 0.5, 3, link.getClient('splitterColor')); } });
最后一个需求可以通过定制Node和NodeUI达到目的:
// 自定义Node构造函数 demo.ScaleNode = function(id) { // 调用基类构造函数 demo.ScaleNode.superClass.constructor.call(this, id); }; // 设置自定义Node继承twaver.Node twaver.Util.ext('demo.ScaleNode', twaver.Node, { getCanvasUIClass: function () { return demo.ScaleNodeUI; } }); // 自定义NodeUI构造函数 demo.ScaleNodeUI = function(network, element){ // 调用基类构造函数 demo.ScaleNodeUI.superClass.constructor.call(this, network, element); }; // 设置自定义NodeUI继承twaver.canvas.NodeUI twaver.Util.ext('demo.ScaleNodeUI', twaver.canvas.NodeUI, { // 重载画网元方法,画上层链路 paintBody: function (ctx) { demo.ScaleNodeUI.superClass.paintBody.call(this, ctx); var result = this.getAttachedLinks(); if (!result) { return; } for (var position in result) { this.paintLink(ctx, result[position], position); } }, // 画链路 paintLink: function (ctx, links, position) { var center = this.getElement().getCenterLocation(), count = links.length, half = count / 2, network = this.getNetwork(), gap = (count - 1) * -10, terminal, link, i, offset, shortenLength, angle, tempCenter, textWidth, textHeight = 20, textCenter; for (i=0; i<count; i++) { link = links[i]; offset = link.getStyle('link.bundle.offset'); shortenLength = link.getClient('shortenLength'); textWidth = ctx.measureText(link.getName()).width; if (position === 'left') { terminal = {x: center.x - offset - shortenLength, y: center.y + gap}; tempCenter = {x: center.x - offset, y: center.y + gap}; textCenter = {x: terminal.x - textWidth/2 - 10, y: terminal.y}; angle = Math.PI/2; } else if (position === 'right') { terminal = {x: center.x + offset + shortenLength, y: center.y + gap}; tempCenter = {x: center.x + offset, y: center.y + gap}; textCenter = {x: terminal.x + textWidth/2 + 10, y: terminal.y}; angle = Math.PI/2; } else if (position === 'top') { terminal = {x: center.x + gap, y: center.y - offset - shortenLength}; tempCenter = {x: center.x + gap, y: center.y - offset}; textCenter = {x: terminal.x, y: terminal.y - 10}; angle = 0; } else { terminal = {x: center.x + gap, y: center.y + offset + shortenLength}; tempCenter = {x: center.x + gap, y: center.y + offset}; textCenter = {x: terminal.x, y: terminal.y + 10}; angle = 0; } gap += 20; var isFrom = link.getFromNode() === this.getElement(), points; if (isFrom) { points = new twaver.List([tempCenter, terminal]); } else { points = new twaver.List([terminal, tempCenter]); } network.getElementUI(link)._paintBody(ctx, points, angle); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'black'; // 另一端节点标签 var name = isFrom ? link.getToNode().getName() : link.getFromNode().getName(); ctx.fillText(name, textCenter.x, textCenter.y); textCenter = {x: (tempCenter.x + terminal.x)/2, y: (tempCenter.y + terminal.y)/2}; // Link标签 ctx.fillText(link.getName(), textCenter.x, textCenter.y); // 画起始箭头 if (link.getClient('arrow.from')) { twaver.Util.drawArrow(ctx, 12, 9, points, true, 'arrow.standard', true, 'gray', 0, 0, 1, 'black'); } // 画结束箭头 if (link.getClient('arrow.to')) { twaver.Util.drawArrow(ctx, 12, 9, points, false, 'arrow.standard', true, 'gray', 0, 0, 1, 'black'); } } }, // 获取不同方位的上层链路集合 getAttachedLinks: function () { var currentSubNetwork = this.getNetwork().getCurrentSubNetwork(); if (!currentSubNetwork || !this.getElement().getLinks()) { return null; } var result; this.getElement().getLinks().forEach(function (link) { var fromSubNetwork = twaver.Util.getSubNetwork(link.getFromNode()), toSubNetwork = twaver.Util.getSubNetwork(link.getToNode()); if (fromSubNetwork !== toSubNetwork) { if (!result) { result = {}; } var fromCenter = link.getFromNode().getCenterLocation(), toCenter = link.getToNode().getCenterLocation(), angle = getAngle(fromCenter, toCenter), isOut = currentSubNetwork === fromSubNetwork, position; if (isOut) { if (fromCenter.x <= toCenter.x) { if (angle >= -Math.PI/4 && angle <= Math.PI/4) { position = 'right'; } else if (angle > Math.PI/4) { position = 'bottom'; } else { position = 'top'; } } else { if (angle >= -Math.PI/4 && angle <= Math.PI/4) { position = 'left'; } else if (angle > Math.PI/4) { position = 'top'; } else { position = 'bottom'; } } } else { if (fromCenter.x <= toCenter.x) { if (angle >= -Math.PI/4 && angle <= Math.PI/4) { position = 'left'; } else if (angle > Math.PI/4) { position = 'top'; } else { position = 'bottom'; } } else { if (angle >= -Math.PI/4 && angle <= Math.PI/4) { position = 'right'; } else if (angle > Math.PI/4) { position = 'bottom'; } else { position = 'top'; } } } if (!result[position]) { result[position] = []; } result[position].push(link); } }); return result; } });
本文完整代码见附件:见原文最下方