<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<h1>CDO</h1>
<h2>VPN Hub & Peers</h2>
<div id="vpn-device"></div>
<h2>All Tunnels</h2>
<button class="reset">Reset</button>
<div id="vpn-all-devices"></div>
</body>
</html>
// Code goes here
var files = [
'all-tunnels.json',
'device-graph.json'
];
d3.json(files[0], function (data) {
var graph = new Graph({
element: document.querySelector('#vpn-all-devices'),
strength: -50e0, // -8e1
distance: 6e0,
radius: 5e0,
graph: data,
draggableNodes: true,
dragAndZoom: {scale: [.1, 2]}
});
});
// d3.json(files[1], function (data) {
// var graph = new Graph({
// element: document.querySelector('#vpn-device'),
// graph: data,
// draggableNodes: true,
// highlightSelectedPath: true,
// dragAndZoom: false
// });
// });
var Graph = function constructor(opts){
var element = opts.element || {},
margin = opts.margin || 20,
width = opts.width || element.offsetWidth || 900,
height = (opts.height || element.offsetHeight || 600) - 0.5 - margin,
distance = opts.distance || 0.5e2,
radius = opts.radius || 5e1,
strength = opts.strength || -8e3,
graph = opts.graph || {},
onSelection = opts.onSelection || function(){};
this.scale = (opts.dragAndZoom && opts.dragAndZoom.scale) || [0.1, 4];
// SVG
var svg = d3.select(element)
.append('svg')
.attr('width', width)
.attr('height', height)
var g =
svg.append('g')
.attr('class', 'graph-container');
// Links
var link = g.append('g')
.attr('class', 'links')
.selectAll('line')
.data(graph.links)
.enter()
.append('line');
// Nodes
var node = g.append('g')
.attr('class', 'nodes')
.selectAll('g')
.data(graph.nodes)
.enter()
.append('g');
node.append('circle')
.attr('r', radius)
.classed('highlight', function(d) { return d.type === 'hub'; });
// node.append('text')
// .text(function(d) { return d.id; });
// ------------------
// Force Simulation
// ------------------
var simulation = d3.forceSimulation()
.force('link', d3.forceLink()
.id(function(d) { return d.id; })
.iterations(4))
// .distance(distance))
.force('charge', d3.forceManyBody()
.strength(strength))
.force('center', d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX())
.force("y", d3.forceY());
simulation
.nodes(graph.nodes)
.on('tick', function ticked() {
link
.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('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
});
simulation.force('link')
.links(graph.links);
// ----- /Simulation
this.svg = svg;
this.g = g;
this.nodes = node;
this.links = link;
this.data = graph;
this.simulation = simulation;
this.onSelection = onSelection;
this.height = height;
this.width = width;
if(opts.draggableNodes){
this.draggableNodes();
}
if(opts.highlightSelectedPath) {
this.highlightSelectedPath();
}
if(opts.dragAndZoom){
this.dragAndZoom();
}
};
// ------------------
// Dragging nodes
// ------------------
Graph.prototype.draggableNodes = function(){
var that = this;
this.nodes.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
function dragstarted(d) {
if (!d3.event.active) {
that.simulation.alphaTarget(0.3).restart();
}
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) {
that.simulation.alphaTarget(0);
}
d.fx = null;
d.fy = null;
}
};
// ------------------
// Selecting Nodes
// ------------------
Graph.prototype.highlightSelectedPath = function () {
var that = this;
this.nodes
.on('click', onClick)
.on('mouseover', onMouseover)
.on('mouseout', onMouseout);
function onClick(d){
if(d.type === 'hub'){
return;
}
// clear spoke hightlight
that.nodes.select('circle').classed('highlight', function(d) { return d.type === 'hub'; });
that.links.classed('highlight', false);
var connected = getConnectedNodes(d);
connected.nodes.select('circle').classed('highlight', true).classed('hover', false);
connected.links.classed('highlight', true).classed('hover', false);
that.onSelection({data: d});
}
function onMouseover(){
/* jshint validthis: true */
var circle = d3.select(this).select('circle');
if(circle.classed('highlight')) { return; }
circle.classed('hover', true);
}
function onMouseout(){
/* jshint validthis: true */
var circle = d3.select(this).select('circle');
circle.classed('hover', false);
}
// Utilities
function getConnectedNodes(data){
//Create an array logging what is connected to what
var linkedByIndex = {};
for (var i = 0; i < that.data.nodes.length; i++) {
linkedByIndex[i + ',' + i] = true;
}
that.data.links.forEach(function (d) {
linkedByIndex[d.source.index + ',' + d.target.index] = true;
linkedByIndex[d.target.index + ',' + d.source.index] = true;
});
function neighboring(node1, node2){
return linkedByIndex[node1.index + ',' + node2.index];
}
function isConnected(link, data){
return link.source.id === data.id || data.id === link.target.id;
}
return {
nodes: that.nodes.filter(function(n) { return neighboring(n, data); }),
links: that.links.filter(function(l) { return isConnected(l, data); })
};
}
};
// ------------------
// Zoom
// ------------------
Graph.prototype.dragAndZoom = function(){
var that = this;
var zoom = d3.zoom()
.scaleExtent(that.scale)
// .scaleExtent([0.5, 10])
// .translateExtent([[0, -800], [600, 800]])
.on("zoom", function zoomed() {
console.log(d3.event.transform)
// console.log(d3.event.scale, d3.event.translate) // v3
that.g.attr("transform", d3.event.transform);
});
that.svg.call(zoom);
// reset button
d3.select(".reset").on("click", function resetted() {
that.svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity);
});
};
// TODO: remove text when zoom out and add text as zoom in
// TODO: constrained zoom: https://bl.ocks.org/mbostock/5e81cc677d186b6845cb00676758a339
.highlight {
stroke: #049FD9 !important;
}
.hover {
stroke: #D4F6FF !important;
}
.links line {
stroke-width: 1;
stroke: white;
stroke: #049FD9;
}
.nodes circle {
stroke-width: 2;
stroke: white;
stroke: #049FD9;
fill: white;
}
text {
font: 8px sans-serif;
fill: #444;
text-anchor: middle;
alignment-baseline: central;
}
/* CDO */
body { background-color: #E8EBF1; }
svg {
border: 1px solid black;
}
{
"nodes": [
{"id": "AMS-5512X"},
{"id": "DEN-ASA5515X"},
{"id": "FRA-ASA5510"},
{"id": "HYD-ASA5512X"},
{"id": "I3VPN-ASA5585X1"},
{"id": "62.23.180.18"},
{"id": "168.215.121.226"},
{"id": "80.113.16.226"},
{"id": "174.47.105.14"},
{"id": "82.118.170.178"},
{"id": "62.134.198.134"},
{"id": "203.121.0.210"},
{"id": "217.33.37.194"},
{"id": "202.212.180.81"},
{"id": "191.234.32.148"},
{"id": "138.91.116.26"},
{"id": "191.234.49.5"},
{"id": "193.235.50.6"},
{"id": "Device 1"},
{"id": "Device 2"},
{"id": "Device 3"},
{"id": "Device 4"},
{"id": "Device 5"},
{"id": "Device 6"},
{"id": "Device 7"},
{"id": "Device 8"},
{"id": "Device 9"},
{"id": "Device 10"},
{"id": "Device A"},
{"id": "Device B"},
{"id": "Device C"},
{"id": "Device D"},
{"id": "Device E"},
{"id": "Device F"},
{"id": "Device G"},
{"id": "Device H"},
{"id": "Device I"},
{"id": "Device J"},
{"id": "Device K"}
],
"links": [
{"source": "AMS-5512X", "target":"62.23.180.18"},
{"source": "AMS-5512X", "target":"168.215.121.226"},
{"source": "AMS-5512X", "target":"80.113.16.226"},
{"source": "DEN-ASA5515X", "target":"174.47.105.14"},
{"source": "FRA-ASA5510", "target":"62.23.180.18"},
{"source": "FRA-ASA5510", "target":"82.118.170.178"},
{"source": "FRA-ASA5510", "target":"62.134.198.134"},
{"source": "HYD-ASA5512X", "target":"174.47.105.14"},
{"source": "HYD-ASA5512X", "target":"203.121.0.210"},
{"source": "HYD-ASA5512X", "target":"217.33.37.194"},
{"source": "HYD-ASA5512X", "target":"202.212.180.81"},
{"source": "I3VPN-ASA5585X1", "target":"62.23.180.18"},
{"source": "I3VPN-ASA5585X1", "target":"168.215.121.226"},
{"source": "I3VPN-ASA5585X1", "target":"191.234.32.148"},
{"source": "I3VPN-ASA5585X1", "target":"138.91.116.26"},
{"source": "I3VPN-ASA5585X1", "target":"191.234.49.5"},
{"source": "I3VPN-ASA5585X1", "target":"193.235.50.6"},
{"source": "AMS-5512X", "target":"HYD-ASA5512X"},
{"source": "193.235.50.6", "target": "Device 1"},
{"source": "193.235.50.6", "target": "Device 2"},
{"source": "193.235.50.6", "target": "Device 3"},
{"source": "Device A", "target": "Device B"}
,{"source": "Device A", "target": "Device C"}
,{"source": "Device A", "target": "Device D"}
,{"source": "Device A", "target": "Device E"}
,{"source": "Device A", "target": "Device F"}
,{"source": "Device A", "target": "Device G"}
,{"source": "Device A", "target": "Device H"}
]
}
{
"nodes": [
{"id": "ACME", "type": "hub"},
{"id": "Device 1", "type": "spoke"},
{"id": "Device 2", "type": "spoke"},
{"id": "Device 3", "type": "spoke"},
{"id": "Device 4", "type": "spoke"},
{"id": "Device 5", "type": "spoke"},
{"id": "Device 6", "type": "spoke"}
],
"links": [
{"source": "ACME", "target": "Device 1"},
{"source": "ACME", "target": "Device 2"},
{"source": "ACME", "target": "Device 3"},
{"source": "ACME", "target": "Device 4"},
{"source": "ACME", "target": "Device 5"},
{"source": "ACME", "target": "Device 6"}
]
}