forcegraph.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. define(["d3"], function (d3) {
  2. var margin = 200
  3. return function (config, linkScale, sidebar, router) {
  4. var self = this
  5. var svg, canvas, ctx, screenRect
  6. var svgNodes, svgLinks
  7. var nodesDict, linksDict
  8. var zoomBehavior
  9. var force
  10. var el
  11. var doAnimation = false
  12. var intNodes = []
  13. var intLinks = []
  14. var highlight
  15. var highlightedNodes = []
  16. var highlightedLinks = []
  17. var nodes = []
  18. var unknownNodes = []
  19. var LINK_DISTANCE = 70
  20. function graphDiameter(nodes) {
  21. return Math.sqrt(nodes.length / Math.PI) * LINK_DISTANCE * 1.41
  22. }
  23. function savePositions() {
  24. if (!localStorageTest())
  25. return
  26. var save = intNodes.map( function (d) {
  27. return { id: d.o.id, x: d.x, y: d.y }
  28. })
  29. localStorage.setItem("graph/nodeposition", JSON.stringify(save))
  30. }
  31. function nodeName(d) {
  32. if (d.o.node && d.o.node.nodeinfo)
  33. return d.o.node.nodeinfo.hostname
  34. else
  35. return d.o.id
  36. }
  37. function dragstart(d) {
  38. d3.event.sourceEvent.stopPropagation()
  39. d.fixed |= 2
  40. }
  41. function dragmove(d) {
  42. d.px = d3.event.x
  43. d.py = d3.event.y
  44. force.resume()
  45. }
  46. function dragend(d) {
  47. d3.event.sourceEvent.stopPropagation()
  48. d.fixed &= 1
  49. }
  50. var draggableNode = d3.behavior.drag()
  51. .on("dragstart", dragstart)
  52. .on("drag", dragmove)
  53. .on("dragend", dragend)
  54. function animatePanzoom(translate, scale) {
  55. var translateP = zoomBehavior.translate()
  56. var scaleP = zoomBehavior.scale()
  57. if (!doAnimation) {
  58. zoomBehavior.translate(translate)
  59. zoomBehavior.scale(scale)
  60. panzoom()
  61. } else {
  62. var start = {x: translateP[0], y: translateP[1], scale: scaleP}
  63. var end = {x: translate[0], y: translate[1], scale: scale}
  64. var interpolate = d3.interpolateObject(start, end)
  65. var duration = 500
  66. var ease = d3.ease("cubic-in-out")
  67. d3.timer(function (t) {
  68. if (t >= duration)
  69. return true
  70. var v = interpolate(ease(t / duration))
  71. zoomBehavior.translate([v.x, v.y])
  72. zoomBehavior.scale(v.scale)
  73. panzoom()
  74. return false
  75. })
  76. }
  77. }
  78. var translateP, scaleP
  79. function panzoom() {
  80. var translate = zoomBehavior.translate()
  81. var scale = zoomBehavior.scale()
  82. panzoomReal(translate, scale)
  83. translateP = translate
  84. scaleP = scale
  85. }
  86. function panzoomReal(translate, scale) {
  87. screenRect = {left: -translate[0] / scale, top: -translate[1] / scale,
  88. right: (canvas.width - translate[0]) / scale,
  89. bottom: (canvas.height - translate[1]) / scale}
  90. svg.attr("transform", "translate(" + translate + ") " +
  91. "scale(" + scale + ")")
  92. redraw()
  93. }
  94. function getSize() {
  95. var sidebarWidth = sidebar.getWidth()
  96. var width = el.offsetWidth - sidebarWidth
  97. var height = el.offsetHeight
  98. return [width, height]
  99. }
  100. function panzoomTo(a, b) {
  101. var sidebarWidth = sidebar.getWidth()
  102. var size = getSize()
  103. var targetWidth = Math.max(1, b[0] - a[0])
  104. var targetHeight = Math.max(1, b[1] - a[1])
  105. var scaleX = size[0] / targetWidth
  106. var scaleY = size[1] / targetHeight
  107. var scaleMax = zoomBehavior.scaleExtent()[1]
  108. var scale = 0.5 * Math.min(scaleMax, Math.min(scaleX, scaleY))
  109. var centroid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]
  110. var x = -centroid[0] * scale + size[0] / 2
  111. var y = -centroid[1] * scale + size[1] / 2
  112. var translate = [x + sidebarWidth, y]
  113. animatePanzoom(translate, scale)
  114. }
  115. function updateHighlight(nopanzoom) {
  116. highlightedNodes = []
  117. highlightedLinks = []
  118. if (highlight !== undefined)
  119. if (highlight.type === "node") {
  120. var n = nodesDict[highlight.o.nodeinfo.node_id]
  121. if (n) {
  122. highlightedNodes = [n]
  123. if (!nopanzoom)
  124. panzoomTo([n.x, n.y], [n.x, n.y])
  125. }
  126. return
  127. } else if (highlight.type === "link") {
  128. var l = linksDict[highlight.o.id]
  129. if (l) {
  130. highlightedLinks = [l]
  131. highlightedNodes = [l.source, l.target]
  132. if (!nopanzoom) {
  133. var x = d3.extent([l.source, l.target], function (d) { return d.x })
  134. var y = d3.extent([l.source, l.target], function (d) { return d.y })
  135. panzoomTo([x[0], y[0]], [x[1], y[1]])
  136. }
  137. }
  138. return
  139. }
  140. if (!nopanzoom)
  141. panzoomTo([0, 0], force.size())
  142. }
  143. function updateLinks(vis, data) {
  144. var link = vis.selectAll("line")
  145. .data(data, function (d) { return d.o.id })
  146. link.exit().remove()
  147. link.enter().append("line")
  148. .on("click", function (d) {
  149. if (!d3.event.defaultPrevented)
  150. router.link(d.o)()
  151. })
  152. return link
  153. }
  154. function updateNodes(vis, data) {
  155. var node = vis.selectAll("circle")
  156. .data(data, function(d) { return d.o.id })
  157. node.exit().remove()
  158. node.enter().append("circle")
  159. .attr("r", 12)
  160. .on("click", function (d) {
  161. if (!d3.event.defaultPrevented)
  162. router.node(d.o.node)()
  163. })
  164. .call(draggableNode)
  165. return node
  166. }
  167. function drawLabel(d) {
  168. var sum = d.neighbours.reduce(function (a, b) {
  169. return [a[0] + b.x, a[1] + b.y]
  170. }, [0, 0])
  171. var sumCos = sum[0] - d.x * d.neighbours.length
  172. var sumSin = sum[1] - d.y * d.neighbours.length
  173. var angle = Math.PI / 2
  174. if (d.neighbours.length > 0)
  175. angle = Math.PI + Math.atan2(sumSin, sumCos)
  176. var cos = Math.cos(angle)
  177. var sin = Math.sin(angle)
  178. var width = d.labelWidth
  179. var height = d.labelHeight
  180. var x = d.x + d.labelA * Math.pow(Math.abs(cos), 2 / 5) * Math.sign(cos) - width / 2
  181. var y = d.y + d.labelB * Math.pow(Math.abs(sin), 2 / 5) * Math.sign(sin) - height / 2
  182. ctx.drawImage(d.label, x, y, width, height)
  183. }
  184. function visibleLinks(d) {
  185. return (d.source.x > screenRect.left && d.source.x < screenRect.right &&
  186. d.source.y > screenRect.top && d.source.y < screenRect.bottom) ||
  187. (d.target.x > screenRect.left && d.target.x < screenRect.right &&
  188. d.target.y > screenRect.top && d.target.y < screenRect.bottom)
  189. }
  190. function visibleNodes(d) {
  191. return d.x + margin > screenRect.left && d.x - margin < screenRect.right &&
  192. d.y + margin > screenRect.top && d.y - margin < screenRect.bottom
  193. }
  194. function redraw() {
  195. var translate = zoomBehavior.translate()
  196. var scale = zoomBehavior.scale()
  197. var links = intLinks.filter(visibleLinks)
  198. var xExtent = d3.extent(intNodes, function (d) { return d.px })
  199. var yExtent = d3.extent(intNodes, function (d) { return d.py })
  200. if (translateP) {
  201. ctx.save()
  202. ctx.translate(translateP[0], translateP[1])
  203. ctx.scale(scaleP, scaleP)
  204. ctx.clearRect(xExtent[0] - margin, yExtent[0] - margin,
  205. xExtent[1] - xExtent[0] + 2 * margin,
  206. yExtent[1] - yExtent[0] + 2 * margin)
  207. ctx.restore()
  208. }
  209. ctx.save()
  210. ctx.translate(translate[0], translate[1])
  211. ctx.scale(scale, scale)
  212. if (!translateP)
  213. ctx.clearRect(xExtent[0] - margin, yExtent[0] - margin,
  214. xExtent[1] - xExtent[0] + 2 * margin,
  215. yExtent[1] - yExtent[0] + 2 * margin)
  216. if (highlightedLinks.length) {
  217. ctx.save()
  218. ctx.lineWidth = 10
  219. ctx.strokeStyle = "#FFD486"
  220. highlightedLinks.forEach(function (d) {
  221. ctx.beginPath()
  222. ctx.moveTo(d.source.x, d.source.y)
  223. ctx.lineTo(d.target.x, d.target.y)
  224. ctx.stroke()
  225. })
  226. ctx.restore()
  227. }
  228. ctx.lineWidth = 2.5
  229. links.forEach(function (d) {
  230. ctx.beginPath()
  231. ctx.moveTo(d.source.x, d.source.y)
  232. ctx.lineTo(d.target.x, d.target.y)
  233. ctx.strokeStyle = d.color
  234. ctx.stroke()
  235. })
  236. if (scale > 0.9)
  237. intNodes.filter(visibleNodes).forEach(drawLabel, scale)
  238. ctx.beginPath()
  239. unknownNodes.filter(visibleNodes).forEach(function (d) {
  240. ctx.moveTo(d.x + 8, d.y)
  241. ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI)
  242. })
  243. ctx.strokeStyle = "#d00000"
  244. ctx.fillStyle = "#ffffff"
  245. ctx.fill()
  246. ctx.stroke()
  247. ctx.beginPath()
  248. nodes.filter(visibleNodes).forEach(function (d) {
  249. ctx.moveTo(d.x + 8, d.y)
  250. ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI)
  251. })
  252. ctx.strokeStyle = "#AEC7E8"
  253. ctx.fillStyle = "#ffffff"
  254. ctx.fill()
  255. ctx.stroke()
  256. if (highlightedNodes.length) {
  257. ctx.save()
  258. ctx.strokeStyle = "#FFD486"
  259. ctx.fillStyle = "orange"
  260. ctx.lineWidth = 6
  261. highlightedNodes.forEach(function (d) {
  262. ctx.beginPath()
  263. ctx.moveTo(d.x + 8, d.y)
  264. ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI)
  265. ctx.fill()
  266. ctx.stroke()
  267. })
  268. ctx.restore()
  269. }
  270. ctx.restore()
  271. }
  272. function tickEvent() {
  273. redraw()
  274. svgLinks.attr("x1", function(d) { return d.source.x })
  275. .attr("y1", function(d) { return d.source.y })
  276. .attr("x2", function(d) { return d.target.x })
  277. .attr("y2", function(d) { return d.target.y })
  278. svgNodes.attr("cx", function (d) { return d.x })
  279. .attr("cy", function (d) { return d.y })
  280. }
  281. function resizeCanvas() {
  282. var r = window.devicePixelRatio
  283. canvas.width = el.offsetWidth * r
  284. canvas.height = el.offsetHeight * r
  285. canvas.style.width = el.offsetWidth + "px"
  286. canvas.style.height = el.offsetHeight + "px"
  287. ctx.resetTransform()
  288. ctx.scale(r, r)
  289. redraw()
  290. }
  291. el = document.createElement("div")
  292. el.classList.add("graph")
  293. self.div = el
  294. zoomBehavior = d3.behavior.zoom()
  295. .scaleExtent([1 / 3, 3])
  296. .on("zoom", panzoom)
  297. .translate([sidebar.getWidth(), 0])
  298. canvas = d3.select(el).append("canvas").node()
  299. svg = d3.select(el).append("svg")
  300. .attr("pointer-events", "all")
  301. .call(zoomBehavior)
  302. .append("g")
  303. var visLinks = svg.append("g")
  304. var visNodes = svg.append("g")
  305. ctx = canvas.getContext("2d")
  306. force = d3.layout.force()
  307. .charge(-80)
  308. .gravity(0.01)
  309. .chargeDistance(8 * LINK_DISTANCE)
  310. .linkDistance(LINK_DISTANCE)
  311. .linkStrength(function (d) {
  312. return Math.max(0.5, 1 / d.o.tq)
  313. })
  314. .on("tick", tickEvent)
  315. .on("end", savePositions)
  316. window.addEventListener("resize", resizeCanvas)
  317. panzoom()
  318. self.setData = function (data) {
  319. var oldNodes = {}
  320. intNodes.forEach( function (d) {
  321. oldNodes[d.o.id] = d
  322. })
  323. intNodes = data.graph.nodes.map( function (d) {
  324. var e
  325. if (d.id in oldNodes)
  326. e = oldNodes[d.id]
  327. else
  328. e = {}
  329. e.o = d
  330. return e
  331. })
  332. var newNodesDict = {}
  333. intNodes.forEach( function (d) {
  334. newNodesDict[d.o.id] = d
  335. })
  336. var oldLinks = {}
  337. intLinks.forEach( function (d) {
  338. oldLinks[d.o.id] = d
  339. })
  340. intLinks = data.graph.links.filter( function (d) {
  341. return !d.vpn
  342. }).map( function (d) {
  343. var e
  344. if (d.id in oldLinks)
  345. e = oldLinks[d.id]
  346. else
  347. e = {}
  348. e.o = d
  349. e.source = newNodesDict[d.source.id]
  350. e.target = newNodesDict[d.target.id]
  351. e.color = linkScale(d.tq).hex()
  352. return e
  353. })
  354. linksDict = {}
  355. nodesDict = {}
  356. intNodes.forEach(function (d) {
  357. d.neighbours = {}
  358. if (d.o.node)
  359. nodesDict[d.o.node.nodeinfo.node_id] = d
  360. var name = nodeName(d)
  361. ctx.font = "11px Roboto"
  362. var offset = 8
  363. var lineWidth = 3
  364. var width = ctx.measureText(name).width
  365. var buffer = document.createElement("canvas")
  366. var r = window.devicePixelRatio
  367. var bctx = buffer.getContext("2d")
  368. var scale = zoomBehavior.scaleExtent()[1] * r
  369. buffer.width = (width + 2 * lineWidth) * scale
  370. buffer.height = (16 + 2 * lineWidth) * scale
  371. bctx.scale(scale, scale)
  372. bctx.textBaseline = "middle"
  373. bctx.textAlign = "center"
  374. bctx.font = ctx.font
  375. bctx.lineWidth = lineWidth
  376. bctx.lineCap = "round"
  377. bctx.strokeStyle = "rgba(255, 255, 255, 0.8)"
  378. bctx.fillStyle = "rgba(0, 0, 0, 0.6)"
  379. bctx.strokeText(name, buffer.width / (2 * scale), buffer.height / (2 * scale))
  380. bctx.fillText(name, buffer.width / (2 * scale), buffer.height / (2 * scale))
  381. d.label = buffer
  382. d.labelWidth = buffer.width / scale
  383. d.labelHeight = buffer.height / scale
  384. d.labelA = offset + buffer.width / (2 * scale)
  385. d.labelB = offset + buffer.height / (2 * scale)
  386. })
  387. intLinks.forEach(function (d) {
  388. d.source.neighbours[d.target.o.id] = d.target
  389. d.target.neighbours[d.source.o.id] = d.source
  390. if (d.o.source.node && d.o.target.node)
  391. linksDict[d.o.id] = d
  392. })
  393. intNodes.forEach(function (d) {
  394. d.neighbours = Object.keys(d.neighbours).map(function (k) {
  395. return d.neighbours[k]
  396. })
  397. })
  398. svgLinks = updateLinks(visLinks, intLinks)
  399. svgNodes = updateNodes(visNodes, intNodes)
  400. nodes = intNodes.filter(function (d) { return d.o.node })
  401. unknownNodes = intNodes.filter(function (d) { return !d.o.node })
  402. if (localStorageTest()) {
  403. var save = JSON.parse(localStorage.getItem("graph/nodeposition"))
  404. if (save) {
  405. var nodePositions = {}
  406. save.forEach( function (d) {
  407. nodePositions[d.id] = d
  408. })
  409. intNodes.forEach( function (d) {
  410. if (nodePositions[d.o.id] && (d.x === undefined || d.y === undefined)) {
  411. d.x = nodePositions[d.o.id].x
  412. d.y = nodePositions[d.o.id].y
  413. }
  414. })
  415. }
  416. }
  417. var diameter = graphDiameter(intNodes)
  418. force.nodes(intNodes)
  419. .links(intLinks)
  420. .size([diameter, diameter])
  421. updateHighlight(true)
  422. force.start()
  423. resizeCanvas()
  424. }
  425. self.resetView = function () {
  426. highlight = undefined
  427. updateHighlight()
  428. doAnimation = true
  429. }
  430. self.gotoNode = function (d) {
  431. highlight = {type: "node", o: d}
  432. updateHighlight()
  433. doAnimation = true
  434. }
  435. self.gotoLink = function (d) {
  436. highlight = {type: "link", o: d}
  437. updateHighlight()
  438. doAnimation = true
  439. }
  440. self.destroy = function () {
  441. force.stop()
  442. canvas.remove()
  443. force = null
  444. svg = null
  445. }
  446. return self
  447. }
  448. })