working rendering using d3-force plugin
This commit is contained in:
@@ -14,7 +14,7 @@ ajax '/ajax/content/device/netmap' => require_login sub {
|
||||
template 'ajax/device/netmap.tt', {}, { layout => undef };
|
||||
};
|
||||
|
||||
ajax '/ajax/data/device/alldevicelinks' => require_login sub {
|
||||
ajax '/ajax/data/device/netmap' => require_login sub {
|
||||
my $q = param('q');
|
||||
my %data = ( nodes => [], links => [] );
|
||||
|
||||
|
||||
197
share/public/css/d3-force.css
Normal file
197
share/public/css/d3-force.css
Normal file
@@ -0,0 +1,197 @@
|
||||
.net_gobrechts_d3_force,
|
||||
.net_gobrechts_d3_force_customize,
|
||||
.net_gobrechts_d3_force_customize td,
|
||||
.net_gobrechts_d3_force_tooltip {
|
||||
box-sizing: content-box;
|
||||
font-family: Arial, Helvetica, Sans Serif;
|
||||
font-size: 10px;
|
||||
background-color: #fff
|
||||
}
|
||||
.net_gobrechts_d3_force.border {
|
||||
border: 1px solid silver;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.net_gobrechts_d3_force circle.highlighted {
|
||||
stroke: #555;
|
||||
stroke-width: 2px;
|
||||
stroke-opacity: 1.0;
|
||||
}
|
||||
.net_gobrechts_d3_force circle.selected {
|
||||
stroke: #555;
|
||||
stroke-width: 4px;
|
||||
stroke-dasharray: 4 2;
|
||||
stroke-opacity: 1.0;
|
||||
}
|
||||
.net_gobrechts_d3_force text.label,
|
||||
.net_gobrechts_d3_force text.labelCircular {
|
||||
fill: black;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.net_gobrechts_d3_force text.label{
|
||||
text-anchor: middle;
|
||||
}
|
||||
.net_gobrechts_d3_force text.highlighted {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.net_gobrechts_d3_force text.link {
|
||||
font-size: 12px;
|
||||
fill: blue;
|
||||
cursor: pointer;
|
||||
}
|
||||
.net_gobrechts_d3_force line.link,
|
||||
.net_gobrechts_d3_force path.link {
|
||||
fill: none;
|
||||
stroke: #bbb;
|
||||
stroke-width: 1.5px;
|
||||
stroke-opacity: 0.8;
|
||||
}
|
||||
.net_gobrechts_d3_force line.dotted,
|
||||
.net_gobrechts_d3_force path.dotted {
|
||||
stroke-dasharray: .01 3;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.net_gobrechts_d3_force line.dashed,
|
||||
.net_gobrechts_d3_force path.dashed {
|
||||
stroke-dasharray: 4 2;
|
||||
}
|
||||
.net_gobrechts_d3_force line.highlighted,
|
||||
.net_gobrechts_d3_force path.highlighted {
|
||||
stroke: #555 !important;
|
||||
stroke-opacity: 1.0;
|
||||
}
|
||||
.net_gobrechts_d3_force marker.normal {
|
||||
stroke: none;
|
||||
fill: #bbb;
|
||||
}
|
||||
.net_gobrechts_d3_force marker.highlighted {
|
||||
stroke: none;
|
||||
fill: #555;
|
||||
}
|
||||
.net_gobrechts_d3_force .graphOverlay,
|
||||
.net_gobrechts_d3_force .graphOverlaySizeHelper {
|
||||
fill: none;
|
||||
pointer-events: all;
|
||||
}
|
||||
.net_gobrechts_d3_force .lasso path {
|
||||
stroke: #505050;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
.net_gobrechts_d3_force .lasso .drawn {
|
||||
fill-opacity: 0.05 ;
|
||||
}
|
||||
.net_gobrechts_d3_force .lasso .loop_close {
|
||||
fill: none;
|
||||
stroke-dasharray: 4,4;
|
||||
}
|
||||
.net_gobrechts_d3_force .lasso .origin {
|
||||
fill: #3399FF;
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
.net_gobrechts_d3_force .loading rect {
|
||||
fill: black;
|
||||
fill-opacity: 0.2;
|
||||
}
|
||||
.net_gobrechts_d3_force .loading text {
|
||||
fill: white;
|
||||
font-size: 36px;
|
||||
text-anchor: middle;
|
||||
}
|
||||
.net_gobrechts_d3_force_tooltip {
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
background-color: silver;
|
||||
opacity: 0.9;
|
||||
width: 150px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
z-index: 100000;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize {
|
||||
border: 1px solid silver;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
background-color:white;
|
||||
box-shadow: 1px 1px 6px #666;
|
||||
z-index: 200000;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize .drag {
|
||||
border: 1px dashed silver;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
cursor: move;
|
||||
font-weight: bold;
|
||||
height: 24px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize .title {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize .close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border: none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize tr.hidden {
|
||||
display: none;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize td {
|
||||
padding: 1px;
|
||||
font-size: 12px;
|
||||
vertical-align: middle;
|
||||
border: none;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize .label {
|
||||
text-align: right;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize .warning {
|
||||
background-color: orange;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize input,
|
||||
.net_gobrechts_d3_force_customize select,
|
||||
.net_gobrechts_d3_force_customize textarea,
|
||||
.net_gobrechts_d3_force_customize a {
|
||||
border: 1px solid silver;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize a {
|
||||
border: 1px solid transparent;
|
||||
color: blue;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize input:focus,
|
||||
.net_gobrechts_d3_force_customize select:focus,
|
||||
.net_gobrechts_d3_force_customize textarea:focus,
|
||||
.net_gobrechts_d3_force_customize a:focus {
|
||||
outline: none !important;
|
||||
border: 1px solid blue !important;
|
||||
background-color: yellow !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.net_gobrechts_d3_force_customize textarea {
|
||||
font-size: 10px !important;
|
||||
padding: 2px;
|
||||
width: 160px;
|
||||
height: 85px;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
4175
share/public/javascripts/d3-force.js
vendored
Normal file
4175
share/public/javascripts/d3-force.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
share/public/javascripts/d3.min.js
vendored
9
share/public/javascripts/d3.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,184 +1,28 @@
|
||||
<script>
|
||||
|
||||
var winHeight = window.innerHeight;
|
||||
var winWidth = window.innerWidth;
|
||||
|
||||
// links in the initial tree drawing use this generator
|
||||
var treeLink = d3.svg.diagonal.radial()
|
||||
.projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
|
||||
|
||||
// actual device neighbor links use this generator
|
||||
var neighLink = d3.svg.diagonal.radial();
|
||||
|
||||
// store x,y for all circles on the map
|
||||
var loc = {};
|
||||
// store actual links between all nodes
|
||||
var neighbors_data = {};
|
||||
|
||||
// main SVG background, with support for pan/zoom
|
||||
var svg = d3.select("#netmap_pane").append("svg")
|
||||
.attr("width", winWidth - 50)
|
||||
.attr("height", winHeight - 100)
|
||||
.attr("pointer-events", "all")
|
||||
.append('g')
|
||||
.call(d3.behavior.zoom().on("zoom", redraw))
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + winHeight / 2 + "," + winHeight / 2 + ")");
|
||||
|
||||
// this is the image background
|
||||
// XXX there must be a way to discover the radial tree's size?
|
||||
svg.append('rect')
|
||||
.attr("x", (0 - (winHeight * 2)))
|
||||
.attr('width', "400%")
|
||||
.attr("y", (0 - (winHeight * 2)))
|
||||
.attr('height', "400%")
|
||||
.attr('fill', 'white');
|
||||
|
||||
// handle pan and zoom
|
||||
function redraw() {
|
||||
svg.attr("transform",
|
||||
"translate(" + d3.event.translate + ")"
|
||||
+ "scale(" + d3.event.scale + ")"
|
||||
+ "translate(" + (winHeight / 2) + "," + (winHeight / 2) + ")");
|
||||
}
|
||||
|
||||
// save the x,y of an element into the loc dictionary
|
||||
function recordLocation(d,i) {
|
||||
var rect = this.getBoundingClientRect();
|
||||
loc[d.ip] = {
|
||||
'x': (rect.left + ((rect.right - rect.left) / 2))
|
||||
,'y': (rect.top + ((rect.bottom - rect.top) / 2))
|
||||
};
|
||||
}
|
||||
|
||||
// convert a device ip to a valid CSS class name
|
||||
function to_class(ip) { return 'nd_' + ip.replace(/[:.]/g, "_") }
|
||||
|
||||
// handler for clicking on a circle - redirect to that device's netmap
|
||||
function circleClick(d) {
|
||||
window.location = '[% uri_for('/device') %]?tab=netmap'
|
||||
+ '&q=' + d.ip
|
||||
+ '&depth=[% params.depth | uri %]'
|
||||
+ '&vlan=[% params.vlan | uri %]';
|
||||
}
|
||||
|
||||
// handler for mouseover on a circle - show that device's real neighbors
|
||||
function circleOver(d) {
|
||||
$('.link').hide();
|
||||
$('path.' + to_class(d.ip)).show();
|
||||
$(this).css('cursor', 'pointer');
|
||||
|
||||
$.each(neighbors_data[d.ip], function(idx, target) {
|
||||
if (! (target in loc)) { return true }
|
||||
$('circle.' + to_class(target)).css('fill', '#e96cfa');
|
||||
});
|
||||
}
|
||||
|
||||
// handler for mouseout on a circle - hide real neighbours and show treeLinks
|
||||
function circleOut(d) {
|
||||
$.each(neighbors_data[d.ip], function(idx, target) {
|
||||
if (! (target in loc)) { return true }
|
||||
$('circle.' + to_class(target)).css('fill', '#fff');
|
||||
});
|
||||
|
||||
$(this).css('cursor', 'auto');
|
||||
$('path.' + to_class(d.ip)).hide();
|
||||
$('.link').show();
|
||||
}
|
||||
|
||||
// load all device connections into neighbors_data dictionary
|
||||
$.getJSON('[% uri_for('/ajax/data/device/alldevicelinks') %]', function(data) {
|
||||
neighbors_data = data;
|
||||
$.getJSON('[% uri_for('/ajax/data/device/netmap') %]', function(data) {
|
||||
function resizeGraphContainer() {
|
||||
setTimeout(function(){
|
||||
var graph = jQuery('#netmap_pane');
|
||||
var toc = jQuery('#dw__toc');
|
||||
netmap_pane.width(
|
||||
parseInt(graph.parent().css('width')) -
|
||||
( toc.css('float') === 'right' && parseInt(toc.css('height')) > 30 ? parseInt(toc.css('width')) : 0 )
|
||||
).resume();
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// draw the tree
|
||||
d3.json("[% uri_for('/ajax/data/device/netmap') %]?"
|
||||
+ '&q=[% params.q | uri %]'
|
||||
+ '&depth=[% params.depth | uri %]'
|
||||
+ '&vlan=[% params.vlan | uri %]', function(error, root) {
|
||||
var tree = d3.layout.tree()
|
||||
// magic number "8" for scaling (network depth). seems to make things look right for me.
|
||||
.size([360, (winHeight / 8 * (root['scale'] || 0))])
|
||||
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
|
||||
|
||||
var nodes = tree.nodes(root),
|
||||
links = tree.links(nodes);
|
||||
|
||||
var link = svg.selectAll(".link")
|
||||
.data(links)
|
||||
.enter().append("path")
|
||||
.attr("class", "link")
|
||||
.attr("d", treeLink);
|
||||
|
||||
var node = svg.selectAll(".node")
|
||||
.data(nodes)
|
||||
.enter().append("g")
|
||||
.attr("class", "node")
|
||||
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; });
|
||||
|
||||
// begin to draw...
|
||||
d3.select("#nd_waiting").remove();
|
||||
|
||||
node.append("circle")
|
||||
.attr("r", 4.5)
|
||||
// circle has class name of its device, so we can show/hide it
|
||||
.attr("class", function(d) { return to_class(d.ip) })
|
||||
// store the x,y of every circle we've just drawn
|
||||
.each(recordLocation)
|
||||
// handlers for mouse interaction with the circles
|
||||
.on("click", circleClick)
|
||||
.on("mouseover", circleOver)
|
||||
.on("mouseout", circleOut);
|
||||
|
||||
node.append("text")
|
||||
.attr("dy", ".31em")
|
||||
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
|
||||
.attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; })
|
||||
.text(function(d) { return d.name; });
|
||||
|
||||
// reorient text on the root node
|
||||
svg.select(".node")
|
||||
.attr("transform", function(d) {
|
||||
return d.x < 180 ? "rotate(0)translate(0)" : "rotate(180)translate(0)"; });
|
||||
|
||||
// key (ip) of the root node in our locations store
|
||||
var rootname = svg.select(".node").data()[0].ip;
|
||||
// reformatted neighbors_data for the real neighbor links
|
||||
var neighbors = [];
|
||||
|
||||
// need to build neighbors array only after we have built loc dictionary,
|
||||
// after drawing the circles and storing their x,y
|
||||
$.each(neighbors_data, function(key, val) {
|
||||
if (! (key in loc)) { return true }
|
||||
|
||||
$.each(val, function(idx, ip) {
|
||||
if (! (ip in loc)) { return true }
|
||||
|
||||
neighbors.push({
|
||||
'source': {
|
||||
'ip': key
|
||||
,'x': loc[key]['x']
|
||||
,'y': loc[key]['y']
|
||||
}
|
||||
,'target': {
|
||||
'ip': ip
|
||||
,'x': loc[ip]['x']
|
||||
,'y': loc[ip]['y']
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// insert Netdisco neighbor links below circles but above tree links
|
||||
svg.selectAll(".neighbor")
|
||||
.data(neighbors)
|
||||
.enter().insert("path", ".node")
|
||||
// add class name of source device, so we can show/hide the link
|
||||
// (also "neighbor" class)
|
||||
.attr("class", function(d) { return ("neighbor " + to_class( d.source.ip )) })
|
||||
.attr("d", neighLink)
|
||||
.attr("transform", "translate(-" + loc[rootname]['x'] + ",-" + loc[rootname]['y'] + ")");
|
||||
jQuery(document).ready(function() {
|
||||
window.netmap_pane = netGobrechtsD3Force('netmap_pane')
|
||||
.debug(true)
|
||||
// .lassoMode(true)
|
||||
.start(data);
|
||||
jQuery(window).on("resize", resizeGraphContainer);
|
||||
// jQuery('#dw__toc h3').on("click", resizeGraphContainer);
|
||||
$('#nd_waiting').hide();
|
||||
resizeGraphContainer();
|
||||
});
|
||||
|
||||
}); // jquery getJSON for all connections
|
||||
|
||||
// vim: ft=javascript
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<script type="text/javascript" src="[% uri_base %]/javascripts/underscore.min.js"></script>
|
||||
<script type="text/javascript" src="[% uri_base %]/javascripts/jquery.qtip.min.js"></script>
|
||||
<script type="text/javascript" src="[% uri_base %]/javascripts/d3.min.js"></script>
|
||||
<script type="text/javascript" src="[% uri_base %]/javascripts/d3-force.js"></script>
|
||||
<script type="text/javascript" src="[% uri_base %]/javascripts/toastr.js"></script>
|
||||
<script type="text/javascript" src="[% uri_base %]/javascripts/jquery.floatThead.js"></script>
|
||||
<script type="text/javascript" src="[% uri_base %]/javascripts/daterangepicker.js"></script>
|
||||
@@ -52,6 +53,7 @@
|
||||
<link rel="stylesheet" href="[% uri_base %]/css/smoothness/jquery-ui.custom.min.css"/>
|
||||
<link rel="stylesheet" href="[% uri_base %]/css/font-awesome.min.css"/>
|
||||
<link rel="stylesheet" href="[% uri_base %]/css/toastr.css"/>
|
||||
<link rel="stylesheet" href="[% uri_base %]/css/d3-force.css"/>
|
||||
<link rel="stylesheet" href="[% uri_base %]/css/netdisco.css"/>
|
||||
<link rel="stylesheet" href="[% uri_base %]/css/bootstrap-tree.css"/>
|
||||
<link rel="stylesheet" href="[% uri_base %]/css/daterangepicker-bs2.css"/>
|
||||
|
||||
Reference in New Issue
Block a user