123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578 |
- define(["d3"], function (d3) {
- var margin = 200
- return function (config, linkScale, sidebar, router) {
- var self = this
- var svg, canvas, ctx, screenRect
- var svgNodes, svgLinks
- var nodesDict, linksDict
- var zoomBehavior
- var force
- var el
- var doAnimation = false
- var intNodes = []
- var intLinks = []
- var highlight
- var highlightedNodes = []
- var highlightedLinks = []
- var nodes = []
- var unknownNodes = []
- var LINK_DISTANCE = 70
- function graphDiameter(nodes) {
- return Math.sqrt(nodes.length / Math.PI) * LINK_DISTANCE * 1.41
- }
- function savePositions() {
- if (!localStorageTest())
- return
- var save = intNodes.map( function (d) {
- return { id: d.o.id, x: d.x, y: d.y }
- })
- localStorage.setItem("graph/nodeposition", JSON.stringify(save))
- }
- function nodeName(d) {
- if (d.o.node && d.o.node.nodeinfo)
- return d.o.node.nodeinfo.hostname
- else
- return d.o.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
- }
- var draggableNode = d3.behavior.drag()
- .on("dragstart", dragstart)
- .on("drag", dragmove)
- .on("dragend", dragend)
- function animatePanzoom(translate, scale) {
- var translateP = zoomBehavior.translate()
- var scaleP = zoomBehavior.scale()
- if (!doAnimation) {
- zoomBehavior.translate(translate)
- zoomBehavior.scale(scale)
- panzoom()
- } else {
- var start = {x: translateP[0], y: translateP[1], scale: scaleP}
- var end = {x: translate[0], y: translate[1], scale: scale}
- var interpolate = d3.interpolateObject(start, end)
- var duration = 500
- var ease = d3.ease("cubic-in-out")
- d3.timer(function (t) {
- if (t >= duration)
- return true
- var v = interpolate(ease(t / duration))
- zoomBehavior.translate([v.x, v.y])
- zoomBehavior.scale(v.scale)
- panzoom()
- return false
- })
- }
- }
- var translateP, scaleP
- function panzoom() {
- var translate = zoomBehavior.translate()
- var scale = zoomBehavior.scale()
- panzoomReal(translate, scale)
- translateP = translate
- scaleP = scale
- }
- function panzoomReal(translate, scale) {
- screenRect = {left: -translate[0] / scale, top: -translate[1] / scale,
- right: (canvas.width - translate[0]) / scale,
- bottom: (canvas.height - translate[1]) / scale}
- svg.attr("transform", "translate(" + translate + ") " +
- "scale(" + scale + ")")
- redraw()
- }
- function getSize() {
- var sidebarWidth = sidebar.getWidth()
- var width = el.offsetWidth - sidebarWidth
- var height = el.offsetHeight
- return [width, height]
- }
- function panzoomTo(a, b) {
- var sidebarWidth = sidebar.getWidth()
- var size = getSize()
- var targetWidth = Math.max(1, b[0] - a[0])
- var targetHeight = Math.max(1, b[1] - a[1])
- var scaleX = size[0] / targetWidth
- var scaleY = size[1] / targetHeight
- var scaleMax = zoomBehavior.scaleExtent()[1]
- var scale = 0.5 * Math.min(scaleMax, Math.min(scaleX, scaleY))
- var centroid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]
- var x = -centroid[0] * scale + size[0] / 2
- var y = -centroid[1] * scale + size[1] / 2
- var translate = [x + sidebarWidth, y]
- animatePanzoom(translate, scale)
- }
- function updateHighlight(nopanzoom) {
- highlightedNodes = []
- highlightedLinks = []
- if (highlight !== undefined)
- if (highlight.type === "node") {
- var n = nodesDict[highlight.o.nodeinfo.node_id]
- if (n) {
- highlightedNodes = [n]
- if (!nopanzoom)
- panzoomTo([n.x, n.y], [n.x, n.y])
- }
- return
- } else if (highlight.type === "link") {
- var l = linksDict[highlight.o.id]
- if (l) {
- highlightedLinks = [l]
- highlightedNodes = [l.source, l.target]
- if (!nopanzoom) {
- var x = d3.extent([l.source, l.target], function (d) { return d.x })
- var y = d3.extent([l.source, l.target], function (d) { return d.y })
- panzoomTo([x[0], y[0]], [x[1], y[1]])
- }
- }
- return
- }
- if (!nopanzoom)
- panzoomTo([0, 0], force.size())
- }
- function updateLinks(vis, data) {
- var link = vis.selectAll("line")
- .data(data, function (d) { return d.o.id })
- link.exit().remove()
- link.enter().append("line")
- .on("click", function (d) {
- if (!d3.event.defaultPrevented)
- router.link(d.o)()
- })
- return link
- }
- function updateNodes(vis, data) {
- var node = vis.selectAll("circle")
- .data(data, function(d) { return d.o.id })
- node.exit().remove()
- node.enter().append("circle")
- .attr("r", 12)
- .on("click", function (d) {
- if (!d3.event.defaultPrevented)
- router.node(d.o.node)()
- })
- .call(draggableNode)
- return node
- }
- function drawLabel(d) {
- var sum = d.neighbours.reduce(function (a, b) {
- return [a[0] + b.x, a[1] + b.y]
- }, [0, 0])
- var sumCos = sum[0] - d.x * d.neighbours.length
- var sumSin = sum[1] - d.y * d.neighbours.length
- var angle = Math.PI / 2
- if (d.neighbours.length > 0)
- angle = Math.PI + Math.atan2(sumSin, sumCos)
- var cos = Math.cos(angle)
- var sin = Math.sin(angle)
- var width = d.labelWidth
- var height = d.labelHeight
- var x = d.x + d.labelA * Math.pow(Math.abs(cos), 2 / 5) * Math.sign(cos) - width / 2
- var y = d.y + d.labelB * Math.pow(Math.abs(sin), 2 / 5) * Math.sign(sin) - height / 2
- ctx.drawImage(d.label, x, y, width, height)
- }
- function visibleLinks(d) {
- return (d.source.x > screenRect.left && d.source.x < screenRect.right &&
- d.source.y > screenRect.top && d.source.y < screenRect.bottom) ||
- (d.target.x > screenRect.left && d.target.x < screenRect.right &&
- d.target.y > screenRect.top && d.target.y < screenRect.bottom)
- }
- function visibleNodes(d) {
- return d.x + margin > screenRect.left && d.x - margin < screenRect.right &&
- d.y + margin > screenRect.top && d.y - margin < screenRect.bottom
- }
- function redraw() {
- var translate = zoomBehavior.translate()
- var scale = zoomBehavior.scale()
- var links = intLinks.filter(visibleLinks)
- var xExtent = d3.extent(intNodes, function (d) { return d.px })
- var yExtent = d3.extent(intNodes, function (d) { return d.py })
- if (translateP) {
- ctx.save()
- ctx.translate(translateP[0], translateP[1])
- ctx.scale(scaleP, scaleP)
- ctx.clearRect(xExtent[0] - margin, yExtent[0] - margin,
- xExtent[1] - xExtent[0] + 2 * margin,
- yExtent[1] - yExtent[0] + 2 * margin)
- ctx.restore()
- }
- ctx.save()
- ctx.translate(translate[0], translate[1])
- ctx.scale(scale, scale)
- if (!translateP)
- ctx.clearRect(xExtent[0] - margin, yExtent[0] - margin,
- xExtent[1] - xExtent[0] + 2 * margin,
- yExtent[1] - yExtent[0] + 2 * margin)
- if (highlightedLinks.length) {
- ctx.save()
- ctx.lineWidth = 10
- ctx.strokeStyle = "#FFD486"
- highlightedLinks.forEach(function (d) {
- ctx.beginPath()
- ctx.moveTo(d.source.x, d.source.y)
- ctx.lineTo(d.target.x, d.target.y)
- ctx.stroke()
- })
- ctx.restore()
- }
- ctx.lineWidth = 2.5
- links.forEach(function (d) {
- ctx.beginPath()
- ctx.moveTo(d.source.x, d.source.y)
- ctx.lineTo(d.target.x, d.target.y)
- ctx.strokeStyle = d.color
- ctx.stroke()
- })
- if (scale > 0.9)
- intNodes.filter(visibleNodes).forEach(drawLabel, scale)
- ctx.beginPath()
- unknownNodes.filter(visibleNodes).forEach(function (d) {
- ctx.moveTo(d.x + 8, d.y)
- ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI)
- })
- ctx.strokeStyle = "#d00000"
- ctx.fillStyle = "#ffffff"
- ctx.fill()
- ctx.stroke()
- ctx.beginPath()
- nodes.filter(visibleNodes).forEach(function (d) {
- ctx.moveTo(d.x + 8, d.y)
- ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI)
- })
- ctx.strokeStyle = "#AEC7E8"
- ctx.fillStyle = "#ffffff"
- ctx.fill()
- ctx.stroke()
- if (highlightedNodes.length) {
- ctx.save()
- ctx.strokeStyle = "#FFD486"
- ctx.fillStyle = "orange"
- ctx.lineWidth = 6
- highlightedNodes.forEach(function (d) {
- ctx.beginPath()
- ctx.moveTo(d.x + 8, d.y)
- ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI)
- ctx.fill()
- ctx.stroke()
- })
- ctx.restore()
- }
- ctx.restore()
- }
- function tickEvent() {
- redraw()
- svgLinks.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 })
- svgNodes.attr("cx", function (d) { return d.x })
- .attr("cy", function (d) { return d.y })
- }
- function resizeCanvas() {
- var r = window.devicePixelRatio
- canvas.width = el.offsetWidth * r
- canvas.height = el.offsetHeight * r
- canvas.style.width = el.offsetWidth + "px"
- canvas.style.height = el.offsetHeight + "px"
- ctx.resetTransform()
- ctx.scale(r, r)
- redraw()
- }
- el = document.createElement("div")
- el.classList.add("graph")
- self.div = el
- zoomBehavior = d3.behavior.zoom()
- .scaleExtent([1 / 3, 3])
- .on("zoom", panzoom)
- .translate([sidebar.getWidth(), 0])
- canvas = d3.select(el).append("canvas").node()
- svg = d3.select(el).append("svg")
- .attr("pointer-events", "all")
- .call(zoomBehavior)
- .append("g")
- var visLinks = svg.append("g")
- var visNodes = svg.append("g")
- ctx = canvas.getContext("2d")
- force = d3.layout.force()
- .charge(-80)
- .gravity(0.01)
- .chargeDistance(8 * LINK_DISTANCE)
- .linkDistance(LINK_DISTANCE)
- .linkStrength(function (d) {
- return Math.max(0.5, 1 / d.o.tq)
- })
- .on("tick", tickEvent)
- .on("end", savePositions)
- window.addEventListener("resize", resizeCanvas)
- panzoom()
- self.setData = function (data) {
- var oldNodes = {}
- intNodes.forEach( function (d) {
- oldNodes[d.o.id] = d
- })
- intNodes = data.graph.nodes.map( function (d) {
- var e
- if (d.id in oldNodes)
- e = oldNodes[d.id]
- else
- e = {}
- e.o = d
- return e
- })
- var newNodesDict = {}
- intNodes.forEach( function (d) {
- newNodesDict[d.o.id] = d
- })
- var oldLinks = {}
- intLinks.forEach( function (d) {
- oldLinks[d.o.id] = d
- })
- intLinks = data.graph.links.filter( function (d) {
- return !d.vpn
- }).map( function (d) {
- var e
- if (d.id in oldLinks)
- e = oldLinks[d.id]
- else
- e = {}
- e.o = d
- e.source = newNodesDict[d.source.id]
- e.target = newNodesDict[d.target.id]
- e.color = linkScale(d.tq).hex()
- return e
- })
- linksDict = {}
- nodesDict = {}
- intNodes.forEach(function (d) {
- d.neighbours = {}
- if (d.o.node)
- nodesDict[d.o.node.nodeinfo.node_id] = d
- var name = nodeName(d)
- ctx.font = "11px Roboto"
- var offset = 8
- var lineWidth = 3
- var width = ctx.measureText(name).width
- var buffer = document.createElement("canvas")
- var r = window.devicePixelRatio
- var bctx = buffer.getContext("2d")
- var scale = zoomBehavior.scaleExtent()[1] * r
- buffer.width = (width + 2 * lineWidth) * scale
- buffer.height = (16 + 2 * lineWidth) * scale
- bctx.scale(scale, scale)
- bctx.textBaseline = "middle"
- bctx.textAlign = "center"
- bctx.font = ctx.font
- bctx.lineWidth = lineWidth
- bctx.lineCap = "round"
- bctx.strokeStyle = "rgba(255, 255, 255, 0.8)"
- bctx.fillStyle = "rgba(0, 0, 0, 0.6)"
- bctx.strokeText(name, buffer.width / (2 * scale), buffer.height / (2 * scale))
- bctx.fillText(name, buffer.width / (2 * scale), buffer.height / (2 * scale))
- d.label = buffer
- d.labelWidth = buffer.width / scale
- d.labelHeight = buffer.height / scale
- d.labelA = offset + buffer.width / (2 * scale)
- d.labelB = offset + buffer.height / (2 * scale)
- })
- intLinks.forEach(function (d) {
- d.source.neighbours[d.target.o.id] = d.target
- d.target.neighbours[d.source.o.id] = d.source
- if (d.o.source.node && d.o.target.node)
- linksDict[d.o.id] = d
- })
- intNodes.forEach(function (d) {
- d.neighbours = Object.keys(d.neighbours).map(function (k) {
- return d.neighbours[k]
- })
- })
- svgLinks = updateLinks(visLinks, intLinks)
- svgNodes = updateNodes(visNodes, intNodes)
- nodes = intNodes.filter(function (d) { return d.o.node })
- unknownNodes = intNodes.filter(function (d) { return !d.o.node })
- if (localStorageTest()) {
- var save = JSON.parse(localStorage.getItem("graph/nodeposition"))
- if (save) {
- var nodePositions = {}
- save.forEach( function (d) {
- nodePositions[d.id] = d
- })
- intNodes.forEach( function (d) {
- if (nodePositions[d.o.id] && (d.x === undefined || d.y === undefined)) {
- d.x = nodePositions[d.o.id].x
- d.y = nodePositions[d.o.id].y
- }
- })
- }
- }
- var diameter = graphDiameter(intNodes)
- force.nodes(intNodes)
- .links(intLinks)
- .size([diameter, diameter])
- updateHighlight(true)
- force.start()
- resizeCanvas()
- }
- self.resetView = function () {
- highlight = undefined
- updateHighlight()
- doAnimation = true
- }
- self.gotoNode = function (d) {
- highlight = {type: "node", o: d}
- updateHighlight()
- doAnimation = true
- }
- self.gotoLink = function (d) {
- highlight = {type: "link", o: d}
- updateHighlight()
- doAnimation = true
- }
- self.destroy = function () {
- force.stop()
- canvas.remove()
- force = null
- svg = null
- }
- return self
- }
- })
|