From 4b076ab3b9a6c87d22c00f2d1cb7354bd3d8561b Mon Sep 17 00:00:00 2001 From: Nils Schneider <nils@nilsschneider.net> Date: Tue, 31 Mar 2015 17:22:36 +0200 Subject: [PATCH] add forcegraph --- app.js | 1 + bower.json | 3 +- img/geometry2.png | Bin 0 -> 2138 bytes lib/forcegraph.js | 211 ++++++++++++++++++++++++++++++++++++++++++ scss/_forcegraph.scss | 57 ++++++++++++ scss/main.scss | 1 + tasks/build.js | 5 + 7 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 img/geometry2.png create mode 100644 lib/forcegraph.js create mode 100644 scss/_forcegraph.scss 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 <nils@nilsschneider.net>" diff --git a/img/geometry2.png b/img/geometry2.png new file mode 100644 index 0000000000000000000000000000000000000000..d43966e05f943614a9b25746a660cf156f1a2f66 GIT binary patch literal 2138 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 literal 0 HcmV?d00001 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" -- GitLab