UNPKG

juijs-chart

Version:

SVG-based JUI chart that can be used in the browser and Node.js. Support many types of charts. (Dashboard, Map, Topology, Full 3D)

712 lines (586 loc) 27.1 kB
import jui from '../main.js'; jui.define("chart.topology.edge", [], function() { /** * @class chart.topology.edge * */ var TopologyEdge = function(start, end, in_xy, out_xy, scale) { var connect = false, element = null; this.key = function() { return start + ":" + end; } this.reverseKey = function() { return end + ":" + start; } this.connect = function(is) { if(arguments.length == 0) { return connect; } connect = is; } this.element = function(elem) { if(arguments.length == 0) { return element; } element = elem; } this.set = function(type, value) { if(type == "start") start = value; else if(type == "end") end = value; else if(type == "in_xy") in_xy = value; else if(type == "out_xy") out_xy = value; else if(type == "scale") scale = value; } this.get = function(type) { if(type == "start") return start; else if(type == "end") return end; else if(type == "in_xy") return in_xy; else if(type == "out_xy") return out_xy; else if(type == "scale") return scale; } } return TopologyEdge; }); jui.define("chart.topology.edgemanager", [ "util.base" ], function(_) { /** * @class chart.topology.edgemanager * */ var TopologyEdgeManager = function() { var list = [], cache = {}; this.add = function(edge) { cache[edge.key()] = edge; list.push(edge); } this.get = function(key) { return cache[key]; } this.is = function(key) { return (cache[key]) ? true : false; } this.list = function() { return list; } this.each = function(callback) { if(!_.typeCheck("function", callback)) return; for(var i = 0; i < list.length; i++) { callback.call(this, list[i]); } } } return TopologyEdgeManager; }); export default { name: "chart.brush.topologynode", extend: "chart.brush.core", component: function () { const _ = jui.include("util.base"); const math = jui.include("util.math"); const Edge = jui.include("chart.topology.edge"); const EdgeManager = jui.include("chart.topology.edgemanager"); const TopologyNode = function() { var self = this, edges = new EdgeManager(), g, tooltip, point, textY = 14, padding = 7, anchor = 7, activeEdges = []; // 선택된 엣지 객체 function getDistanceXY(x1, y1, x2, y2, dist) { var a = x1 - x2, b = y1 - y2, c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)), dist = (!dist) ? 0 : dist, angle = math.angle(x1, y1, x2, y2); return { x: x1 + Math.cos(angle) * (c + dist), y: y1 + Math.sin(angle) * (c + dist), angle: angle, distance: c } } function getNodeData(key) { for(var i = 0; i < self.axis.data.length; i++) { var d = self.axis.data[i], k = self.getValue(d, "key"); if(k == key) { return self.axis.data[i]; } } return null; } function getEdgeData(key) { for(var i = 0; i < self.brush.edgeData.length; i++) { if(self.brush.edgeData[i].key == key) { return self.brush.edgeData[i]; } } return null; } function getTooltipData(edge) { for(var j = 0; j < self.brush.edgeData.length; j++) { if(edge.key() == self.brush.edgeData[j].key) { return self.brush.edgeData[j]; } } return null; } function getTooltipTitle(key) { var names = [], keys = key.split(":"); self.eachData(function(data, i) { var title = _.typeCheck("function", self.brush.nodeTitle) ? self.brush.nodeTitle.call(self.chart, data) : ""; if(data.key == keys[0]) { names[0] = title || data.key; } if(data.key == keys[1]) { names[1] = title || data.key; } }); if(names.length > 0) return names; return key; } function getNodeRadius(data) { var r = self.chart.theme("topologyNodeRadius"), scale = 1; if(_.typeCheck("function", self.brush.nodeScale) && data) { scale = self.brush.nodeScale.call(self.chart, data); r = r * scale; } return { r: r, scale: scale } } function getEdgeOpacity(data) { var opacity = self.chart.theme("topologyEdgeOpacity"); if(_.typeCheck("function", self.brush.edgeOpacity) && data) { opacity = self.brush.edgeOpacity.call(self.chart, data); } return opacity; } function createNodes(index, data) { var key = self.getValue(data, "key"), xy = self.axis.c(index), color = self.color(index, 0), title = _.typeCheck("function", self.brush.nodeTitle) ? self.brush.nodeTitle.call(self.chart, data) : "", text =_.typeCheck("function", self.brush.nodeText) ? self.brush.nodeText.call(self.chart, data) : "", size = getNodeRadius(data); var node = self.svg.group({ index: index }, function() { if(_.typeCheck("function", self.brush.nodeImage)) { self.svg.image({ "xlink:href": self.brush.nodeImage.call(self.chart, data), width: (size.r * 2) * xy.scale, height: (size.r * 2) * xy.scale, x: -size.r, y: -size.r, cursor: "pointer" }); } else { self.svg.circle({ "class": "circle", r: size.r * xy.scale, fill: color, cursor: "pointer" }); } if(text && text != "") { var fontSize = self.chart.theme("topologyNodeFontSize"); self.chart.text({ "class": "text", x: 0.1 * xy.scale, y: (size.r / 2) * xy.scale, fill: self.chart.theme("topologyNodeFontColor"), "font-size": fontSize * size.scale * xy.scale, "text-anchor": "middle", cursor: "pointer" }, text); } if(title && title != "") { self.chart.text({ "class": "title", x: 0.1 * xy.scale, y: (size.r + 13) * xy.scale, fill: self.chart.theme("topologyNodeTitleFontColor"), "font-size": self.chart.theme("topologyNodeTitleFontSize") * xy.scale, "font-weight": "bold", "text-anchor": "middle", cursor: "pointer" }, title); } }).translate(xy.x, xy.y); node.on(self.brush.activeEvent, function(e) { onNodeActiveHandler(data); self.chart.emit("topology.nodeclick", [ data, e ]); }); // 맨 앞에 배치할 노드 체크 if(self.axis.cache.nodeKey == key) { node.order = 1; } // 노드에 공통 이벤트 설정 self.addEvent(node, index, null); return node; } function createEdges() { edges.each(function(edge) { var in_xy = edge.get("in_xy"), out_xy = edge.get("out_xy"); var node = self.svg.group(); node.append(createEdgeLine(edge, in_xy, out_xy)); node.append(createEdgeText(edge, in_xy, out_xy)); g.append(node); }); } function createEdgeLine(edge, in_xy, out_xy) { var g = self.svg.group(), size = self.chart.theme("topologyEdgeWidth"), opacity = getEdgeOpacity(getEdgeData(edge.key())); if(!edge.connect()) { g.append(self.svg.line({ cursor: "pointer", x1: in_xy.x, y1: in_xy.y, x2: out_xy.x, y2: out_xy.y, stroke: self.chart.theme("topologyEdgeColor"), "stroke-width": size * edge.get("scale"), "stroke-opacity": opacity, "shape-rendering": "geometricPrecision" })); } else { var reverseElem = edges.get(edge.reverseKey()).element(); reverseElem.get(0).attr({ "stroke-opacity": opacity }); reverseElem.get(1).attr({ "fill-opacity": opacity }); } g.append(self.svg.circle({ fill: self.chart.theme("topologyEdgeColor"), "fill-opacity": opacity, stroke: self.chart.theme("backgroundColor"), "stroke-width": (size * 2) * edge.get("scale"), r: point * edge.get("scale"), cx: out_xy.x, cy: out_xy.y })); g.on(self.brush.activeEvent, function(e) { onEdgeActiveHandler(edge); }); g.on("mouseover", function(e) { onEdgeMouseOverHandler(edge); }); g.on("mouseout", function(e) { onEdgeMouseOutHandler(edge); }); edge.element(g); return g; } function createEdgeText(edge, in_xy, out_xy) { var text = null; var edgeAlign = (out_xy.x > in_xy.x) ? "end" : "start", edgeData = getEdgeData(edge.key()); if(edgeData != null) { var edgeText = _.typeCheck("function", self.brush.edgeText) ? self.brush.edgeText.call(self.chart, edgeData, edgeAlign) : null; if (edgeText != null) { if (edgeAlign == "end") { text = self.svg.text({ x: out_xy.x - 9, y: out_xy.y + 13, cursor: "pointer", fill: self.chart.theme("topologyEdgeFontColor"), "font-size": self.chart.theme("topologyEdgeFontSize") * edge.get("scale"), "text-anchor": edgeAlign }, edgeText) .rotate(math.degree(out_xy.angle), out_xy.x, out_xy.y); } else { text = self.svg.text({ x: out_xy.x + 8, y: out_xy.y - 7, cursor: "pointer", fill: self.chart.theme("topologyEdgeFontColor"), "font-size": self.chart.theme("topologyEdgeFontSize") * edge.get("scale"), "text-anchor": edgeAlign }, edgeText) .rotate(math.degree(in_xy.angle), out_xy.x, out_xy.y); } text.on(self.brush.activeEvent, function (e) { onEdgeActiveHandler(edge); }); text.on("mouseover", function (e) { onEdgeMouseOverHandler(edge); }); text.on("mouseout", function (e) { onEdgeMouseOutHandler(edge); }); } } return text; } function setDataEdges(index, targetIndex) { var data = self.getData(index), key = self.getValue(data, "key"), targetKey = self.getValue(data, "outgoing", [])[targetIndex]; // 자신의 키와 동일한지 체크 if(key == targetKey) return; var targetData = getNodeData(targetKey), target = self.axis.c(targetKey), xy = self.axis.c(index), in_dist = (getNodeRadius(data).r + point + 1) * xy.scale, out_dist = (getNodeRadius(targetData).r + point + 1) * xy.scale, in_xy = getDistanceXY(target.x, target.y, xy.x, xy.y, -in_dist), out_xy = getDistanceXY(xy.x, xy.y, target.x, target.y, -out_dist), edge = new Edge(key, targetKey, in_xy, out_xy, xy.scale); if(edges.is(edge.reverseKey())) { edge.connect(true); } edges.add(edge); } function showTooltip(edge, e) { if(!_.typeCheck("function", self.brush.tooltipTitle) || !_.typeCheck("function", self.brush.tooltipText)) return; var rect = tooltip.get(0), text = tooltip.get(1); // 텍스트 초기화 rect.attr({ points: "" }); text.element.textContent = ""; var edge_data = getTooltipData(edge), in_xy = edge.get("in_xy"), out_xy = edge.get("out_xy"), align = (out_xy.x > in_xy.x) ? "end" : "start"; // 커스텀 이벤트 발생 self.chart.emit("topology.edgeclick", [ edge_data, e ]); if(edge_data != null) { // 엘리먼트 생성 및 추가 var title = document.createElementNS("http://www.w3.org/2000/svg", "tspan"), contents = document.createElementNS("http://www.w3.org/2000/svg", "tspan"), y = (padding * 2) + ((align == "end") ? anchor : 0); text.element.appendChild(title); text.element.appendChild(contents); title.setAttribute("x", padding); title.setAttribute("y", y); title.setAttribute("font-weight", "bold"); title.textContent = self.brush.tooltipTitle.call(self.chart, getTooltipTitle(edge_data.key), align); contents.setAttribute("x", padding); contents.setAttribute("y", y + textY + (padding / 2)); contents.textContent = self.brush.tooltipText.call(self.chart, edge_data, align); // 엘리먼트 위치 설정 var size = text.size(), w = size.width + padding * 2, h = size.height + padding * 2, x = out_xy.x - (w / 2) + (anchor / 2) + (point / 2); text.attr({ x: w / 2 }); rect.attr({ points: self.balloonPoints((align == "end") ? "bottom" : "top", w, h, anchor) }); tooltip.attr({ visibility: "visible" }); if(align == "end") { tooltip.translate(x, out_xy.y + (anchor / 2) + point); } else { tooltip.translate(x, out_xy.y - anchor - h + point); } } } function onNodeActiveHandler(data) { var color = self.chart.theme("topologyEdgeColor"), activeColor = self.chart.theme("topologyActiveEdgeColor"), size = self.chart.theme("topologyEdgeWidth"), activeSize = self.chart.theme("topologyActiveEdgeWidth"); activeEdges = []; for(var i = 0; i < data.outgoing.length; i++) { var key = data.key + ":" + data.outgoing[i], edge = edges.get(key); if(edge != null) { activeEdges.push(edge); if (edge.connect()) { // 같이 연결된 노드도 추가 activeEdges.push(edges.get(edge.reverseKey())); } } } edges.each(function(edge) { var elem = edge.element(), circle = (elem.children.length == 2) ? elem.get(1) : elem.get(0), line = (elem.children.length == 2) ? elem.get(0) : null; if(_.inArray(edge, activeEdges) != -1) { // 연결된 엣지 var lineAttr = { stroke: activeColor, "stroke-width": activeSize * edge.get("scale") }, circleAttr = { fill: activeColor }; if(line != null) { line.attr(lineAttr); } circle.attr(circleAttr); tooltip.attr({ visibility: "hidden" }); } else { // 연결되지 않은 엣지 if(line != null) { line.attr({ stroke: color, "stroke-width": size * edge.get("scale") }); } circle.attr({ fill: color }); } }); } function onEdgeActiveHandler(edge) { edges.each(function(newEdge) { var elem = newEdge.element(), circle = (elem.children.length == 2) ? elem.get(1) : elem.get(0), line = (elem.children.length == 2) ? elem.get(0) : null, color = self.chart.theme("topologyEdgeColor"), activeColor = self.chart.theme("topologyActiveEdgeColor"), size = self.chart.theme("topologyEdgeWidth"), activeSize = self.chart.theme("topologyActiveEdgeWidth"); if(edge != null && (edge.key() == newEdge.key() || edge.reverseKey() == newEdge.key())) { if(line != null) { line.attr({ stroke: activeColor, "stroke-width": activeSize * newEdge.get("scale") }); } circle.attr({ fill: activeColor }); // 툴팁에 보여지는 데이터 설정 if(edge.key() == newEdge.key()) { // 엣지 툴팁 보이기 showTooltip(edge); } activeEdges = [ edge ]; if(edge.connect()) { // 같이 연결된 노드도 추가 activeEdges.push(edges.get(edge.reverseKey())); } } else { if(line != null) { line.attr({ stroke: color, "stroke-width": size * newEdge.get("scale") }); } circle.attr({ fill: color }); } }); } function onEdgeMouseOverHandler(edge) { if(_.inArray(edge, activeEdges) != -1) return; var elem = edge.element(), circle = (elem.children.length == 2) ? elem.get(1) : elem.get(0), line = (elem.children.length == 2) ? elem.get(0) : null, color = self.chart.theme("topologyHoverEdgeColor"), size = self.chart.theme("topologyHoverEdgeWidth"); if(line != null) { line.attr({ stroke: color, "stroke-width": size * edge.get("scale") }); } circle.attr({ fill: color }); } function onEdgeMouseOutHandler(edge) { if(_.inArray(edge, activeEdges) != -1) return; var elem = edge.element(), circle = (elem.children.length == 2) ? elem.get(1) : elem.get(0), line = (elem.children.length == 2) ? elem.get(0) : null, color = self.chart.theme("topologyEdgeColor"), size = self.chart.theme("topologyEdgeWidth"); if(line != null) { line.attr({ stroke: color, "stroke-width": size * edge.get("scale") }); } circle.attr({ fill: color }); } this.drawBefore = function() { g = self.svg.group(); point = self.chart.theme("topologyEdgePointRadius"); tooltip = self.svg.group({ visibility: "hidden" }, function() { self.svg.polygon({ fill: self.chart.theme("topologyTooltipBackgroundColor"), stroke: self.chart.theme("topologyTooltipBorderColor"), "stroke-width": 1 }); self.chart.text({ "font-size": self.chart.theme("topologyTooltipFontSize"), "fill": self.chart.theme("topologyTooltipFontColor"), y: textY }); }); } this.draw = function() { var nodes = []; this.eachData(function(data, i) { for(var j = 0; j < data.outgoing.length; j++) { setDataEdges(i, j); } }); // 엣지 그리기 createEdges(); // 노드 그리기 this.eachData(function(data, i) { var node = createNodes(i, data); g.append(node); nodes[i] = { node: node, data: data }; }); // 툴팁 숨기기 이벤트 (차트 배경 클릭시) this.on("axis.mousedown", function(e) { if(self.axis.root.element == e.target) { onEdgeActiveHandler(null); tooltip.attr({ visibility: "hidden" }); } }); // 액티브 엣지 선택 (렌더링 이후에 설정) if(_.typeCheck("string", self.brush.activeEdge)) { this.on("render", function(init) { if(!init) { var edge = edges.get(self.brush.activeEdge); onEdgeActiveHandler(edge); } }); } // 액티브 노드 선택 (렌더링 이후에 설정) if(_.typeCheck("string", self.brush.activeNode)) { this.on("render", function(init) { if(!init) { onNodeActiveHandler(getNodeData(self.brush.activeNode)); } }); } return g; } } TopologyNode.setup = function() { return { /** @cfg {Boolean} [clip=true] If the brush is drawn outside of the chart, cut the area. */ clip: true, // topology options /** @cfg {Function} [nodeTitle=null] */ nodeTitle: null, /** @cfg {Function} [nodeText=null] */ nodeText: null, /** @cfg {Function} [nodeImage=null] */ nodeImage: null, /** @cfg {Function} [nodeScale=null] */ nodeScale: null, /** @cfg {Array} [edgeData=[]] */ edgeData: [], /** @cfg {String} [edgeText=null] */ edgeText: null, /** @cfg {Function} [edgeText=null] */ edgeOpacity: null, /** @cfg {Function} [tooltipTitle=null] */ tooltipTitle: null, /** @cfg {Function} [tooltipText=null] */ tooltipText: null, /** @cfg {String} [activeNode=null] */ activeNode: null, /** @cfg {String} [activeEdge=null] */ activeEdge: null, /** @cfg {String} [activeEvent="click"] */ activeEvent: "click" } } /** * @event topoloygy_nodeclick * Event that occurs when click on the topology node. (real name ``` topoloygy.nodeclick ```) * @param {Object} data The node data. * @param {jQueryEvent} e The event object. */ /** * @event topoloygy_edgeclick * Event that occurs when click on the topology edge. (real name ``` topoloygy.edgeclick ```) * @param {Object} data The edge data. * @param {jQueryEvent} e The event object. */ return TopologyNode; } }