| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>Topology Viewer</title> |
| <script src="libs/d3.v3.min.js"></script> |
| <script src="libs/jquery-1.11.1.min.js"></script> |
| <style> |
| .link { |
| } |
| |
| .node { |
| stroke-width: 3px; |
| } |
| |
| .textClass { |
| stroke: #323232; |
| font-family: "Lucida Grande", "Droid Sans", Arial, Helvetica, sans-serif; |
| font-weight: normal; |
| stroke-width: .5; |
| font-size: 14px; |
| } |
| </style> |
| </head> |
| <body> |
| <script> |
| // Adapted from sample code @ ericcoopey’s block #6c602d7cb14b25c179a4 |
| // by Thomas Vachuska |
| |
| var graph, |
| topo = {vertexes:{}, edges:{}}, |
| paths = [], |
| currentPath = 0; |
| |
| // Set up the canvas |
| var w = 2048, |
| h = 2048; |
| |
| // Allocate colors predictably |
| var color = d3.scale.category10(), |
| deviceColor = color(2), |
| sourceColor = color(0), |
| targetColor = color(1), |
| dummy1Color = color(9), |
| dummy2Color = color(8), |
| dummy3Color = color(7), |
| dummy4Color = color(6), |
| offlineColor = color(5), |
| dummy5Color = color(4), |
| hostColor = color(3); |
| |
| var selectedNode, |
| sourceNode, |
| targetNode, |
| pathRequested; |
| |
| |
| function fillColor(d) { |
| return ((targetNode && targetNode.id == d.id) ? targetColor : |
| ((sourceNode && sourceNode.id == d.id) ? sourceColor : |
| d.online ? color(d.group) : offlineColor)); |
| } |
| |
| function strokeColor(d) { |
| return selectedNode && d.id == selectedNode.id ? "#f00" : "#aaa"; |
| } |
| |
| function linkColor(d) { |
| if (!paths || paths.length == 0) { |
| return "#666"; |
| } |
| |
| var path = paths[currentPath]; |
| if (path) { |
| for (var i = 0, n = path.length; i < n; i++) { |
| var link = path[i]; |
| if ((link.src == d.source.id || link.dst == d.source.id) && |
| (link.dst == d.target.id || link.src == d.target.id)) { |
| return "#f00"; |
| } |
| } |
| } |
| return "#666"; |
| } |
| |
| function linkKey(link) { |
| return link.source.id + "-" + link.target.id; |
| |
| }; |
| |
| function toggleNode(node) { |
| pathRequested = false; |
| return selectedNode && selectedNode != node ? selectedNode : null; |
| |
| }; |
| |
| function refreshPaths() { |
| d3.selectAll("line").attr("stroke", linkColor); |
| } |
| |
| function fetchPaths() { |
| if (!pathRequested && sourceNode && targetNode) { |
| pathRequested = true; |
| d3.json("rs/topology/paths/" + sourceNode.id + "/" + targetNode.id, function(error, data) { |
| currentPath = 0; |
| paths = data.paths; |
| refreshPaths(); |
| }); |
| } |
| } |
| |
| function resetSelections() { |
| selectedNode = null; |
| sourceNode = null; |
| targetNode = null; |
| paths = []; |
| currentPath = 0; |
| refreshPaths(); |
| d3.selectAll(".nodeStrokeClass").attr("stroke", strokeColor); |
| } |
| |
| function nextPath() { |
| currentPath = paths && currentPath < paths.length - 1 ? currentPath + 1 : 0 |
| console.log("Showing path: " + currentPath); |
| refreshPaths(); |
| } |
| |
| function dblclick(d) { |
| d3.select(this).classed("fixed", d.fixed = false); |
| } |
| |
| function dragstart(d) { |
| d3.select(this).classed("fixed", d.fixed = true); |
| } |
| |
| |
| function topoGraph() { |
| // Add and remove elements on the graph object |
| this.addNode = function (vertex, stamp) { |
| var node = topo.vertexes[vertex.name]; |
| if (node) { |
| var oldState = node.online; |
| node.online = vertex.online; |
| node.stamp = stamp; |
| if (oldState != node.online) { |
| update(); |
| return true; |
| } |
| return false; |
| } |
| node = {"id": vertex.name, "group": vertex.group, |
| "online": vertex.online, "stamp": stamp}; |
| nodes.push(node); |
| topo.vertexes[vertex.name] = node; |
| update(); |
| return true; |
| }; |
| |
| this.addLink = function (edge, stamp) { |
| var key = edge.source + "-" + edge.target; |
| var link = topo.edges[key]; |
| if (link) { |
| var oldValue = link.value; |
| link.value = edge.value; |
| link.stamp = stamp; |
| if (oldValue != link.value) { |
| update(); |
| return true; |
| } |
| return false; |
| } |
| link = {"source": findNode(edge.source), "target": findNode(edge.target), |
| "value": edge.value, "stamp": stamp}; |
| links.push(link); |
| topo.edges[key] = link; |
| update(); |
| return true; |
| }; |
| |
| this.prune = function (stamp) { |
| var linksChanged = pruneArray(links, stamp, topo.edges, linkKey); |
| var nodesChanged = pruneArray(nodes, stamp, topo.vertexes, |
| function(node) { return node.id; }); |
| if (linksChanged || nodesChanged) { |
| update(); |
| return true; |
| } |
| return false; |
| }; |
| |
| var pruneArray = function(array, stamp, map, key) { |
| var changed = false; |
| for (var i = 0; i < array.length; i++) { |
| if (array[i].stamp < stamp) { |
| changed = true; |
| map[key(array[i])] = null; |
| array.splice(i, 1); |
| i--; |
| } |
| } |
| return changed; |
| }; |
| |
| var findNode = function (id) { |
| for (var i in nodes) { |
| if (nodes[i]["id"] === id) return nodes[i]; |
| } |
| }; |
| |
| var force = d3.layout.force(); |
| |
| var drag = force.drag() |
| .on("dragstart", dragstart); |
| |
| var nodes = force.nodes(), |
| links = force.links(); |
| |
| var vis = d3.select("body") |
| .append("svg:svg") |
| .attr("width", w) |
| .attr("height", h) |
| .attr("id", "svg") |
| .attr("pointer-events", "all") |
| .attr("viewBox", "0 0 " + w + " " + h) |
| .attr("perserveAspectRatio", "xMinYMid") |
| .append('svg:g'); |
| |
| d3.select("body") |
| .on("keydown", function(d) { |
| console.log(d3.event.keyCode); |
| if (d3.event.keyCode == 27) { |
| resetSelections(); |
| } else if (d3.event.keyCode == 83) { |
| sourceNode = toggleNode(sourceNode); |
| } else if (d3.event.keyCode == 68) { |
| targetNode = toggleNode(targetNode); |
| } else if (d3.event.keyCode == 65) { |
| var aux = sourceNode; |
| sourceNode = targetNode; |
| targetNode = aux; |
| } else if (d3.event.keyCode == 70) { |
| nextPath(); |
| } |
| |
| d3.selectAll(".nodeStrokeClass").attr("fill", fillColor); |
| fetchPaths(); |
| }); |
| |
| |
| var update = function () { |
| var link = vis.selectAll("line") |
| .data(links, linkKey); |
| |
| link.enter().append("line") |
| .attr("id", linkKey) |
| .attr("class", "link") |
| .attr("stroke-width", function (d) { return d.value; }) |
| .attr("stroke", linkColor); |
| link.append("title").text(function (d) { return d.id; }); |
| link.exit().remove(); |
| |
| var node = vis.selectAll("g.node") |
| .data(nodes, function (d) { return d.id; }) |
| .on("click", function(d) { |
| selectedNode = d; |
| d3.selectAll(".nodeStrokeClass").attr("stroke", strokeColor); |
| }); |
| |
| var nodeEnter = node.enter().append("g") |
| .attr("class", "node") |
| .on("dblclick", dblclick) |
| .call(force.drag); |
| |
| nodeEnter.append("svg:circle") |
| .attr("r", function(d) { return 28 / 2; }) |
| .attr("id", function (d) { return "n-" + d.id.replace(/[.:]/g, ""); }) |
| .attr("class", "nodeStrokeClass") |
| .attr("fill", fillColor) |
| .attr("stroke", strokeColor); |
| |
| nodeEnter.append("image") |
| .attr("xlink:href", function(d) { return d.group == 2 ? "images/switch.png" : "images/server.png"; }) |
| .attr("x", -12) |
| .attr("y", -12) |
| .attr("width", 24) |
| .attr("height", 24); |
| |
| nodeEnter.append("svg:text") |
| .attr("class", "textClass") |
| .attr("x", 20) |
| .attr("y", ".31em") |
| .text(function (d) { return d.id; }); |
| |
| node.exit().remove(); |
| |
| d3.selectAll("nodeStrokeClass").attr("stroke", strokeColor); |
| |
| force.on("tick", function () { |
| node.attr("transform", function (d) { |
| return "translate(" + d.x + "," + d.y + ")"; |
| }); |
| |
| link.attr("x1", function (d) { return d.source.x; }) |
| .attr("y1", function (d) { return d.source.y; }) |
| .attr("x2", function (d) { return d.target.x; }) |
| .attr("y2", function (d) { return d.target.y; }); |
| }); |
| |
| // Restart the force layout. |
| force |
| .gravity(0.3) |
| .charge(-15000) |
| .friction(0.1) |
| .linkDistance(function(d) { return d.value * 30; }) |
| .linkStrength(function(d) { return d.value * 0.6; }) |
| .size([w, h]) |
| .start(); |
| }; |
| |
| // Make it all go |
| update(); |
| } |
| |
| function drawGraph() { |
| graph = new topoGraph("#svgdiv"); |
| bringNodesToFront(); |
| } |
| |
| function bringNodesToFront() { |
| $(".nodeStrokeClass").each(function( index ) { |
| var gnode = this.parentNode; |
| gnode.parentNode.appendChild(gnode); |
| }); |
| } |
| |
| function addNodes() { |
| d3.select("svg") |
| .remove(); |
| drawGraph(); |
| } |
| |
| function fetchData() { |
| var stamp = new Date().getTime(); |
| d3.json("rs/topology/graph", function(error, data) { |
| var changed = false; |
| data.vertexes.forEach(function(vertex) { changed = graph.addNode(vertex, stamp) || changed; }); |
| data.edges.forEach(function(edge) { changed = graph.addLink(edge, stamp) || changed; }); |
| |
| changed = graph.prune(stamp) || changed; |
| if (changed) { |
| bringNodesToFront(); |
| // Update node and links styles |
| d3.selectAll(".nodeStrokeClass").attr("fill", fillColor); |
| d3.selectAll(".line").attr("stroke-width", function (d) { return d.value; }) |
| } |
| |
| setTimeout(fetchData, 1000); |
| }); |
| }; |
| |
| drawGraph(); |
| setTimeout(fetchData, 500); |
| |
| </script> |
| </body> |
| </html> |