From 4b076ab3b9a6c87d22c00f2d1cb7354bd3d8561b Mon Sep 17 00:00:00 2001 From: Nils Schneider 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 " diff --git a/img/geometry2.png b/img/geometry2.png new file mode 100644 index 0000000000000000000000000000000000000000..d43966e05f943614a9b25746a660cf156f1a2f66 GIT binary patch literal 2138 zcmY+E2{aUp7RSf-QIXwv)_DwHqp?($FOFyUY-5HPBqKu}LQ|IF8-#2L&(cgLGKj`L zSw=M(RCv-%r1Xp>O!ZBwS(0tW5c7PU_d4Hq&$;K`bI(2J-t+sPdkcMiJe2n9><0h< zN^nnC6aXMq{)a2b>{V!Ip6>32W4_1$H}Tdsk1tqVTmQ2BbyF^4`e*8ED4$ZAN$EZBM#;PVyS?Ip%J=Jx|vS zbuzuY_4Chl*En~I-haD>%$ar*E_#IivJEmSoRm2V_b%#~3#>937anmvim-Pil$#jx zo7J313fGa7{iGU3k~*wta#Z?fJm|bulm9UY>OmN|wMt2if&KM)Ty#n#weBbkzr%zR zl+@t&>yJb9j~R;-p56}mk||W>lfkjH_AZjOlex%!xFN-dqA%5y7jbiGHBKCC*PX_Ol&NUu4z!_q9(BR{b~xa(J8x5b^O{cGHO95h zU$%R_<$Rs!AhnTWI`t;6KiPdmrzyJLYZm*m;{}B#=?9L|zqYyC@b>rJn>~@sud#Zi z6H}CL6eTi4Fw%w_hOrCv-0`yAga4Q5d#vWNnC=pXn<$ia=7Iv6xVh_jOJ7J)@qiv6*AvJsdz zp~^Xva;Gk@K1W!kW{|X4#(VL_&07{rFi@Q>qAa!S0i0O~`^PyvzUD9v9^x5X*I6Ko zB1bfv`E~9XkKu{op{mbJ8A0jY?hq|r0W6NlxgeQ2;22qiI&636bT3+dj9S{1*xY#O zi9H#fNDDdRR~H&}lIwkBCz`y>7iJ=fLsjaOt-*<9OAN$40#yEnqG zXDbLA$MV!X(8F2eT^8G(Tqt6x=3E#T>hD2%5~`^yx69j*R#5Q2?oyK2ggKz=I$CyIv zCd)p_eOrjULs-RwX68SsmPeep3dB$^&Pplss5vmezNeew4NuOT3kTSCRZoHsq z`=B~l&Z(tJ@}0P)v}l5L0fyHap|9B2txYy*XCp`9tidO`#TcOZ-?9~{-46{Xe9~JQ zXVS4y)?m|dEOaU2ME;2R=`mZ4fNkqPp#lIH<``yX$XzF-cYMPEmSMpJWNTJD%@(j! zP2q_Mp%K!1QjY>e(xmu)Oz_c{XU^IndXlp>8YAsoh$nec;x7zko3>#2%05S-{Qp!Ht+T=}L0^{vV| z4*OW-)PW;#nj@2IOrm~WmKkVnv8ztb7Vne>*4)8BN48+}SgokvI%J0{Z;v%$7Fd)3&-Kajzxsk9j{x>*0^`DY1M>VG|uhGOy zJ_&fHe+6aO-b4Q$QmCZCaH6+bwr}85K6cJy_~|jI`)9%pxo|0$07Y9xEb6eW;=@CX zGWC62N^$R;?=|v_mDNt>-<3?!m5mEmvyGy3<%cMd#?JRurT(J$6?6wyBY{rsgMkw8 zlF098R2lV`9z0c)nexKBF@UGImEYr}#)p=Fn_|mN`XFSNT|BeUR6MEX8f=$k5vVJl zJ%*iuKemZmI-&@K?#A;5G^6TZNNlkO|1_Bb#o=t+;8Xfp4XO6 z(rrRFBaLTPoisg}_&I|MPPNJ1v%q06#3#k0#^gg>*uEaJ`AkM5{KzbDi1KhQK409f nK1k$0#-e(%!?#|!{UE+8Ke%WWII*|&A0@b(k86iZc+P(TkKzc| 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"