diff --git a/app.js b/app.js index 88f666b..d5e5602 100644 --- a/app.js +++ b/app.js @@ -7,6 +7,7 @@ require.config({ "moment": "../bower_components/moment/min/moment-with-locales.min", "tablesort": "../bower_components/tablesort/tablesort.min", "tablesort.numeric": "../bower_components/tablesort/src/sorts/tablesort.numeric", + "d3": "../bower_components/d3/d3.min", "helper": "../helper" }, shim: { diff --git a/bower.json b/bower.json index 0c3b47e..01cbcd2 100644 --- a/bower.json +++ b/bower.json @@ -19,7 +19,8 @@ "roboto-slab-fontface": "*", "es6-shim": "~0.27.1", "almond": "~0.3.1", - "r.js": "~2.1.16" + "r.js": "~2.1.16", + "d3": "~3.5.5" }, "authors": [ "Nils Schneider " diff --git a/img/geometry2.png b/img/geometry2.png new file mode 100644 index 0000000..d43966e Binary files /dev/null and b/img/geometry2.png differ diff --git a/lib/forcegraph.js b/lib/forcegraph.js new file mode 100644 index 0000000..681ecec --- /dev/null +++ b/lib/forcegraph.js @@ -0,0 +1,211 @@ +// TODO +// - window size +// - avoid sidebar +// - pan to node +// - pan and zoom to link +define(["d3"], function (d3) { + return function (linkScale, sidebar, router) { + var self = this + var vis, link, node, label + var nodesDict, linksDict + var force + + function nodeName(d) { + if (d.node && d.node.nodeinfo) + return d.node.nodeinfo.hostname + else + return d.id + } + + function dragstart(d) { + d3.event.sourceEvent.stopPropagation() + d.fixed |= 2 + } + + function dragmove(d) { + d.px = d3.event.x + d.py = d3.event.y + force.resume() + } + + function dragend(d) { + d3.event.sourceEvent.stopPropagation() + d.fixed &= 1 + } + + function panzoom() { + vis.attr("transform", + "translate(" + d3.event.translate + ") " + + "scale(" + d3.event.scale + ")") + } + + function tickEvent() { + link.selectAll("line") + .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 }) + + node + .attr("cx", function(d) { return d.x }) + .attr("cy", function(d) { return d.y }) + + label.attr("transform", function(d) { + return "translate(" + d.x + "," + d.y + ")" + }) + } + + var el = document.createElement("div") + el.classList.add("graph") + self.div = el + + vis = d3.select(el).append("svg") + .attr("pointer-events", "all") + .call(d3.behavior.zoom().on("zoom", panzoom)) + .append("g") + + vis.append("g").attr("class", "links") + vis.append("g").attr("class", "nodes") + vis.append("g").attr("class", "labels").attr("pointer-events", "none") + + force = d3.layout.force() + .size([500, 500]) + .charge(-100) + .gravity(0.05) + .friction(0.73) + .theta(0.8) + .linkDistance(70) + .linkStrength(0.2) + .on("tick", tickEvent) + + var draggableNode = d3.behavior.drag() + .on("dragstart", dragstart) + .on("drag", dragmove) + .on("dragend", dragend) + + self.setData = function (data) { + var links = data.graph.links.filter( function (d) { + return !d.vpn + }) + + link = vis.select("g.links") + .selectAll("g.link") + .data(links, linkId) + + var linkEnter = link.enter().append("g") + .attr("class", "link") + .on("click", function (d) { + if (!d3.event.defaultPrevented) + router.link(d)() + }) + + linkEnter.append("line") + .append("title") + + link.selectAll("line") + .style("stroke", function (d) { return linkScale(d.tq) }) + + link.selectAll("title").text(showTq) + + linksDict = {} + + link.each( function (d) { + if (d.source.node && d.target.node) + linksDict[linkId(d)] = this + }) + + var nodes = data.graph.nodes + + node = vis.select("g.nodes") + .selectAll(".node") + .data(nodes, + function(d) { + return d.id + } + ) + + var nodeEnter = node.enter().append("circle") + .attr("r", 8) + .on("click", function (d) { + if (!d3.event.defaultPrevented) + router.node(d.node)() + }) + .call(draggableNode) + + node.attr("class", function (d) { + var s = ["node"] + if (!d.node) + s.push("unknown") + + return s.join(" ") + }) + + nodesDict = {} + + node.each( function (d) { + if (d.node) + nodesDict[d.node.nodeinfo.node_id] = this + }) + + label = vis.select("g.labels") + .selectAll("g.label") + .data(data.graph.nodes, function (d) { + return d.id + }) + + var labelEnter = label.enter() + .append("g") + .attr("class", "label") + + labelEnter.append("path").attr("class", "clients") + + labelEnter.append("text") + .attr("class", "name") + .attr("text-anchor", "middle") + .attr("y", "21px") + .attr("x", "0px") + + label.selectAll("text.name").text(nodeName) + + var labelTextWidth = function (e) { + return e.parentNode.querySelector("text").getBBox().width + 3 + } + + labelEnter.insert("rect", "text") + .attr("y", "10px") + .attr("x", function() { return labelTextWidth(this) / (-2)}) + .attr("width", function() { return labelTextWidth(this)}) + .attr("height", "15px") + + nodeEnter.append("title") + + node.selectAll("title").text(nodeName) + + force.nodes(nodes) + .links(links) + .alpha(0.1) + .start() + } + + self.resetView = function () { + node.classed("highlight", false) + link.classed("highlight", false) + } + + self.gotoNode = function (d) { + link.classed("highlight", false) + node.classed("highlight", function (e) { + return e.node === d && d !== undefined + }) + } + + self.gotoLink = function (d) { + node.classed("highlight", false) + link.classed("highlight", function (e) { + return e === d && d !== undefined + }) + } + + return self + } +}) diff --git a/scss/_forcegraph.scss b/scss/_forcegraph.scss new file mode 100644 index 0000000..29e7678 --- /dev/null +++ b/scss/_forcegraph.scss @@ -0,0 +1,57 @@ +.graph { + height: 100vh; + background: url(img/geometry2.png); + + svg { + display: block; + width: 100%; + height: 100%; + } + + .link { + stroke-opacity: 0.8; + + line { + stroke-width: 2.5px; + cursor: pointer; + } + + &.highlight line { + stroke-width: 7px; + stroke-dasharray: 10, 10; + opacity: 1; + } + } + + .node { + fill: #fff; + stroke-width: 2.5px; + stroke: #AEC7E8; + + &:not(.unknown) { + cursor: pointer; + } + + &.highlight { + stroke: #FFD486; + stroke-width: 6px; + fill: orange; + point-order: stroke; + } + + &.unknown { + stroke: #d00000; + } + } + + .label { + text { + font-size: 0.8em; + } + + rect { + fill: rgba(255, 255, 255, 0.8); + } + } +} + diff --git a/scss/main.scss b/scss/main.scss index 0589d67..178edc8 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -1,5 +1,6 @@ @import '_leaflet'; @import '_leaflet.label'; +@import '_forcegraph'; .stroke-first { paint-order: stroke; diff --git a/tasks/build.js b/tasks/build.js index 1846ec7..cfa89a7 100644 --- a/tasks/build.js +++ b/tasks/build.js @@ -7,6 +7,11 @@ module.exports = function(grunt) { cwd: "html/", dest: "build/" }, + img: { + src: ["img/*"], + expand: true, + dest: "build/" + }, vendorjs: { src: [ "es6-shim/es6-shim.min.js", "intl/Intl.complete.js"