| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| /** |
| * Code for drawing the connected circles that make up the logical plan for this topology. |
| * Exports a single global function drawLogicalPlan. |
| */ |
| (function (global) { |
| function htmlDecode(input){ |
| var e = document.createElement('div'); |
| e.innerHTML = input; |
| return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue; |
| } |
| |
| var sleep = function(delay) { |
| var start = new Date().getTime(); |
| while (new Date().getTime() < start + delay); |
| }; |
| |
| // Rearrage nodes in the groups so that |
| // the number of intersections of edges |
| // is minimized. |
| // Ideally, there should be no intersections, |
| // but if that is not achievable, rearrage |
| // so as to have minimum intersections. |
| var minimizeIntersections = function (groups) { |
| // First assign group level index. |
| // Also set the source group level indices. |
| for (var i = 0; i < groups.length; i++) { |
| for (var j = 0; j < groups[i].length; j++) { |
| groups[i][j].groupsLevelIndex = j; |
| for (var e = 0; e < groups[i][j].edges.length; e++) { |
| var node = groups[i][j].edges[e].target; |
| if (node.sourceGroupsLevelIndices === undefined) { |
| node.sourceGroupsLevelIndices = []; |
| } |
| node.sourceGroupsLevelIndices.push(j); |
| } |
| } |
| } |
| |
| var compare = function (a, b) { |
| var aIndices = a.sourceGroupsLevelIndices; |
| var bIndices = b.sourceGroupsLevelIndices; |
| var sumA = 0; |
| var sumB = 0; |
| for (var i = 0; i < aIndices.length; i++) { |
| sumA += aIndices[i]; |
| } |
| for (var i = 0; i < bIndices.length; i++) { |
| sumB += bIndices[i]; |
| } |
| var avgA = sumA / aIndices.length; |
| var avgB = sumB / bIndices.length; |
| |
| return avgA - avgB; |
| } |
| |
| // Sort individual groups based on avg sourceGroupsLevelIndices. |
| // Don't forget to update its own groupLevelIndex and |
| // next level nodes' sourceGroupsLevelIndices. |
| // Need to start from 1, since 0 level nodes won't have |
| // any sourceGroupsLevelIndices. |
| for (var i = 1; i < groups.length; i++) { |
| groups[i].sort(compare); |
| for (var j = 0; j < groups[i].length; j++) { |
| groups[i][j].groupsLevelIndex = j; |
| // Reset the next level's sourceGroupsLevelIndices. |
| for (var e = 0; e < groups[i][j].edges.length; e++) { |
| groups[i][j].edges[e].target.sourceGroupsLevelIndices = undefined; |
| } |
| } |
| for (var j = 0; j < groups[i].length; j++) { |
| groups[i][j].groupsLevelIndex = j; |
| // Now set those values again. |
| for (var e = 0; e < groups[i][j].edges.length; e++) { |
| var node = groups[i][j].edges[e].target; |
| if (node.sourceGroupsLevelIndices === undefined) { |
| node.sourceGroupsLevelIndices = []; |
| } |
| node.sourceGroupsLevelIndices.push(j); |
| } |
| } |
| } |
| return groups; |
| }; |
| |
| // Sort the graph topologically |
| // and also add virual nodes. |
| var groupify = _.memoize(function (nodes, links) { |
| |
| nodes.forEach(function (node) { |
| node.isReal = true; |
| }); |
| |
| var numNodes = nodes.length; |
| |
| var nodesToLinks = {}; |
| links.forEach(function (l) { |
| if (!(l.source.name in nodesToLinks)) { |
| nodesToLinks[l.source.name] = []; |
| } |
| nodesToLinks[l.source.name].push(l); |
| }); |
| |
| nodes.forEach(function (node) { |
| var nextLinks = nodesToLinks[node.name]; |
| if (nextLinks) { |
| node.edges = nodesToLinks[node.name]; |
| } else { |
| node.edges = []; |
| } |
| }); |
| |
| // Find the first elements |
| var group = nodes.filter(function (node) { |
| return links.every(function (link) { |
| return node !== link.target; |
| }); |
| }); |
| |
| var groupIndex = 0; |
| group.forEach(function (node) { |
| node.groupIndex = groupIndex; |
| }); |
| |
| groupIndex++; |
| var toProcess = []; |
| var nextLinks = group.map(function (node) { |
| return (nodesToLinks[node.name] === undefined) ? [] : nodesToLinks[node.name]; |
| }); |
| |
| nextLinks.forEach(function (arr) { |
| toProcess = toProcess.concat(arr); |
| }); |
| |
| while (toProcess.length > 0) { |
| // In case of circular graphs, this will prevent infinte loop |
| if (groupIndex > numNodes) { |
| break; |
| } |
| |
| group = toProcess.map(function (link) { |
| return link.target; |
| }); |
| |
| group.forEach(function (node) { |
| node.groupIndex = groupIndex; |
| }); |
| |
| nextLinks = group.map(function (node) { |
| if (node.name in nodesToLinks) { |
| return nodesToLinks[node.name]; |
| } else { |
| return []; |
| } |
| }); |
| |
| toProcess = []; |
| if (nextLinks.length > 0) { |
| nextLinks.forEach(function (arr) { |
| toProcess = toProcess.concat(arr); |
| }); |
| } |
| groupIndex++; |
| } |
| |
| var groups = Array.apply(null, new Array(groupIndex)).map(function (x) { return []; }); |
| |
| nodes.forEach(function (node) { |
| groups[node.groupIndex].push(node); |
| }); |
| |
| console.warn("done groupifying"); |
| console.warn("adding virtual nodes"); |
| |
| for (var i = 0; i < groups.length; i++) { |
| group = groups[i]; |
| group.forEach(function (node) { |
| |
| var edges = node.edges; |
| edges.forEach(function (edge) { |
| |
| var diff = edge.target.groupIndex - node.groupIndex; |
| if (diff > 1) { |
| |
| var newNode = { |
| "groupIndex": i + 1, |
| "edges": [], |
| "isReal": false |
| }; |
| |
| newNode.edges.push({ |
| "source": newNode, |
| "target": edge.target |
| }); |
| |
| edge.target = newNode; |
| groups[i+1].push(newNode); |
| } |
| }); |
| }); |
| } |
| |
| console.warn("done adding virtual nodes"); |
| var goodGroups = minimizeIntersections(groups); |
| return goodGroups; |
| }); |
| |
| // TODO: Too long function. Will Refactor. |
| function drawLogicalPlan(planController, topology, id, outerWidth, outerHeight, |
| cluster, environ, toponame) { |
| |
| id = id === undefined ? "#content" : id; |
| outerWidth = outerWidth === undefined ? 1000 : outerWidth; |
| outerHeight = outerHeight === undefined ? 300 : outerHeight; |
| |
| var padding = { |
| top: 50, |
| left: 30, |
| right: 30, |
| bottom: 50 |
| }; |
| |
| var height = outerHeight - padding.top - padding.bottom; |
| var width = outerWidth - padding.left - padding.right; |
| |
| // create the svg |
| var outerSvg = d3.select(id).text("") |
| .append("svg") |
| .attr("width", outerWidth) |
| .attr("height", outerHeight) |
| .attr("id", "topology"); |
| var svg = outerSvg.append("g") |
| .attr("tranform", "translate(" + padding.left + "," + padding.top + ")"); |
| |
| spoutsArr = []; |
| boltsArr = []; |
| |
| // create the spout array |
| for (var i in topology.spouts) { |
| spoutsArr.push({ |
| "name": i |
| }); |
| } |
| |
| // create the bolt array |
| for (var i in topology.bolts) { |
| boltsArr.push({ |
| "name": i, |
| "inputComponents": topology.bolts[i]["inputComponents"], |
| "inputStreams": topology.bolts[i]["inputs"] |
| }); |
| } |
| |
| var nodes = spoutsArr.concat(boltsArr) |
| var links = []; |
| |
| for (var b in boltsArr) { |
| for (w in nodes) { |
| if (boltsArr[b].inputComponents.indexOf(nodes[w].name) >= 0) { |
| // Found that node[w] is upstream of boltsArr[b], build a link |
| var streams = [] |
| for (i in boltsArr[b].inputComponents) { |
| if (boltsArr[b].inputComponents[i] == nodes[w].name) { |
| streams.push(boltsArr[b].inputStreams[i].stream_name |
| + ":" + boltsArr[b].inputStreams[i].grouping); |
| } |
| } |
| links.push({ |
| "source": nodes[w], |
| "target": boltsArr[b], |
| "streams": streams.sort().join("<br>") |
| }); |
| } |
| } |
| } |
| |
| var sptblt = Object.keys(topology.spouts) |
| .concat(Object.keys(topology.bolts)); |
| |
| var color = d3.scale.ordinal().domain(sptblt.sort()).range(colors); |
| |
| // Groupify |
| var groups = groupify(nodes, links); |
| |
| // Done groupifying |
| nodes = []; |
| links = []; |
| groups.forEach(function (group) { |
| group.forEach(function (node) { |
| nodes.push(node); |
| node.edges.forEach(function (edge) { |
| links.push(edge); |
| }); |
| }); |
| }); |
| |
| // layout using force directed graph |
| var force = d3.layout.force() |
| .gravity(0.05) |
| .distance(100) |
| .charge(-1000) |
| .size([width, height]); |
| |
| force.nodes(nodes) |
| .links(links) |
| .linkDistance(200) |
| .start(); |
| |
| hOffset = 1.0 / (groups.length + 1); |
| |
| nodes.forEach(function (node) { |
| var x = hOffset * (node.groupIndex + 1); |
| var group = groups[node.groupIndex]; |
| var vOffset = 1.0 / (group.length + 1); |
| var y = vOffset * (group.indexOf(node) + 1); |
| // Interpolate it with lesser width |
| // to account for nodes that are |
| // in the extreme right. Otherwise, those |
| // nodes would sometimes not respond to |
| // hover function. |
| node.x = d3.interpolate(0, width)(x); |
| node.y = d3.interpolate(0, height)(y); |
| }); |
| |
| var yRange = d3.extent(nodes, function (d) { return d.y; }); |
| outerSvg.attr('height', (yRange[1] - yRange[0]) + padding.top + padding.bottom); |
| svg.attr('transform', 'translate(' + padding.left + ',' + (padding.top - yRange[0]) + ')') |
| |
| var connection_tip = d3.tip() |
| .attr('class', 'd3-tip main text-center connection') |
| .offset([10, 0]) |
| .direction('s') |
| .html(function (d) { |
| return d.streams; |
| }); |
| |
| var node = svg.selectAll(".topnode") |
| .data(nodes) |
| .enter() |
| .append("g") |
| .attr("class", "topnode") |
| .style("fill", "black"); |
| |
| // Links |
| node.each(function (n) { |
| d3.select(this) |
| .selectAll(".link") |
| .data(n.edges) |
| .enter() |
| .append("path") |
| .attr('class', 'link') |
| .attr("stroke-width", linestyle.boldwidth) |
| .attr("stroke", linestyle.color) |
| .attr("fill", "none") |
| .attr("d", function (edge) { |
| var p0 = edge.source; |
| var p3 = edge.target; |
| var m = (p0.x + p3.x) / 2; |
| var p = [p0, {x: m, y: p0.y}, {x: m, y: p3.y}, p3]; |
| return "M" + p[0].x + " " + p[0].y + |
| "C" + p[1].x + " " + p[1].y + |
| " " + p[2].x + " " + p[2].y + |
| " " + p[3].x + " " + p[3].y; |
| }) |
| .on('mouseover', connection_tip.show) |
| .on('mouseout', connection_tip.hide); |
| }); |
| |
| // Component |
| node.append("circle") |
| .attr('class', 'background') |
| .attr("cx", function (d) { return d.x; }) |
| .attr("cy", function (d) { return d.y; }) |
| .attr("r", function (d) { |
| if (d.isReal) { |
| return d.r = 17; |
| } |
| return d.r = 0; |
| }) |
| .style('fill', 'white'); |
| |
| node.append("circle") |
| .attr("class", "node") |
| .attr("cx", function (d) { return d.cx = d.x; }) |
| .attr("cy", function (d) { return d.cy = d.y; }) |
| .attr("r", function (d) { |
| if (d.isReal) { |
| return d.r = 15; |
| } |
| return d.r = 0; |
| }) |
| .style("stroke", linestyle.color) |
| .style("stroke-width", linestyle.width) |
| .style('fill', function (d) { |
| d.defaultColor = color(d.name); |
| d.color = d.color || d.defaultColor; |
| return d.color; |
| }) |
| .on("click", planController.logicalComponentClicked) |
| .on("dblclick", planController.logicalComponentClicked) |
| .on("mouseover", planController.logicalComponentHoverOver) |
| .on("mouseout", planController.logicalComponentHoverOut); |
| |
| // Component name |
| node.append("text") |
| .attr("id", function(d) { return "text+" + d.name; }) |
| .attr("x", function (d) { return d.cx; }) |
| .attr("y", function (d) { return d.cy - d.r - 10; }) |
| .attr("class", "fade") |
| .style("text-anchor", "middle") |
| .style("user-select", "all") |
| .text(function (d) { |
| if (d.isReal) { |
| return d.name; |
| } |
| return ""; |
| }); |
| |
| |
| svg.call(connection_tip); |
| } |
| |
| global.drawLogicalPlan = drawLogicalPlan; |
| }(this)); |