|
@@ -60,6 +60,9 @@ define(["d3"], function (d3) {
|
|
|
d3.event.sourceEvent.stopPropagation()
|
|
|
d3.event.sourceEvent.preventDefault()
|
|
|
draggedNode.fixed |= 2
|
|
|
+
|
|
|
+ draggedNode.px = draggedNode.x
|
|
|
+ draggedNode.py = draggedNode.y
|
|
|
}
|
|
|
|
|
|
function dragmove() {
|
|
@@ -76,7 +79,7 @@ define(["d3"], function (d3) {
|
|
|
if (draggedNode) {
|
|
|
d3.event.sourceEvent.stopPropagation()
|
|
|
d3.event.sourceEvent.preventDefault()
|
|
|
- draggedNode.fixed &= 1
|
|
|
+ draggedNode.fixed &= ~2
|
|
|
draggedNode = undefined
|
|
|
}
|
|
|
}
|
|
@@ -117,8 +120,6 @@ define(["d3"], function (d3) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- var translateP, scaleP
|
|
|
-
|
|
|
function onPanZoom() {
|
|
|
savedPanZoom = {translate: zoomBehavior.translate(),
|
|
|
scale: zoomBehavior.scale()}
|
|
@@ -142,7 +143,7 @@ define(["d3"], function (d3) {
|
|
|
}
|
|
|
|
|
|
function getSize() {
|
|
|
- var sidebarWidth = sidebar.getWidth()
|
|
|
+ var sidebarWidth = sidebar()
|
|
|
var width = el.offsetWidth - sidebarWidth
|
|
|
var height = el.offsetHeight
|
|
|
|
|
@@ -150,7 +151,7 @@ define(["d3"], function (d3) {
|
|
|
}
|
|
|
|
|
|
function panzoomTo(a, b) {
|
|
|
- var sidebarWidth = sidebar.getWidth()
|
|
|
+ var sidebarWidth = sidebar()
|
|
|
var size = getSize()
|
|
|
|
|
|
var targetWidth = Math.max(1, b[0] - a[0])
|
|
@@ -190,7 +191,6 @@ define(["d3"], function (d3) {
|
|
|
|
|
|
if (l) {
|
|
|
highlightedLinks = [l]
|
|
|
- highlightedNodes = [l.source, l.target]
|
|
|
|
|
|
if (!nopanzoom) {
|
|
|
var x = d3.extent([l.source, l.target], function (d) { return d.x })
|
|
@@ -239,11 +239,11 @@ define(["d3"], function (d3) {
|
|
|
}
|
|
|
|
|
|
function visibleLinks(d) {
|
|
|
- if (!d.o.vpn)
|
|
|
- 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)
|
|
|
+ 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) ||
|
|
|
+ d.o.vpn
|
|
|
}
|
|
|
|
|
|
function visibleNodes(d) {
|
|
@@ -252,144 +252,185 @@ define(["d3"], function (d3) {
|
|
|
}
|
|
|
|
|
|
function redraw() {
|
|
|
+ var r = window.devicePixelRatio
|
|
|
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.setTransform(1, 0, 0, 1, 0, 0)
|
|
|
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
|
+ 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)
|
|
|
+ var clientColor = "rgba(230, 50, 75, 1.0)"
|
|
|
+ var unknownColor = "#D10E2A"
|
|
|
+ var nodeColor = "#F2E3C6"
|
|
|
+ var highlightColor = "rgba(252, 227, 198, 0.15)"
|
|
|
+ var nodeRadius = 6
|
|
|
+
|
|
|
+ // -- draw links --
|
|
|
+ ctx.save()
|
|
|
+ links.forEach(function (d) {
|
|
|
+ var dx = d.target.x - d.source.x
|
|
|
+ var dy = d.target.y - d.source.y
|
|
|
+ var a = Math.sqrt(dx * dx + dy * dy)
|
|
|
+ dx /= a
|
|
|
+ dy /= a
|
|
|
+
|
|
|
+ ctx.beginPath()
|
|
|
+ ctx.moveTo(d.source.x + dx * nodeRadius, d.source.y + dy * nodeRadius)
|
|
|
+ ctx.lineTo(d.target.x - dx * nodeRadius, d.target.y - dy * nodeRadius)
|
|
|
+ ctx.strokeStyle = d.color
|
|
|
+ ctx.globalAlpha = d.o.vpn ? 0.1 : 0.8
|
|
|
+ ctx.lineWidth = d.o.vpn ? 1.5 : 2.5
|
|
|
+ ctx.stroke()
|
|
|
+ })
|
|
|
|
|
|
- // Remeber last translate/scale state
|
|
|
- translateP = translate
|
|
|
- scaleP = scale
|
|
|
+ ctx.restore()
|
|
|
|
|
|
+ // -- draw unknown nodes --
|
|
|
ctx.beginPath()
|
|
|
- nodes.filter(visibleNodes).forEach(function (d) {
|
|
|
- var clients = d.o.node.statistics.clients
|
|
|
- if (d.clients === 0)
|
|
|
- return
|
|
|
+ unknownNodes.filter(visibleNodes).forEach(function (d) {
|
|
|
+ ctx.moveTo(d.x + nodeRadius, d.y)
|
|
|
+ ctx.arc(d.x, d.y, nodeRadius, 0, 2 * Math.PI)
|
|
|
+ })
|
|
|
|
|
|
- var distance = 16
|
|
|
- var radius = 3
|
|
|
- var a = 1.2
|
|
|
- var startAngle = Math.PI
|
|
|
- var angle = startAngle
|
|
|
+ ctx.strokeStyle = unknownColor
|
|
|
+ ctx.lineWidth = nodeRadius
|
|
|
|
|
|
- for (var i = 0; i < clients; i++) {
|
|
|
- if ((angle - startAngle) > 2 * Math.PI) {
|
|
|
- angle = startAngle
|
|
|
- distance += 2 * radius * a
|
|
|
- }
|
|
|
+ ctx.stroke()
|
|
|
|
|
|
- var x = d.x + distance * Math.cos(angle)
|
|
|
- var y = d.y + distance * Math.sin(angle)
|
|
|
|
|
|
- ctx.moveTo(x, y)
|
|
|
- ctx.arc(x, y, radius, 0, 2 * Math.PI)
|
|
|
+ // -- draw nodes --
|
|
|
|
|
|
- var n = Math.floor((Math.PI * distance) / (a * radius))
|
|
|
- var angleDelta = 2 * Math.PI / n
|
|
|
- angle += angleDelta
|
|
|
- }
|
|
|
- })
|
|
|
+ var node = document.createElement("canvas")
|
|
|
+ node.width = scale * nodeRadius * 8 * r
|
|
|
+ node.height = node.width
|
|
|
|
|
|
- ctx.fillStyle = "#73A7CC"
|
|
|
- ctx.fill()
|
|
|
+ var nctx = node.getContext("2d")
|
|
|
+ nctx.scale(scale * r, scale * r)
|
|
|
+ nctx.save()
|
|
|
|
|
|
- if (highlightedLinks.length) {
|
|
|
- ctx.save()
|
|
|
- ctx.lineWidth = 10
|
|
|
- ctx.strokeStyle = "#FFD486"
|
|
|
+ nctx.translate(-node.width / scale, -node.height / scale)
|
|
|
+ nctx.lineWidth = nodeRadius
|
|
|
|
|
|
- highlightedLinks.forEach(function (d) {
|
|
|
- ctx.beginPath()
|
|
|
- ctx.moveTo(d.source.x, d.source.y)
|
|
|
- ctx.lineTo(d.target.x, d.target.y)
|
|
|
- ctx.stroke()
|
|
|
- })
|
|
|
+ nctx.beginPath()
|
|
|
+ nctx.moveTo(nodeRadius, 0)
|
|
|
+ nctx.arc(0, 0, nodeRadius, 0, 2 * Math.PI)
|
|
|
|
|
|
- ctx.restore()
|
|
|
- }
|
|
|
+ nctx.strokeStyle = "rgba(255, 0, 0, 1)"
|
|
|
+ nctx.shadowOffsetX = node.width * 1.5 + 0
|
|
|
+ nctx.shadowOffsetY = node.height * 1.5 + 3
|
|
|
+ nctx.shadowBlur = 12
|
|
|
+ nctx.shadowColor = "rgba(0, 0, 0, 0.16)"
|
|
|
+ nctx.stroke()
|
|
|
+ nctx.shadowOffsetX = node.width * 1.5 + 0
|
|
|
+ nctx.shadowOffsetY = node.height * 1.5 + 3
|
|
|
+ nctx.shadowBlur = 12
|
|
|
+ nctx.shadowColor = "rgba(0, 0, 0, 0.23)"
|
|
|
+ nctx.stroke()
|
|
|
|
|
|
- ctx.save()
|
|
|
+ nctx.restore()
|
|
|
+ nctx.translate(node.width / 2 / scale / r, node.height / 2 / scale / r)
|
|
|
|
|
|
- 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.globalAlpha = d.o.vpn ? 0.1 : 1
|
|
|
- ctx.lineWidth = d.o.vpn ? 1.5 : 2.5
|
|
|
- ctx.stroke()
|
|
|
- })
|
|
|
+ nctx.beginPath()
|
|
|
+ nctx.moveTo(nodeRadius, 0)
|
|
|
+ nctx.arc(0, 0, nodeRadius, 0, 2 * Math.PI)
|
|
|
|
|
|
- ctx.restore()
|
|
|
+ nctx.strokeStyle = nodeColor
|
|
|
+ nctx.lineWidth = nodeRadius
|
|
|
+ nctx.stroke()
|
|
|
|
|
|
- if (scale > 0.9)
|
|
|
- intNodes.filter(visibleNodes).forEach(drawLabel, scale)
|
|
|
+ ctx.save()
|
|
|
+ ctx.scale(1 / scale / r, 1 / scale / r)
|
|
|
+ nodes.filter(visibleNodes).forEach(function (d) {
|
|
|
+ ctx.drawImage(node, scale * r * d.x - node.width / 2, scale * r * d.y - node.height / 2)
|
|
|
+ })
|
|
|
+ ctx.restore()
|
|
|
|
|
|
+ // -- draw clients --
|
|
|
+ ctx.save()
|
|
|
ctx.beginPath()
|
|
|
+ nodes.filter(visibleNodes).forEach(function (d) {
|
|
|
+ var clients = d.o.node.statistics.clients
|
|
|
+ if (clients === 0)
|
|
|
+ return
|
|
|
|
|
|
- unknownNodes.filter(visibleNodes).forEach(function (d) {
|
|
|
- ctx.moveTo(d.x + 8, d.y)
|
|
|
- ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI)
|
|
|
- })
|
|
|
+ var startDistance = 16
|
|
|
+ var radius = 3
|
|
|
+ var a = 1.2
|
|
|
+ var startAngle = Math.PI
|
|
|
|
|
|
- ctx.strokeStyle = "#d00000"
|
|
|
- ctx.fillStyle = "#ffffff"
|
|
|
- ctx.lineWidth = 2.5
|
|
|
+ for (var orbit = 0, i = 0; i < clients; orbit++) {
|
|
|
+ var distance = startDistance + orbit * 2 * radius * a
|
|
|
+ var n = Math.floor((Math.PI * distance) / (a * radius))
|
|
|
+ var delta = clients - i
|
|
|
|
|
|
- ctx.fill()
|
|
|
- ctx.stroke()
|
|
|
+ for (var j = 0; j < Math.min(delta, n); i++, j++) {
|
|
|
+ var angle = 2 * Math.PI / n * j
|
|
|
+ var x = d.x + distance * Math.cos(angle + startAngle)
|
|
|
+ var y = d.y + distance * Math.sin(angle + startAngle)
|
|
|
|
|
|
- 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.moveTo(x, y)
|
|
|
+ ctx.arc(x, y, radius, 0, 2 * Math.PI)
|
|
|
+ }
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
- ctx.strokeStyle = "#AEC7E8"
|
|
|
- ctx.fillStyle = "#ffffff"
|
|
|
-
|
|
|
+ ctx.fillStyle = clientColor
|
|
|
ctx.fill()
|
|
|
- ctx.stroke()
|
|
|
+ ctx.restore()
|
|
|
|
|
|
+ // -- draw node highlights --
|
|
|
if (highlightedNodes.length) {
|
|
|
ctx.save()
|
|
|
- ctx.strokeStyle = "#FFD486"
|
|
|
- ctx.fillStyle = "orange"
|
|
|
- ctx.lineWidth = 6
|
|
|
+ ctx.shadowColor = "rgba(255, 255, 255, 1.0)"
|
|
|
+ ctx.shadowBlur = 10 * nodeRadius
|
|
|
+ ctx.shadowOffsetX = 0
|
|
|
+ ctx.shadowOffsetY = 0
|
|
|
+ ctx.globalCompositeOperation = "lighten"
|
|
|
+ ctx.fillStyle = highlightColor
|
|
|
|
|
|
+ ctx.beginPath()
|
|
|
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.moveTo(d.x + 5 * nodeRadius, d.y)
|
|
|
+ ctx.arc(d.x, d.y, 5 * nodeRadius, 0, 2 * Math.PI)
|
|
|
})
|
|
|
+ ctx.fill()
|
|
|
|
|
|
ctx.restore()
|
|
|
}
|
|
|
|
|
|
+ // -- draw link highlights --
|
|
|
+ if (highlightedLinks.length) {
|
|
|
+ ctx.save()
|
|
|
+ ctx.lineWidth = 2 * 5 * nodeRadius
|
|
|
+ ctx.shadowColor = "rgba(255, 255, 255, 1.0)"
|
|
|
+ ctx.shadowBlur = 10 * nodeRadius
|
|
|
+ ctx.shadowOffsetX = 0
|
|
|
+ ctx.shadowOffsetY = 0
|
|
|
+ ctx.globalCompositeOperation = "lighten"
|
|
|
+ ctx.strokeStyle = highlightColor
|
|
|
+ ctx.lineCap = "round"
|
|
|
+
|
|
|
+ ctx.beginPath()
|
|
|
+ highlightedLinks.forEach(function (d) {
|
|
|
+ ctx.moveTo(d.source.x, d.source.y)
|
|
|
+ ctx.lineTo(d.target.x, d.target.y)
|
|
|
+ })
|
|
|
+ ctx.stroke()
|
|
|
+
|
|
|
+ ctx.restore()
|
|
|
+ }
|
|
|
+
|
|
|
+ // -- draw labels --
|
|
|
+ if (scale > 0.9)
|
|
|
+ intNodes.filter(visibleNodes).forEach(drawLabel, scale)
|
|
|
+
|
|
|
ctx.restore()
|
|
|
}
|
|
|
|
|
@@ -403,7 +444,7 @@ define(["d3"], function (d3) {
|
|
|
canvas.height = el.offsetHeight * r
|
|
|
canvas.style.width = el.offsetWidth + "px"
|
|
|
canvas.style.height = el.offsetHeight + "px"
|
|
|
- ctx.resetTransform()
|
|
|
+ ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
|
ctx.scale(r, r)
|
|
|
requestAnimationFrame(redraw)
|
|
|
}
|
|
@@ -461,6 +502,8 @@ define(["d3"], function (d3) {
|
|
|
}
|
|
|
|
|
|
var links = intLinks.filter(function (d) {
|
|
|
+ return !d.o.vpn
|
|
|
+ }).filter(function (d) {
|
|
|
return distanceLink(e, d.source, d.target) < LINE_RADIUS
|
|
|
})
|
|
|
|
|
@@ -470,19 +513,46 @@ define(["d3"], function (d3) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ function zoom(z, scale) {
|
|
|
+ var size = getSize()
|
|
|
+ var newSize = [size[0] / scale, size[1] / scale]
|
|
|
+
|
|
|
+ var sidebarWidth = sidebar()
|
|
|
+ var delta = [size[0] - newSize[0], size[1] - newSize[1]]
|
|
|
+ var translate = z.translate()
|
|
|
+ var translateNew = [sidebarWidth + (translate[0] - sidebarWidth - delta[0] / 2) * scale, (translate[1] - delta[1] / 2) * scale]
|
|
|
+
|
|
|
+ animatePanzoom(translateNew, z.scale() * scale)
|
|
|
+ }
|
|
|
+
|
|
|
+ function keyboardZoom(z) {
|
|
|
+ return function () {
|
|
|
+ var e = d3.event
|
|
|
+
|
|
|
+ if (e.altKey || e.ctrlKey || e.metaKey)
|
|
|
+ return
|
|
|
+
|
|
|
+ if (e.keyCode === 43)
|
|
|
+ zoom(z, 1.41)
|
|
|
+
|
|
|
+ if (e.keyCode === 45)
|
|
|
+ zoom(z, 1 / 1.41)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
el = document.createElement("div")
|
|
|
el.classList.add("graph")
|
|
|
- self.div = el
|
|
|
|
|
|
zoomBehavior = d3.behavior.zoom()
|
|
|
.scaleExtent([1 / 3, 3])
|
|
|
.on("zoom", onPanZoom)
|
|
|
- .translate([sidebar.getWidth(), 0])
|
|
|
+ .translate([sidebar(), 0])
|
|
|
|
|
|
canvas = d3.select(el)
|
|
|
+ .attr("tabindex", 1)
|
|
|
+ .on("keypress", keyboardZoom(zoomBehavior))
|
|
|
.call(zoomBehavior)
|
|
|
.append("canvas")
|
|
|
- .attr("pointer-events", "all")
|
|
|
.on("click", onClick)
|
|
|
.call(draggableNode)
|
|
|
.node()
|
|
@@ -552,7 +622,11 @@ define(["d3"], function (d3) {
|
|
|
e.o = d
|
|
|
e.source = newNodesDict[d.source.id]
|
|
|
e.target = newNodesDict[d.target.id]
|
|
|
- e.color = linkScale(d.tq).hex()
|
|
|
+
|
|
|
+ if (d.vpn)
|
|
|
+ e.color = "rgba(255, 255, 255, " + (0.6 / d.tq) + ")"
|
|
|
+ else
|
|
|
+ e.color = linkScale(d.tq).hex()
|
|
|
|
|
|
return e
|
|
|
})
|
|
@@ -568,7 +642,7 @@ define(["d3"], function (d3) {
|
|
|
|
|
|
var name = nodeName(d)
|
|
|
|
|
|
- var offset = 8
|
|
|
+ var offset = 5
|
|
|
var lineWidth = 3
|
|
|
var buffer = document.createElement("canvas")
|
|
|
var r = window.devicePixelRatio
|
|
@@ -581,12 +655,9 @@ define(["d3"], function (d3) {
|
|
|
bctx.scale(scale, scale)
|
|
|
bctx.textBaseline = "middle"
|
|
|
bctx.textAlign = "center"
|
|
|
- bctx.lineWidth = lineWidth
|
|
|
- bctx.lineCap = "round"
|
|
|
- bctx.strokeStyle = "rgba(255, 255, 255, 0.8)"
|
|
|
- bctx.fillStyle = "rgba(0, 0, 0, 0.6)"
|
|
|
- bctx.miterLimit = 2
|
|
|
- bctx.strokeText(name, buffer.width / (2 * scale), buffer.height / (2 * scale))
|
|
|
+ bctx.fillStyle = "rgba(242, 227, 198, 1.0)"
|
|
|
+ bctx.shadowColor = "rgba(0, 0, 0, 1)"
|
|
|
+ bctx.shadowBlur = 5
|
|
|
bctx.fillText(name, buffer.width / (2 * scale), buffer.height / (2 * scale))
|
|
|
|
|
|
d.label = buffer
|
|
@@ -665,6 +736,15 @@ define(["d3"], function (d3) {
|
|
|
force.stop()
|
|
|
canvas.remove()
|
|
|
force = null
|
|
|
+
|
|
|
+ if (el.parentNode)
|
|
|
+ el.parentNode.removeChild(el)
|
|
|
+ }
|
|
|
+
|
|
|
+ self.render = function (d) {
|
|
|
+ d.appendChild(el)
|
|
|
+ resizeCanvas()
|
|
|
+ updateHighlight()
|
|
|
}
|
|
|
|
|
|
return self
|